Merge "UsageStatsBackup Bug Fix"
diff --git a/api/current.txt b/api/current.txt
index 47a5242..5c0628c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -28,7 +28,6 @@
     field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD";
     field public static final java.lang.String BIND_MIDI_DEVICE_SERVICE = "android.permission.BIND_MIDI_DEVICE_SERVICE";
     field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE";
-    field public static final java.lang.String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE";
     field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
     field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE";
     field public static final java.lang.String BIND_QUICK_SETTINGS_TILE = "android.permission.BIND_QUICK_SETTINGS_TILE";
@@ -8088,6 +8087,7 @@
     field public static final int BIND_ALLOW_OOM_MANAGEMENT = 16; // 0x10
     field public static final int BIND_AUTO_CREATE = 1; // 0x1
     field public static final int BIND_DEBUG_UNBIND = 2; // 0x2
+    field public static final int BIND_EXTERNAL_SERVICE = -2147483648; // 0x80000000
     field public static final int BIND_IMPORTANT = 64; // 0x40
     field public static final int BIND_NOT_FOREGROUND = 4; // 0x4
     field public static final int BIND_WAIVE_PRIORITY = 32; // 0x20
@@ -9943,6 +9943,7 @@
     method public int describeContents();
     method public void dump(android.util.Printer, java.lang.String);
     field public static final android.os.Parcelable.Creator<android.content.pm.ServiceInfo> CREATOR;
+    field public static final int FLAG_EXTERNAL_SERVICE = 4; // 0x4
     field public static final int FLAG_ISOLATED_PROCESS = 2; // 0x2
     field public static final int FLAG_SINGLE_USER = 1073741824; // 0x40000000
     field public static final int FLAG_STOP_WITH_TASK = 1; // 0x1
@@ -32110,6 +32111,7 @@
     field public static final java.lang.String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
     field public static final java.lang.String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
     field public static final java.lang.String ACTION_HOME_SETTINGS = "android.settings.HOME_SETTINGS";
+    field public static final java.lang.String ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS = "android.settings.IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS";
     field public static final java.lang.String ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS = "android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS";
     field public static final java.lang.String ACTION_INPUT_METHOD_SETTINGS = "android.settings.INPUT_METHOD_SETTINGS";
     field public static final java.lang.String ACTION_INPUT_METHOD_SUBTYPE_SETTINGS = "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
@@ -34366,6 +34368,9 @@
     ctor public MediaBrowserService.BrowserRoot(java.lang.String, android.os.Bundle);
     method public android.os.Bundle getExtras();
     method public java.lang.String getRootId();
+    field public static final java.lang.String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+    field public static final java.lang.String EXTRA_RECENT = "android.service.media.extra.RECENT";
+    field public static final java.lang.String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
   }
 
   public class MediaBrowserService.Result {
@@ -34419,39 +34424,6 @@
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.ConditionProviderService";
   }
 
-  public abstract class NotificationAssistantService extends android.service.notification.NotificationListenerService {
-    ctor public NotificationAssistantService();
-    method public final void adjustImportance(java.lang.String, android.service.notification.NotificationAssistantService.Adjustment);
-    method public final void clearAnnotation(java.lang.String);
-    method public void onNotificationActionClick(java.lang.String, long, int);
-    method public void onNotificationClick(java.lang.String, long);
-    method public abstract android.service.notification.NotificationAssistantService.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean);
-    method public void onNotificationRemoved(java.lang.String, long, int);
-    method public void onNotificationVisibilityChanged(java.lang.String, long, boolean);
-    method public final void setAnnotation(java.lang.String, android.app.Notification);
-    field public static final int REASON_APP_CANCEL = 8; // 0x8
-    field public static final int REASON_APP_CANCEL_ALL = 9; // 0x9
-    field public static final int REASON_DELEGATE_CANCEL = 2; // 0x2
-    field public static final int REASON_DELEGATE_CANCEL_ALL = 3; // 0x3
-    field public static final int REASON_DELEGATE_CLICK = 1; // 0x1
-    field public static final int REASON_DELEGATE_ERROR = 4; // 0x4
-    field public static final int REASON_GROUP_OPTIMIZATION = 13; // 0xd
-    field public static final int REASON_GROUP_SUMMARY_CANCELED = 12; // 0xc
-    field public static final int REASON_LISTENER_CANCEL = 10; // 0xa
-    field public static final int REASON_LISTENER_CANCEL_ALL = 11; // 0xb
-    field public static final int REASON_PACKAGE_BANNED = 7; // 0x7
-    field public static final int REASON_PACKAGE_CHANGED = 5; // 0x5
-    field public static final int REASON_PACKAGE_SUSPENDED = 15; // 0xf
-    field public static final int REASON_PROFILE_TURNED_OFF = 16; // 0x10
-    field public static final int REASON_TOPIC_BANNED = 14; // 0xe
-    field public static final int REASON_USER_STOPPED = 6; // 0x6
-    field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
-  }
-
-  public class NotificationAssistantService.Adjustment {
-    ctor public NotificationAssistantService.Adjustment(int, java.lang.CharSequence, android.net.Uri);
-  }
-
   public abstract class NotificationListenerService extends android.app.Service {
     ctor public NotificationListenerService();
     method public final void cancelAllNotifications();
@@ -36472,6 +36444,7 @@
     field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
     field public static final java.lang.String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool";
     field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool";
+    field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int";
     field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool";
     field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool";
     field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool";
@@ -40067,7 +40040,6 @@
     method public static android.util.LocaleList getDefault();
     method public static android.util.LocaleList getEmptyLocaleList();
     method public java.util.Locale getFirstMatch(java.lang.String[]);
-    method public java.util.Locale getPrimary();
     method public int indexOf(java.util.Locale);
     method public boolean isEmpty();
     method public static void setDefault(android.util.LocaleList);
@@ -50205,13 +50177,16 @@
     method public static java.lang.Class<?> forName(java.lang.String, boolean, java.lang.ClassLoader) throws java.lang.ClassNotFoundException;
     method public A getAnnotation(java.lang.Class<A>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
     method public java.lang.String getCanonicalName();
     method public java.lang.ClassLoader getClassLoader();
     method public java.lang.Class<?>[] getClasses();
     method public java.lang.Class<?> getComponentType();
     method public java.lang.reflect.Constructor<T> getConstructor(java.lang.Class<?>...) throws java.lang.NoSuchMethodException, java.lang.SecurityException;
     method public java.lang.reflect.Constructor<?>[] getConstructors() throws java.lang.SecurityException;
+    method public T getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public java.lang.Class<?>[] getDeclaredClasses();
     method public java.lang.reflect.Constructor<T> getDeclaredConstructor(java.lang.Class<?>...) throws java.lang.NoSuchMethodException, java.lang.SecurityException;
     method public java.lang.reflect.Constructor<?>[] getDeclaredConstructors() throws java.lang.SecurityException;
@@ -50742,7 +50717,10 @@
   public class Package implements java.lang.reflect.AnnotatedElement {
     method public A getAnnotation(java.lang.Class<A>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
+    method public java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public java.lang.String getImplementationTitle();
     method public java.lang.String getImplementationVendor();
     method public java.lang.String getImplementationVersion();
@@ -51436,7 +51414,10 @@
     ctor protected AccessibleObject();
     method public T getAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
+    method public java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public boolean isAccessible();
     method public boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>);
     method public static void setAccessible(java.lang.reflect.AccessibleObject[], boolean) throws java.lang.SecurityException;
@@ -51446,7 +51427,10 @@
   public abstract interface AnnotatedElement {
     method public abstract T getAnnotation(java.lang.Class<T>);
     method public abstract java.lang.annotation.Annotation[] getAnnotations();
+    method public abstract T[] getAnnotationsByType(java.lang.Class<T>);
+    method public abstract java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public abstract java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public abstract T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public abstract boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>);
   }
 
diff --git a/api/system-current.txt b/api/system-current.txt
index edcd9e6..2adb1e6 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -41,7 +41,6 @@
     field public static final java.lang.String BIND_KEYGUARD_APPWIDGET = "android.permission.BIND_KEYGUARD_APPWIDGET";
     field public static final java.lang.String BIND_MIDI_DEVICE_SERVICE = "android.permission.BIND_MIDI_DEVICE_SERVICE";
     field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE";
-    field public static final java.lang.String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE";
     field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
     field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE";
     field public static final java.lang.String BIND_QUICK_SETTINGS_TILE = "android.permission.BIND_QUICK_SETTINGS_TILE";
@@ -10346,6 +10345,7 @@
     method public int describeContents();
     method public void dump(android.util.Printer, java.lang.String);
     field public static final android.os.Parcelable.Creator<android.content.pm.ServiceInfo> CREATOR;
+    field public static final int FLAG_EXTERNAL_SERVICE = 4; // 0x4
     field public static final int FLAG_ISOLATED_PROCESS = 2; // 0x2
     field public static final int FLAG_SINGLE_USER = 1073741824; // 0x40000000
     field public static final int FLAG_STOP_WITH_TASK = 1; // 0x1
@@ -34520,6 +34520,7 @@
     field public static final java.lang.String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
     field public static final java.lang.String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
     field public static final java.lang.String ACTION_HOME_SETTINGS = "android.settings.HOME_SETTINGS";
+    field public static final java.lang.String ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS = "android.settings.IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS";
     field public static final java.lang.String ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS = "android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS";
     field public static final java.lang.String ACTION_INPUT_METHOD_SETTINGS = "android.settings.INPUT_METHOD_SETTINGS";
     field public static final java.lang.String ACTION_INPUT_METHOD_SUBTYPE_SETTINGS = "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
@@ -36777,6 +36778,9 @@
     ctor public MediaBrowserService.BrowserRoot(java.lang.String, android.os.Bundle);
     method public android.os.Bundle getExtras();
     method public java.lang.String getRootId();
+    field public static final java.lang.String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+    field public static final java.lang.String EXTRA_RECENT = "android.service.media.extra.RECENT";
+    field public static final java.lang.String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
   }
 
   public class MediaBrowserService.Result {
@@ -36834,13 +36838,11 @@
   public abstract class NotificationAssistantService extends android.service.notification.NotificationListenerService {
     ctor public NotificationAssistantService();
     method public final void adjustImportance(java.lang.String, android.service.notification.NotificationAssistantService.Adjustment);
-    method public final void clearAnnotation(java.lang.String);
     method public void onNotificationActionClick(java.lang.String, long, int);
     method public void onNotificationClick(java.lang.String, long);
     method public abstract android.service.notification.NotificationAssistantService.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean);
     method public void onNotificationRemoved(java.lang.String, long, int);
     method public void onNotificationVisibilityChanged(java.lang.String, long, boolean);
-    method public final void setAnnotation(java.lang.String, android.app.Notification);
     field public static final int REASON_APP_CANCEL = 8; // 0x8
     field public static final int REASON_APP_CANCEL_ALL = 9; // 0x9
     field public static final int REASON_DELEGATE_CANCEL = 2; // 0x2
@@ -39057,6 +39059,7 @@
     field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
     field public static final java.lang.String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool";
     field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool";
+    field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int";
     field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool";
     field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool";
     field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool";
@@ -42716,7 +42719,6 @@
     method public static android.util.LocaleList getDefault();
     method public static android.util.LocaleList getEmptyLocaleList();
     method public java.util.Locale getFirstMatch(java.lang.String[]);
-    method public java.util.Locale getPrimary();
     method public int indexOf(java.util.Locale);
     method public boolean isEmpty();
     method public static void setDefault(android.util.LocaleList);
@@ -53190,13 +53192,16 @@
     method public static java.lang.Class<?> forName(java.lang.String, boolean, java.lang.ClassLoader) throws java.lang.ClassNotFoundException;
     method public A getAnnotation(java.lang.Class<A>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
     method public java.lang.String getCanonicalName();
     method public java.lang.ClassLoader getClassLoader();
     method public java.lang.Class<?>[] getClasses();
     method public java.lang.Class<?> getComponentType();
     method public java.lang.reflect.Constructor<T> getConstructor(java.lang.Class<?>...) throws java.lang.NoSuchMethodException, java.lang.SecurityException;
     method public java.lang.reflect.Constructor<?>[] getConstructors() throws java.lang.SecurityException;
+    method public T getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public java.lang.Class<?>[] getDeclaredClasses();
     method public java.lang.reflect.Constructor<T> getDeclaredConstructor(java.lang.Class<?>...) throws java.lang.NoSuchMethodException, java.lang.SecurityException;
     method public java.lang.reflect.Constructor<?>[] getDeclaredConstructors() throws java.lang.SecurityException;
@@ -53727,7 +53732,10 @@
   public class Package implements java.lang.reflect.AnnotatedElement {
     method public A getAnnotation(java.lang.Class<A>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
+    method public java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public java.lang.String getImplementationTitle();
     method public java.lang.String getImplementationVendor();
     method public java.lang.String getImplementationVersion();
@@ -54421,7 +54429,10 @@
     ctor protected AccessibleObject();
     method public T getAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
+    method public java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public boolean isAccessible();
     method public boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>);
     method public static void setAccessible(java.lang.reflect.AccessibleObject[], boolean) throws java.lang.SecurityException;
@@ -54431,7 +54442,10 @@
   public abstract interface AnnotatedElement {
     method public abstract T getAnnotation(java.lang.Class<T>);
     method public abstract java.lang.annotation.Annotation[] getAnnotations();
+    method public abstract T[] getAnnotationsByType(java.lang.Class<T>);
+    method public abstract java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public abstract java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public abstract T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public abstract boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>);
   }
 
diff --git a/api/test-current.txt b/api/test-current.txt
index 4dbad61..7abc6d4 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -28,7 +28,6 @@
     field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD";
     field public static final java.lang.String BIND_MIDI_DEVICE_SERVICE = "android.permission.BIND_MIDI_DEVICE_SERVICE";
     field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE";
-    field public static final java.lang.String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE";
     field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
     field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE";
     field public static final java.lang.String BIND_QUICK_SETTINGS_TILE = "android.permission.BIND_QUICK_SETTINGS_TILE";
@@ -8092,6 +8091,7 @@
     field public static final int BIND_ALLOW_OOM_MANAGEMENT = 16; // 0x10
     field public static final int BIND_AUTO_CREATE = 1; // 0x1
     field public static final int BIND_DEBUG_UNBIND = 2; // 0x2
+    field public static final int BIND_EXTERNAL_SERVICE = -2147483648; // 0x80000000
     field public static final int BIND_IMPORTANT = 64; // 0x40
     field public static final int BIND_NOT_FOREGROUND = 4; // 0x4
     field public static final int BIND_WAIVE_PRIORITY = 32; // 0x20
@@ -9951,6 +9951,7 @@
     method public int describeContents();
     method public void dump(android.util.Printer, java.lang.String);
     field public static final android.os.Parcelable.Creator<android.content.pm.ServiceInfo> CREATOR;
+    field public static final int FLAG_EXTERNAL_SERVICE = 4; // 0x4
     field public static final int FLAG_ISOLATED_PROCESS = 2; // 0x2
     field public static final int FLAG_SINGLE_USER = 1073741824; // 0x40000000
     field public static final int FLAG_STOP_WITH_TASK = 1; // 0x1
@@ -32123,6 +32124,7 @@
     field public static final java.lang.String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
     field public static final java.lang.String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
     field public static final java.lang.String ACTION_HOME_SETTINGS = "android.settings.HOME_SETTINGS";
+    field public static final java.lang.String ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS = "android.settings.IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS";
     field public static final java.lang.String ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS = "android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS";
     field public static final java.lang.String ACTION_INPUT_METHOD_SETTINGS = "android.settings.INPUT_METHOD_SETTINGS";
     field public static final java.lang.String ACTION_INPUT_METHOD_SUBTYPE_SETTINGS = "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
@@ -34381,6 +34383,9 @@
     ctor public MediaBrowserService.BrowserRoot(java.lang.String, android.os.Bundle);
     method public android.os.Bundle getExtras();
     method public java.lang.String getRootId();
+    field public static final java.lang.String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+    field public static final java.lang.String EXTRA_RECENT = "android.service.media.extra.RECENT";
+    field public static final java.lang.String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
   }
 
   public class MediaBrowserService.Result {
@@ -34434,39 +34439,6 @@
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.ConditionProviderService";
   }
 
-  public abstract class NotificationAssistantService extends android.service.notification.NotificationListenerService {
-    ctor public NotificationAssistantService();
-    method public final void adjustImportance(java.lang.String, android.service.notification.NotificationAssistantService.Adjustment);
-    method public final void clearAnnotation(java.lang.String);
-    method public void onNotificationActionClick(java.lang.String, long, int);
-    method public void onNotificationClick(java.lang.String, long);
-    method public abstract android.service.notification.NotificationAssistantService.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean);
-    method public void onNotificationRemoved(java.lang.String, long, int);
-    method public void onNotificationVisibilityChanged(java.lang.String, long, boolean);
-    method public final void setAnnotation(java.lang.String, android.app.Notification);
-    field public static final int REASON_APP_CANCEL = 8; // 0x8
-    field public static final int REASON_APP_CANCEL_ALL = 9; // 0x9
-    field public static final int REASON_DELEGATE_CANCEL = 2; // 0x2
-    field public static final int REASON_DELEGATE_CANCEL_ALL = 3; // 0x3
-    field public static final int REASON_DELEGATE_CLICK = 1; // 0x1
-    field public static final int REASON_DELEGATE_ERROR = 4; // 0x4
-    field public static final int REASON_GROUP_OPTIMIZATION = 13; // 0xd
-    field public static final int REASON_GROUP_SUMMARY_CANCELED = 12; // 0xc
-    field public static final int REASON_LISTENER_CANCEL = 10; // 0xa
-    field public static final int REASON_LISTENER_CANCEL_ALL = 11; // 0xb
-    field public static final int REASON_PACKAGE_BANNED = 7; // 0x7
-    field public static final int REASON_PACKAGE_CHANGED = 5; // 0x5
-    field public static final int REASON_PACKAGE_SUSPENDED = 15; // 0xf
-    field public static final int REASON_PROFILE_TURNED_OFF = 16; // 0x10
-    field public static final int REASON_TOPIC_BANNED = 14; // 0xe
-    field public static final int REASON_USER_STOPPED = 6; // 0x6
-    field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
-  }
-
-  public class NotificationAssistantService.Adjustment {
-    ctor public NotificationAssistantService.Adjustment(int, java.lang.CharSequence, android.net.Uri);
-  }
-
   public abstract class NotificationListenerService extends android.app.Service {
     ctor public NotificationListenerService();
     method public final void cancelAllNotifications();
@@ -36487,6 +36459,7 @@
     field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
     field public static final java.lang.String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool";
     field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool";
+    field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int";
     field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool";
     field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool";
     field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool";
@@ -40084,7 +40057,6 @@
     method public static android.util.LocaleList getDefault();
     method public static android.util.LocaleList getEmptyLocaleList();
     method public java.util.Locale getFirstMatch(java.lang.String[]);
-    method public java.util.Locale getPrimary();
     method public int indexOf(java.util.Locale);
     method public boolean isEmpty();
     method public static void setDefault(android.util.LocaleList);
@@ -50222,13 +50194,16 @@
     method public static java.lang.Class<?> forName(java.lang.String, boolean, java.lang.ClassLoader) throws java.lang.ClassNotFoundException;
     method public A getAnnotation(java.lang.Class<A>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
     method public java.lang.String getCanonicalName();
     method public java.lang.ClassLoader getClassLoader();
     method public java.lang.Class<?>[] getClasses();
     method public java.lang.Class<?> getComponentType();
     method public java.lang.reflect.Constructor<T> getConstructor(java.lang.Class<?>...) throws java.lang.NoSuchMethodException, java.lang.SecurityException;
     method public java.lang.reflect.Constructor<?>[] getConstructors() throws java.lang.SecurityException;
+    method public T getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public java.lang.Class<?>[] getDeclaredClasses();
     method public java.lang.reflect.Constructor<T> getDeclaredConstructor(java.lang.Class<?>...) throws java.lang.NoSuchMethodException, java.lang.SecurityException;
     method public java.lang.reflect.Constructor<?>[] getDeclaredConstructors() throws java.lang.SecurityException;
@@ -50759,7 +50734,10 @@
   public class Package implements java.lang.reflect.AnnotatedElement {
     method public A getAnnotation(java.lang.Class<A>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
+    method public java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public java.lang.String getImplementationTitle();
     method public java.lang.String getImplementationVendor();
     method public java.lang.String getImplementationVersion();
@@ -51453,7 +51431,10 @@
     ctor protected AccessibleObject();
     method public T getAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getAnnotations();
+    method public T[] getAnnotationsByType(java.lang.Class<T>);
+    method public java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public boolean isAccessible();
     method public boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>);
     method public static void setAccessible(java.lang.reflect.AccessibleObject[], boolean) throws java.lang.SecurityException;
@@ -51463,7 +51444,10 @@
   public abstract interface AnnotatedElement {
     method public abstract T getAnnotation(java.lang.Class<T>);
     method public abstract java.lang.annotation.Annotation[] getAnnotations();
+    method public abstract T[] getAnnotationsByType(java.lang.Class<T>);
+    method public abstract java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>);
     method public abstract java.lang.annotation.Annotation[] getDeclaredAnnotations();
+    method public abstract T[] getDeclaredAnnotationsByType(java.lang.Class<T>);
     method public abstract boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>);
   }
 
diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java
index 9d81c43..acc68cf 100644
--- a/cmds/am/src/com/android/commands/am/Am.java
+++ b/cmds/am/src/com/android/commands/am/Am.java
@@ -124,7 +124,7 @@
         PrintWriter pw = new PrintWriter(out);
         pw.println(
                 "usage: am [subcommand] [options]\n" +
-                "usage: am start [-D] [-W] [-P <FILE>] [--start-profiler <FILE>]\n" +
+                "usage: am start [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]\n" +
                 "               [--sampling INTERVAL] [-R COUNT] [-S]\n" +
                 "               [--track-allocation] [--user <USER_ID> | current] <INTENT>\n" +
                 "       am startservice [--user <USER_ID> | current] <INTENT>\n" +
@@ -183,6 +183,7 @@
                 "\n" +
                 "am start: start an Activity.  Options are:\n" +
                 "    -D: enable debugging\n" +
+                "    -N: enable native debugging\n" +
                 "    -W: wait for launch to complete\n" +
                 "    --start-profiler <FILE>: start profiler and send results to <FILE>\n" +
                 "    --sampling INTERVAL: use sample profiling with INTERVAL microseconds\n" +
@@ -478,6 +479,8 @@
             public boolean handleOption(String opt, ShellCommand cmd) {
                 if (opt.equals("-D")) {
                     mStartFlags |= ActivityManager.START_FLAG_DEBUG;
+                } else if (opt.equals("-N")) {
+                    mStartFlags |= ActivityManager.START_FLAG_NATIVE_DEBUGGING;
                 } else if (opt.equals("-W")) {
                     mWaitOption = true;
                 } else if (opt.equals("-P")) {
@@ -1774,18 +1777,33 @@
             System.err.println("Error: invalid input bounds");
             return;
         }
-        resizeStack(stackId, bounds, 0, false);
+        resizeStack(stackId, bounds, 0);
     }
 
     private void runStackResizeAnimated() throws Exception {
         String stackIdStr = nextArgRequired();
         int stackId = Integer.valueOf(stackIdStr);
-        final Rect bounds = getBounds();
-        if (bounds == null) {
-            System.err.println("Error: invalid input bounds");
-            return;
+        final Rect bounds;
+        if ("null".equals(mArgs.peekNextArg())) {
+            bounds = null;
+        } else {
+            bounds = getBounds();
+            if (bounds == null) {
+                System.err.println("Error: invalid input bounds");
+                return;
+            }
         }
-        resizeStack(stackId, bounds, 0, true);
+        resizeStackUnchecked(stackId, bounds, 0, true);
+    }
+
+    private void resizeStackUnchecked(int stackId, Rect bounds, int delayMs, boolean animate) {
+        try {
+            mAm.resizeStack(stackId, bounds, false, false, animate);
+            Thread.sleep(delayMs);
+        } catch (RemoteException e) {
+            showError("Error: resizing stack " + e);
+        } catch (InterruptedException e) {
+        }
     }
 
     private void runStackResizeDocked() throws Exception {
@@ -1802,20 +1820,13 @@
         }
     }
 
-    private void resizeStack(int stackId, Rect bounds, int delayMs, boolean animate)
+    private void resizeStack(int stackId, Rect bounds, int delayMs)
             throws Exception {
         if (bounds == null) {
             showError("Error: invalid input bounds");
             return;
         }
-
-        try {
-            mAm.resizeStack(stackId, bounds, false, false, animate);
-            Thread.sleep(delayMs);
-        } catch (RemoteException e) {
-            showError("Error: resizing stack " + e);
-        } catch (InterruptedException e) {
-        }
+        resizeStackUnchecked(stackId, bounds, delayMs, false);
     }
 
     private void runStackPositionTask() throws Exception {
@@ -1924,7 +1935,7 @@
             maxChange = Math.min(stepSize, currentPoint - minPoint);
             currentPoint -= maxChange;
             setBoundsSide(bounds, side, currentPoint);
-            resizeStack(DOCKED_STACK_ID, bounds, delayMs, false);
+            resizeStack(DOCKED_STACK_ID, bounds, delayMs);
         }
 
         System.out.println("Growing docked stack side=" + side);
@@ -1932,7 +1943,7 @@
             maxChange = Math.min(stepSize, maxPoint - currentPoint);
             currentPoint += maxChange;
             setBoundsSide(bounds, side, currentPoint);
-            resizeStack(DOCKED_STACK_ID, bounds, delayMs, false);
+            resizeStack(DOCKED_STACK_ID, bounds, delayMs);
         }
 
         System.out.println("Back to Original size side=" + side);
@@ -1940,7 +1951,7 @@
             maxChange = Math.min(stepSize, currentPoint - startPoint);
             currentPoint -= maxChange;
             setBoundsSide(bounds, side, currentPoint);
-            resizeStack(DOCKED_STACK_ID, bounds, delayMs, false);
+            resizeStack(DOCKED_STACK_ID, bounds, delayMs);
         }
     }
 
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index cb29419..1eb2fe2 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -245,6 +245,13 @@
     public static final int START_FLAG_TRACK_ALLOCATION = 1<<2;
 
     /**
+     * Flag for IActivityManaqer.startActivity: launch the app with
+     * native debugging support.
+     * @hide
+     */
+    public static final int START_FLAG_NATIVE_DEBUGGING = 1<<3;
+
+    /**
      * Result for IActivityManaqer.broadcastIntent: success!
      * @hide
      */
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 805b203..6424520 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4822,14 +4822,12 @@
     }
 
     private void updateDefaultDensity() {
-        if (mCurDefaultDisplayDpi != Configuration.DENSITY_DPI_UNDEFINED
-                && mCurDefaultDisplayDpi != DisplayMetrics.DENSITY_DEVICE
-                && !mDensityCompatMode) {
-            Slog.i(TAG, "Switching default density from "
-                    + DisplayMetrics.DENSITY_DEVICE + " to "
-                    + mCurDefaultDisplayDpi);
-            DisplayMetrics.DENSITY_DEVICE = mCurDefaultDisplayDpi;
-            Bitmap.setDefaultDensity(DisplayMetrics.DENSITY_DEFAULT);
+        final int densityDpi = mCurDefaultDisplayDpi;
+        if (!mDensityCompatMode
+                && densityDpi != Configuration.DENSITY_DPI_UNDEFINED
+                && densityDpi != DisplayMetrics.DENSITY_DEVICE) {
+            DisplayMetrics.DENSITY_DEVICE = densityDpi;
+            Bitmap.setDefaultDensity(densityDpi);
         }
     }
 
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 1e22bef..da52c1e 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -382,6 +382,13 @@
                     libraryPermittedPath += File.pathSeparator +
                                             System.getProperty("java.library.path");
                 }
+                // DO NOT SHIP: this is a workaround for apps loading native libraries
+                // provided by 3rd party apps using absolute path instead of corresponding
+                // classloader; see http://b/26954419 for example.
+                if (mApplicationInfo.targetSdkVersion <= 23) {
+                    libraryPermittedPath += File.pathSeparator + "/data/app";
+                }
+                // -----------------------------------------------------------------------------
 
                 final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);
 
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 34c90c1..a78076b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3236,6 +3236,7 @@
                 return;
             }
             contentView.setTextViewText(R.id.app_name_text, appName);
+            contentView.setTextColor(R.id.app_name_text, resolveColor());
         }
 
         private void bindSmallIcon(RemoteViews contentView) {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 1247afe..02eb115 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -4109,6 +4109,29 @@
     }
 
     /**
+     * Called by the system to check if a specific accessibility service is disabled by admin.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @param packageName Accessibility service package name that needs to be checked.
+     * @param userHandle user id the admin is running as.
+     * @return true if the accessibility service is permitted, otherwise false.
+     *
+     * @hide
+     */
+    public boolean isAccessibilityServicePermittedByAdmin(@NonNull ComponentName admin,
+            @NonNull String packageName, int userHandle) {
+        if (mService != null) {
+            try {
+                return mService.isAccessibilityServicePermittedByAdmin(admin, packageName,
+                        userHandle);
+            } catch (RemoteException e) {
+                Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e);
+            }
+        }
+        return false;
+    }
+
+    /**
      * Returns the list of accessibility services permitted by the device or profiles
      * owners of this user.
      *
@@ -4188,6 +4211,28 @@
     }
 
     /**
+     * Called by the system to check if a specific input method is disabled by admin.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @param packageName Input method package name that needs to be checked.
+     * @param userHandle user id the admin is running as.
+     * @return true if the input method is permitted, otherwise false.
+     *
+     * @hide
+     */
+    public boolean isInputMethodPermittedByAdmin(@NonNull ComponentName admin,
+            @NonNull String packageName, int userHandle) {
+        if (mService != null) {
+            try {
+                return mService.isInputMethodPermittedByAdmin(admin, packageName, userHandle);
+            } catch (RemoteException e) {
+                Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e);
+            }
+        }
+        return false;
+    }
+
+    /**
      * Returns the list of input methods permitted by the device or profiles
      * owners of the current user.  (*Not* calling user, due to a limitation in InputMethodManager.)
      *
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index b57e1b7..c6a5344 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -174,10 +174,12 @@
     boolean setPermittedAccessibilityServices(in ComponentName admin,in List packageList);
     List getPermittedAccessibilityServices(in ComponentName admin);
     List getPermittedAccessibilityServicesForUser(int userId);
+    boolean isAccessibilityServicePermittedByAdmin(in ComponentName admin, String packageName, int userId);
 
     boolean setPermittedInputMethods(in ComponentName admin,in List packageList);
     List getPermittedInputMethods(in ComponentName admin);
     List getPermittedInputMethodsForCurrentUser();
+    boolean isInputMethodPermittedByAdmin(in ComponentName admin, String packageName, int userId);
 
     boolean setApplicationHidden(in ComponentName admin, in String packageName, boolean hidden);
     boolean isApplicationHidden(in ComponentName admin, in String packageName);
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index b2ca023..5398e7f 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -24,6 +24,7 @@
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.util.Log;
+import static android.util.TimeUtils.formatForLogging;
 
 import java.util.ArrayList;
 
@@ -640,12 +641,14 @@
             }
             JobInfo job = new JobInfo(this);
             if (job.intervalMillis != job.getIntervalMillis()) {
-                Log.w(TAG, "Specified interval is less than minimum interval. Clamped to "
-                        + job.getIntervalMillis());
+                Log.w(TAG, "Specified interval for " + mJobService.getPackageName() + " is "
+                        + formatForLogging(mIntervalMillis) + ". Clamped to " +
+                        formatForLogging(job.getIntervalMillis()));
             }
             if (job.flexMillis != job.getFlexMillis()) {
-                Log.w(TAG, "Specified flex is less than minimum flex. Clamped to "
-                        + job.getFlexMillis());
+                Log.w(TAG, "Specified interval for " + mJobService.getPackageName() + " is "
+                        + formatForLogging(mFlexMillis) + ". Clamped to " +
+                        formatForLogging(job.getFlexMillis()));
             }
             return job;
         }
diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java
index a88aa3125..2937ccc 100644
--- a/core/java/android/app/usage/UsageStats.java
+++ b/core/java/android/app/usage/UsageStats.java
@@ -47,20 +47,6 @@
     public long mLastTimeUsed;
 
     /**
-     * The last time the package was used via implicit, non-user initiated actions (service
-     * was bound, etc).
-     * {@hide}
-     */
-    public long mLastTimeSystemUsed;
-
-    /**
-     * Last time the package was used and the beginning of the idle countdown.
-     * This uses a different timebase that is about how much the device has been in use in general.
-     * {@hide}
-     */
-    public long mBeginIdleTime;
-
-    /**
      * {@hide}
      */
     public long mTotalTimeInForeground;
@@ -89,8 +75,6 @@
         mTotalTimeInForeground = stats.mTotalTimeInForeground;
         mLaunchCount = stats.mLaunchCount;
         mLastEvent = stats.mLastEvent;
-        mBeginIdleTime = stats.mBeginIdleTime;
-        mLastTimeSystemUsed = stats.mLastTimeSystemUsed;
     }
 
     public String getPackageName() {
@@ -127,25 +111,6 @@
     }
 
     /**
-     * @hide
-     * Get the last time this package was used by the system (not the user). This can be different
-     * from {@link #getLastTimeUsed()} when the system binds to one of this package's services.
-     * See {@link System#currentTimeMillis()}.
-     */
-    public long getLastTimeSystemUsed() {
-        return mLastTimeSystemUsed;
-    }
-
-    /**
-     * @hide
-     * Get the last time this package was active, measured in milliseconds. This timestamp
-     * uses a timebase that represents how much the device was used and not wallclock time.
-     */
-    public long getBeginIdleTime() {
-        return mBeginIdleTime;
-    }
-
-    /**
      * Get the total time this package spent in the foreground, measured in milliseconds.
      */
     public long getTotalTimeInForeground() {
@@ -172,8 +137,6 @@
             // regards to their mEndTimeStamp.
             mLastEvent = right.mLastEvent;
             mLastTimeUsed = right.mLastTimeUsed;
-            mBeginIdleTime = right.mBeginIdleTime;
-            mLastTimeSystemUsed = right.mLastTimeSystemUsed;
         }
         mBeginTimeStamp = Math.min(mBeginTimeStamp, right.mBeginTimeStamp);
         mEndTimeStamp = Math.max(mEndTimeStamp, right.mEndTimeStamp);
@@ -195,8 +158,6 @@
         dest.writeLong(mTotalTimeInForeground);
         dest.writeInt(mLaunchCount);
         dest.writeInt(mLastEvent);
-        dest.writeLong(mBeginIdleTime);
-        dest.writeLong(mLastTimeSystemUsed);
     }
 
     public static final Creator<UsageStats> CREATOR = new Creator<UsageStats>() {
@@ -210,8 +171,6 @@
             stats.mTotalTimeInForeground = in.readLong();
             stats.mLaunchCount = in.readInt();
             stats.mLastEvent = in.readInt();
-            stats.mBeginIdleTime = in.readLong();
-            stats.mLastTimeSystemUsed = in.readLong();
             return stats;
         }
 
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index 4135487..9221fbb 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -148,8 +148,7 @@
                 return null;
             }
 
-            if ("com.google.android.gms".equals(mPackageName)
-                    || "com.google.android.syncadapters.contacts".equals(mPackageName)) {
+            if ("com.google.android.gms".equals(mPackageName)) {
                 // They're casting to a concrete subclass, sigh
                 return cursor;
             } else {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 622aad9..4918914 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -342,9 +342,7 @@
      * {@link android.R.attr#isolatedProcess isolated},
      * {@link android.R.attr#externalService external} service.  This binds the service into the
      * calling application's package, rather than the package in which the service is declared.
-     * @hide
      */
-    @SystemApi
     public static final int BIND_EXTERNAL_SERVICE = 0x80000000;
 
     /**
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 83943ad..7dc7401 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -8939,6 +8939,8 @@
                 case ACTION_MEDIA_SCANNER_STARTED:
                 case ACTION_MEDIA_SCANNER_FINISHED:
                 case ACTION_MEDIA_SCANNER_SCAN_FILE:
+                case ACTION_PACKAGE_NEEDS_VERIFICATION:
+                case ACTION_PACKAGE_VERIFIED:
                     // Ignore legacy actions
                     break;
                 default:
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index eecf0de..6bd285a 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -52,7 +52,6 @@
      * Bit in {@link #flags}: If set, the service can be bound and run in the
      * calling application's package, rather than the package in which it is
      * declared.  Set from {@link android.R.attr#externalService} attribute.
-     * @hide
      */
     public static final int FLAG_EXTERNAL_SERVICE = 0x0004;
 
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index 7db5a08..be4f895 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -715,7 +715,7 @@
      * about setLocales() has changed locale directly. */
     private void fixUpLocaleList() {
         if ((locale == null && !mLocaleList.isEmpty()) ||
-                (locale != null && !locale.equals(mLocaleList.getPrimary()))) {
+                (locale != null && !locale.equals(mLocaleList.get(0)))) {
             mLocaleList = new LocaleList(locale);
         }
     }
@@ -1269,7 +1269,7 @@
             localeArray[i] = Locale.forLanguageTag(source.readString());
         }
         mLocaleList = new LocaleList(localeArray);
-        locale = mLocaleList.getPrimary();
+        locale = mLocaleList.get(0);
 
         userSetLocale = (source.readInt()==1);
         touchscreen = source.readInt();
@@ -1435,7 +1435,7 @@
      */
     public void setLocales(@Nullable LocaleList locales) {
         mLocaleList = locales == null ? LocaleList.getEmptyLocaleList() : locales;
-        locale = mLocaleList.getPrimary();
+        locale = mLocaleList.get(0);
         setLayoutDirection(locale);
     }
 
@@ -1900,7 +1900,7 @@
 
         final String localesStr = XmlUtils.readStringAttribute(parser, XML_ATTR_LOCALES);
         configOut.mLocaleList = LocaleList.forLanguageTags(localesStr);
-        configOut.locale = configOut.mLocaleList.getPrimary();
+        configOut.locale = configOut.mLocaleList.get(0);
 
         configOut.touchscreen = XmlUtils.readIntAttribute(parser, XML_ATTR_TOUCHSCREEN,
                 TOUCHSCREEN_UNDEFINED);
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 4967d05..915fae0 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -381,7 +381,7 @@
     private PluralRules getPluralRule() {
         synchronized (sSync) {
             if (mPluralRule == null) {
-                mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().getPrimary());
+                mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().get(0));
             }
             return mPluralRule;
         }
@@ -444,7 +444,7 @@
     @NonNull
     public String getString(@StringRes int id, Object... formatArgs) throws NotFoundException {
         final String raw = getString(id);
-        return String.format(mConfiguration.getLocales().getPrimary(), raw, formatArgs);
+        return String.format(mConfiguration.getLocales().get(0), raw, formatArgs);
     }
 
     /**
@@ -475,7 +475,7 @@
     public String getQuantityString(@PluralsRes int id, int quantity, Object... formatArgs)
             throws NotFoundException {
         String raw = getQuantityText(id, quantity).toString();
-        return String.format(mConfiguration.getLocales().getPrimary(), raw, formatArgs);
+        return String.format(mConfiguration.getLocales().get(0), raw, formatArgs);
     }
 
     /**
@@ -1971,7 +1971,7 @@
             }
 
             mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
-                    adjustLanguageTag(locales.getPrimary().toLanguageTag()),
+                    adjustLanguageTag(locales.get(0).toLanguageTag()),
                     mConfiguration.orientation,
                     mConfiguration.touchscreen,
                     mConfiguration.densityDpi, mConfiguration.keyboard,
@@ -1996,7 +1996,7 @@
         }
         synchronized (sSync) {
             if (mPluralRule != null) {
-                mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().getPrimary());
+                mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().get(0));
             }
         }
     }
diff --git a/core/java/android/nfc/tech/NfcF.java b/core/java/android/nfc/tech/NfcF.java
index b3e3ab6..4487121 100644
--- a/core/java/android/nfc/tech/NfcF.java
+++ b/core/java/android/nfc/tech/NfcF.java
@@ -98,8 +98,13 @@
     /**
      * Send raw NFC-F commands to the tag and receive the response.
      *
-     * <p>Applications must not append the SoD (length) or EoD (CRC) to the payload,
-     * it will be automatically calculated.
+     * <p>Applications must not prefix the SoD (preamble and sync code)
+     * and/or append the EoD (CRC) to the payload, it will be automatically calculated.
+     *
+     * <p>A typical NFC-F frame for this method looks like:
+     * <pre>
+     * LENGTH (1 byte) --- CMD (1 byte) -- IDm (8 bytes) -- PARAMS (LENGTH - 10 bytes)
+     * </pre>
      *
      * <p>Use {@link #getMaxTransceiveLength} to retrieve the maximum amount of bytes
      * that can be sent with {@link #transceive}.
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 52fa2ed..b33e807 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -147,6 +147,11 @@
     public static final int WAKE_TYPE_DRAW = 18;
 
     /**
+     * A constant indicating a bluetooth scan timer.
+     */
+    public static final int BLUETOOTH_SCAN_ON = 19;
+
+    /**
      * Include all of the data in the stats, including previously saved data.
      */
     public static final int STATS_SINCE_CHARGED = 0;
@@ -438,6 +443,7 @@
         public abstract Timer getFlashlightTurnedOnTimer();
         public abstract Timer getCameraTurnedOnTimer();
         public abstract Timer getForegroundActivityTimer();
+        public abstract Timer getBluetoothScanTimer();
 
         // Time this uid has any processes in the top state.
         public static final int PROCESS_STATE_TOP = 0;
@@ -1179,6 +1185,7 @@
         public static final int STATE2_PHONE_IN_CALL_FLAG = 1<<23;
         public static final int STATE2_BLUETOOTH_ON_FLAG = 1<<22;
         public static final int STATE2_CAMERA_FLAG = 1<<21;
+        public static final int STATE2_BLUETOOTH_SCAN_FLAG = 1 << 20;
 
         public static final int MOST_INTERESTING_STATES2 =
             STATE2_POWER_SAVE_FLAG | STATE2_WIFI_ON_FLAG | STATE2_DEVICE_IDLE_MASK
@@ -1922,6 +1929,7 @@
                 HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT, "wifi_suppl", "Wsp",
                 WIFI_SUPPL_STATE_NAMES, WIFI_SUPPL_STATE_SHORT_NAMES),
         new BitDescription(HistoryItem.STATE2_CAMERA_FLAG, "camera", "ca"),
+        new BitDescription(HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG, "ble_scan", "bles"),
     };
 
     public static final String[] HISTORY_EVENT_NAMES = new String[] {
@@ -2041,6 +2049,13 @@
      */
     public abstract long getCameraOnTime(long elapsedRealtimeUs, int which);
 
+    /**
+     * Returns the time in microseconds that bluetooth scans were running while the device was
+     * on battery.
+     *
+     * {@hide}
+     */
+    public abstract long getBluetoothScanTime(long elapsedRealtimeUs, int which);
 
     public static final int NETWORK_MOBILE_RX_DATA = 0;
     public static final int NETWORK_MOBILE_TX_DATA = 1;
@@ -2797,9 +2812,12 @@
         final long mobileTxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which);
         final long wifiRxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which);
         final long wifiTxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which);
+        final long btRxTotalBytes = getNetworkActivityBytes(NETWORK_BT_RX_DATA, which);
+        final long btTxTotalBytes = getNetworkActivityBytes(NETWORK_BT_TX_DATA, which);
         dumpLine(pw, 0 /* uid */, category, GLOBAL_NETWORK_DATA,
                 mobileRxTotalBytes, mobileTxTotalBytes, wifiRxTotalBytes, wifiTxTotalBytes,
-                mobileRxTotalPackets, mobileTxTotalPackets, wifiRxTotalPackets, wifiTxTotalPackets);
+                mobileRxTotalPackets, mobileTxTotalPackets, wifiRxTotalPackets, wifiTxTotalPackets,
+                btRxTotalBytes, btTxTotalBytes);
 
         // Dump Modem controller stats
         dumpControllerActivityLine(pw, 0 /* uid */, category, GLOBAL_MODEM_CONTROLLER_DATA,
@@ -3017,14 +3035,18 @@
             final int mobileActiveCount = u.getMobileRadioActiveCount(which);
             final long wifiPacketsRx = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which);
             final long wifiPacketsTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which);
+            final long btBytesRx = u.getNetworkActivityBytes(NETWORK_BT_RX_DATA, which);
+            final long btBytesTx = u.getNetworkActivityBytes(NETWORK_BT_TX_DATA, which);
             if (mobileBytesRx > 0 || mobileBytesTx > 0 || wifiBytesRx > 0 || wifiBytesTx > 0
                     || mobilePacketsRx > 0 || mobilePacketsTx > 0 || wifiPacketsRx > 0
-                    || wifiPacketsTx > 0 || mobileActiveTime > 0 || mobileActiveCount > 0) {
+                    || wifiPacketsTx > 0 || mobileActiveTime > 0 || mobileActiveCount > 0
+                    || btBytesRx > 0 || btBytesTx > 0) {
                 dumpLine(pw, uid, category, NETWORK_DATA, mobileBytesRx, mobileBytesTx,
                         wifiBytesRx, wifiBytesTx,
                         mobilePacketsRx, mobilePacketsTx,
                         wifiPacketsRx, wifiPacketsTx,
-                        mobileActiveTime, mobileActiveCount);
+                        mobileActiveTime, mobileActiveCount,
+                        btBytesRx, btBytesTx);
             }
 
             // Dump modem controller data, per UID.
@@ -3046,6 +3068,9 @@
             dumpControllerActivityLine(pw, uid, category, WIFI_CONTROLLER_DATA,
                     u.getWifiControllerActivity(), which);
 
+            dumpControllerActivityLine(pw, uid, category, BLUETOOTH_CONTROLLER_DATA,
+                    u.getBluetoothControllerActivity(), which);
+
             if (u.hasUserActivity()) {
                 args = new Object[Uid.NUM_USER_ACTIVITY_TYPES];
                 boolean hasData = false;
@@ -3668,6 +3693,12 @@
         pw.print("  Bluetooth total received: "); pw.print(formatBytesLocked(btRxTotalBytes));
         pw.print(", sent: "); pw.println(formatBytesLocked(btTxTotalBytes));
 
+        final long bluetoothScanTimeMs = getBluetoothScanTime(rawRealtime, which) / 1000;
+        sb.setLength(0);
+        sb.append(prefix);
+        sb.append("  Bluetooth scan time: "); formatTimeMs(sb, bluetoothScanTimeMs);
+        pw.println(sb.toString());
+
         printControllerActivity(pw, sb, prefix, "Bluetooth", getBluetoothControllerActivity(),
                 which);
 
@@ -3793,6 +3824,10 @@
                         pw.print(" wifi=");
                         printmAh(pw, bs.wifiPowerMah);
                     }
+                    if (bs.bluetoothPowerMah != 0) {
+                        pw.print(" bt=");
+                        printmAh(pw, bs.bluetoothPowerMah);
+                    }
                     if (bs.gpsPowerMah != 0) {
                         pw.print(" gps=");
                         printmAh(pw, bs.gpsPowerMah);
@@ -4035,6 +4070,9 @@
                 pw.println(" sent");
             }
 
+            uidActivity |= printTimer(pw, sb, u.getBluetoothScanTimer(), rawRealtime, which, prefix,
+                    "Bluetooth Scan");
+
             if (u.hasUserActivity()) {
                 boolean hasData = false;
                 for (int i=0; i<Uid.NUM_USER_ACTIVITY_TYPES; i++) {
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index b2cabd8..b51d2dfb 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -653,6 +653,12 @@
             if ((debugFlags & Zygote.DEBUG_GENERATE_DEBUG_INFO) != 0) {
                 argsForZygote.add("--generate-debug-info");
             }
+            if ((debugFlags & Zygote.DEBUG_ALWAYS_JIT) != 0) {
+                argsForZygote.add("--always-jit");
+            }
+            if ((debugFlags & Zygote.DEBUG_NATIVE_DEBUGGABLE) != 0) {
+                argsForZygote.add("--native-debuggable");
+            }
             if ((debugFlags & Zygote.DEBUG_ENABLE_ASSERT) != 0) {
                 argsForZygote.add("--enable-assert");
             }
diff --git a/core/java/android/os/ShellCommand.java b/core/java/android/os/ShellCommand.java
index 54d1090..fc804e5 100644
--- a/core/java/android/os/ShellCommand.java
+++ b/core/java/android/os/ShellCommand.java
@@ -234,6 +234,16 @@
         }
     }
 
+    public String peekNextArg() {
+        if (mCurArgData != null) {
+            return mCurArgData;
+        } else if (mArgPos < mArgs.length) {
+            return mArgs[mArgPos];
+        } else {
+            return null;
+        }
+    }
+
     /**
      * Return the next argument on the command line, whatever it is; if there are
      * no arguments left, throws an IllegalArgumentException to report this to the user.
diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java
index 344d06e..24666fe 100644
--- a/core/java/android/os/UserHandle.java
+++ b/core/java/android/os/UserHandle.java
@@ -74,6 +74,9 @@
     /** @hide A user id constant to indicate the "system" user of the device */
     public static final @UserIdInt int USER_SYSTEM = 0;
 
+    /** @hide A user serial constant to indicate the "system" user of the device */
+    public static final int USER_SERIAL_SYSTEM = 0;
+
     /** @hide A user handle to indicate the "system" user of the device */
     public static final UserHandle SYSTEM = new UserHandle(USER_SYSTEM);
 
diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java
index 9b3f02d..dd8eb5f 100644
--- a/core/java/android/os/storage/IMountService.java
+++ b/core/java/android/os/storage/IMountService.java
@@ -1284,7 +1284,7 @@
 
             @Override
             public void prepareUserStorage(
-                    String volumeUuid, int userId, int serialNumber, boolean ephemeral)
+                    String volumeUuid, int userId, int serialNumber, int flags)
                     throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
@@ -1293,7 +1293,7 @@
                     _data.writeString(volumeUuid);
                     _data.writeInt(userId);
                     _data.writeInt(serialNumber);
-                    _data.writeInt(ephemeral ? 1 : 0);
+                    _data.writeInt(flags);
                     mRemote.transact(Stub.TRANSACTION_prepareUserStorage, _data, _reply, 0);
                     _reply.readException();
                 } finally {
@@ -2055,8 +2055,8 @@
                     String volumeUuid = data.readString();
                     int userId = data.readInt();
                     int serialNumber = data.readInt();
-                    boolean ephemeral = data.readInt() != 0;
-                    prepareUserStorage(volumeUuid, userId, serialNumber, ephemeral);
+                    int _flags = data.readInt();
+                    prepareUserStorage(volumeUuid, userId, serialNumber, _flags);
                     reply.writeNoException();
                     return true;
                 }
@@ -2389,7 +2389,7 @@
     public boolean isUserKeyUnlocked(int userId) throws RemoteException;
 
     public void prepareUserStorage(String volumeUuid, int userId, int serialNumber,
-            boolean ephemeral) throws RemoteException;
+            int flags) throws RemoteException;
 
     public ParcelFileDescriptor mountAppFuse(String name) throws RemoteException;
 }
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index c8b942b..b82638a 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -92,8 +92,14 @@
     /** {@hide} */
     public static final int DEBUG_EMULATE_FBE = 1 << 1;
 
+    // NOTE: keep in sync with installd
     /** {@hide} */
-    public static final int FLAG_FOR_WRITE = 1 << 0;
+    public static final int FLAG_STORAGE_DE = 1 << 0;
+    /** {@hide} */
+    public static final int FLAG_STORAGE_CE = 1 << 1;
+
+    /** {@hide} */
+    public static final int FLAG_FOR_WRITE = 1 << 8;
 
     private final Context mContext;
     private final ContentResolver mResolver;
@@ -1003,10 +1009,9 @@
     }
 
     /** {@hide} */
-    public void prepareUserStorage(
-            String volumeUuid, int userId, int serialNumber, boolean ephemeral) {
+    public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) {
         try {
-            mMountService.prepareUserStorage(volumeUuid, userId, serialNumber, ephemeral);
+            mMountService.prepareUserStorage(volumeUuid, userId, serialNumber, flags);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
diff --git a/core/java/android/provider/BlockedNumberContract.java b/core/java/android/provider/BlockedNumberContract.java
index 7a9d062..ea54f92 100644
--- a/core/java/android/provider/BlockedNumberContract.java
+++ b/core/java/android/provider/BlockedNumberContract.java
@@ -20,35 +20,126 @@
 import android.os.Bundle;
 
 /**
- * Constants and methods to access blocked phone numbers for incoming calls and texts.
+ * <p>
+ * The contract between the blockednumber provider and applications. Contains definitions for
+ * the supported URIs and columns.
+ * </p>
  *
- * TODO javadoc
- * - Proper javadoc tagging.
- * - Code sample?
- * - Describe who can access it.
+ * <h3> Overview </h3>
+ * <p>
+ * The content provider exposes a table containing blocked numbers. The columns and URIs for
+ * accessing this table are defined by the {@link BlockedNumbers} class. Messages, and calls from
+ * blocked numbers are discarded by the platform. Notifications upon provider changes can be
+ * received using a {@link android.database.ContentObserver}.
+ * </p>
+ *
+ * <h3> Permissions </h3>
+ * <p>
+ * Only the system, the default SMS application, and the default phone app
+ * (See {@link android.telecom.TelecomManager#getDefaultDialerPackage()}), and carrier apps
+ * (See {@link android.service.carrier.CarrierService}) can read, and write to the blockednumber
+ * provider.
+ * </p>
+ *
+ * <h3> Data </h3>
+ * <p>
+ * Other than regular phone numbers, the blocked number provider can also store addresses (such
+ * as email) from which a user can receive messages, and calls. The blocked numbers are stored
+ * in the {@link BlockedNumbers#COLUMN_ORIGINAL_NUMBER} column. A normalized version of phone
+ * numbers (if normalization is possible) is stored in {@link BlockedNumbers#COLUMN_E164_NUMBER}
+ * column. The platform blocks calls, and messages from an address if it is present in in the
+ * {@link BlockedNumbers#COLUMN_ORIGINAL_NUMBER} column or if the E164 version of the address
+ * matches the {@link BlockedNumbers#COLUMN_E164_NUMBER} column.
+ * </p>
+ *
+ * <h3> Operations </h3>
+ * <dl>
+ * <dt><b>Insert</b></dt>
+ * <dd>
+ * <p>
+ * {@link BlockedNumbers#COLUMN_ORIGINAL_NUMBER} is a required column that needs to be populated.
+ * Apps can optionally provide the {@link BlockedNumbers#COLUMN_E164_NUMBER} which is the phone
+ * number's E164 representation. The provider automatically populates this column if the app does
+ * not provide it. Note that this column is not populated if normalization fails or if the address
+ * is not a phone number (eg: email). The provider enforces uniqueness constraint on this column.
+ * Examples:
+ * <pre>
+ * ContentValues values = new ContentValues();
+ * values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1234567890");
+ * Uri uri = getContentResolver().insert(BlockedNumbers.CONTENT_URI, values);
+ * </pre>
+ * <pre>
+ * ContentValues values = new ContentValues();
+ * values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1234567890");
+ * values.put(BlockedNumbers.COLUMN_E164_NUMBER, "+11234567890");
+ * Uri uri = getContentResolver().insert(BlockedNumbers.CONTENT_URI, values);
+ * </pre>
+ * <pre>
+ * ContentValues values = new ContentValues();
+ * values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "12345@abdcde.com");
+ * Uri uri = getContentResolver().insert(BlockedNumbers.CONTENT_URI, values);
+ * </pre>
+ * </p>
+ * </dd>
+ * <dt><b>Update</b></dt>
+ * <dd>
+ * <p>
+ * Updates are not supported. Use Delete, and Insert instead.
+ * </p>
+ * </dd>
+ * <dt><b>Delete</b></dt>
+ * <dd>
+ * <p>
+ * Deletions can be performed as follows:
+ * <pre>
+ * ContentValues values = new ContentValues();
+ * values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1234567890");
+ * Uri uri = getContentResolver().insert(BlockedNumbers.CONTENT_URI, values);
+ * getContentResolver().delete(uri, null, null);
+ * </pre>
+ * </p>
+ * </dd>
+ * <dt><b>Query</b></dt>
+ * <dd>
+ * <p>
+ * All blocked numbers can be enumerated as follows:
+ * <pre>
+ * Cursor c = getContentResolver().query(BlockedNumbers.CONTENT_URI,
+ *          new String[]{BlockedNumbers.COLUMN_ID, BlockedNumbers.COLUMN_ORIGINAL_NUMBER,
+ *          BlockedNumbers.COLUMN_E164_NUMBER}, null, null, null);
+ * </pre>
+ * To check if a particular number is blocked, use the method
+ * {@link #isBlocked(Context, String)}.
+ * </p>
+ * </dd>
+ *
+ * <h3> Multi-user </h3>
+ * <p>
+ * Apps must use the method {@link #canCurrentUserBlockNumbers(Context)} before performing any
+ * operation on the blocked number provider. If {@link #canCurrentUserBlockNumbers(Context)} returns
+ * {@code false}, all operations on the provider will fail with an
+ * {@link UnsupportedOperationException}. The platform will block calls, and messages from numbers
+ * in the provider independent of the current user.
+ * </p>
  */
 public class BlockedNumberContract {
     private BlockedNumberContract() {
     }
 
-    /** The authority for the contacts provider */
+    /** The authority for the blocked number provider */
     public static final String AUTHORITY = "com.android.blockednumber";
 
-    /** A content:// style uri to the authority for the contacts provider */
+    /** A content:// style uri to the authority for the blocked number provider */
     public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
 
     /**
-     * TODO javadoc
-     *
-     * Constants to interact with the blocked phone number list.
+     * Constants to interact with the blocked numbers list.
      */
     public static class BlockedNumbers {
         private BlockedNumbers() {
         }
 
         /**
-         * TODO javadoc
-         *
          * Content URI for the blocked numbers.
          *
          * Supported operations
@@ -117,8 +208,8 @@
      * context {@code context}, this method will throw an {@link UnsupportedOperationException}.
      */
     public static boolean isBlocked(Context context, String phoneNumber) {
-        final Bundle res = context.getContentResolver().call(AUTHORITY_URI,
-                METHOD_IS_BLOCKED, phoneNumber, null);
+        final Bundle res = context.getContentResolver().call(
+                AUTHORITY_URI, METHOD_IS_BLOCKED, phoneNumber, null);
         return res != null && res.getBoolean(RES_NUMBER_IS_BLOCKED, false);
     }
 
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 330fcf6..dfdd36d 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -2079,10 +2079,11 @@
             if (preferHighres) {
                 final Uri displayPhotoUri = Uri.withAppendedPath(contactUri,
                         Contacts.Photo.DISPLAY_PHOTO);
-                InputStream inputStream;
                 try {
                     AssetFileDescriptor fd = cr.openAssetFileDescriptor(displayPhotoUri, "r");
-                    return fd.createInputStream();
+                    if (fd != null) {
+                        return fd.createInputStream();
+                    }
                 } catch (IOException e) {
                     // fallback to the thumbnail code
                 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 5535eaa..7830142 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -670,14 +670,14 @@
             "android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS";
 
     /**
-     * Activity Action: Ask the user to allow an to ignore battery optimizations (that is,
+     * Activity Action: Ask the user to allow an app to ignore battery optimizations (that is,
      * put them on the whitelist of apps shown by
      * {@link #ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS}).  For an app to use this, it also
      * must hold the {@link android.Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS}
      * permission.
      * <p><b>Note:</b> most applications should <em>not</em> use this; there are many facilities
      * provided by the platform for applications to operate correctly in the various power
-     * saving mode.  This is only for unusual applications that need to deeply control their own
+     * saving modes.  This is only for unusual applications that need to deeply control their own
      * execution, at the potential expense of the user's battery life.  Note that these applications
      * greatly run the risk of showing to the user as high power consumers on their device.</p>
      * <p>
@@ -695,6 +695,24 @@
             "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
 
     /**
+     * Activity Action: Show screen for controlling which apps can ignore background data
+     * restrictions.
+     * <p>
+     * Input: if the Intent's data URI is set with an application name (using the "package" schema,
+     * like "package:com.my.app"), then when the screen is displayed it will focus on such app. If
+     * the data is not set, it will just open the screen.
+     * <p>
+     * Output: Nothing.
+     * <p>
+     * Applications can also use {@link android.net.ConnectivityManager#getRestrictBackgroundStatus
+     * ConnectivityManager#getRestrictBackgroundStatus()} to determine the status of the background
+     * data restrictions for them.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS =
+            "android.settings.IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS";
+
+    /**
      * @hide
      * Activity Action: Show the "app ops" settings screen.
      * <p>
@@ -3851,7 +3869,6 @@
             MOVED_TO_GLOBAL.add(Settings.Global.DATA_ROAMING);
             MOVED_TO_GLOBAL.add(Settings.Global.DEVELOPMENT_SETTINGS_ENABLED);
             MOVED_TO_GLOBAL.add(Settings.Global.DEVICE_PROVISIONED);
-            MOVED_TO_GLOBAL.add(Settings.Global.DISPLAY_DENSITY_FORCED);
             MOVED_TO_GLOBAL.add(Settings.Global.DISPLAY_SIZE_FORCED);
             MOVED_TO_GLOBAL.add(Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE);
             MOVED_TO_GLOBAL.add(Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE);
@@ -5086,6 +5103,15 @@
             "disabled_print_services";
 
         /**
+         * The saved value for WindowManagerService.setForcedDisplayDensity()
+         * formatted as a single integer representing DPI. If unset, then use
+         * the real display density.
+         *
+         * @hide
+         */
+        public static final String DISPLAY_DENSITY_FORCED = "display_density_forced";
+
+        /**
          * Setting to always use the default text-to-speech settings regardless
          * of the application settings.
          * 1 = override application settings,
@@ -6517,13 +6543,6 @@
        public static final String DEVICE_PROVISIONED = "device_provisioned";
 
        /**
-        * The saved value for WindowManagerService.setForcedDisplayDensity().
-        * One integer in dpi.  If unset, then use the real display density.
-        * @hide
-        */
-       public static final String DISPLAY_DENSITY_FORCED = "display_density_forced";
-
-       /**
         * The saved value for WindowManagerService.setForcedDisplaySize().
         * Two integers separated by a comma.  If unset, then use the real display size.
         * @hide
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index a56e030..fb58f4e 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -45,7 +45,9 @@
  *         &lt;action android:name="android.service.notification.NotificationAssistantService" />
  *     &lt;/intent-filter>
  * &lt;/service></pre>
+ * @hide
  */
+@SystemApi
 public abstract class NotificationAssistantService extends NotificationListenerService {
     private static final String TAG = "NotificationAssistant";
 
@@ -210,29 +212,6 @@
         }
     }
 
-    /**
-     * Add an annotation to a an existing notification. The delete intent will
-     * be fired when the host notification is deleted, or when this annotation
-     * is removed or replaced.
-     *
-     * @param key the key of the notification to be annotated
-     * @param annotation the new annotation object
-     */
-    public final void setAnnotation(String key, Notification annotation)
-    {
-        // TODO: pack up the annotation and send it to the NotificationManager.
-    }
-
-    /**
-     * Remove the annotation from a notification.
-     *
-     * @param key the key of the notification to be cleansed of annotatons
-     */
-    public final void clearAnnotation(String key)
-    {
-        // TODO: ask the NotificationManager to clear the annotation.
-    }
-
     private class NotificationAssistantWrapper extends NotificationListenerWrapper {
         @Override
         public void onNotificationEnqueued(IStatusBarNotificationHolder sbnHolder,
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index ed90e79..7ff883e 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -695,7 +695,7 @@
     /**
      * Request that the listener be rebound, after a previous call to (@link requestUnbind).
      *
-     * <P>This method will fail for assistants that have
+     * <P>This method will fail for listeners that have
      * not been granted the permission by the user.
      *
      * <P>The service should wait for the {@link #onListenerConnected()} event
@@ -1022,8 +1022,7 @@
         }
 
         /**
-         * If the importance has been overriden by user preference, or by a
-         * {@link NotificationAssistantService}, then this will be non-null,
+         * If the importance has been overriden by user preference, then this will be non-null,
          * and should be displayed to the user.
          *
          * @return the explanation for the importance, or null if it is the natural importance
diff --git a/core/java/android/text/BidiFormatter.java b/core/java/android/text/BidiFormatter.java
index a535480..675803c 100644
--- a/core/java/android/text/BidiFormatter.java
+++ b/core/java/android/text/BidiFormatter.java
@@ -293,7 +293,7 @@
      * directionality is determined by scanning the end of the string, the overall directionality is
      * given explicitly by a heuristic to estimate the {@code str}'s directionality.
      *
-     * @param str String after which the mark may need to appear.
+     * @param str CharSequence after which the mark may need to appear.
      * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s
      *                  directionality.
      * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context;
@@ -301,7 +301,7 @@
      *
      * @hide
      */
-    public String markAfter(String str, TextDirectionHeuristic heuristic) {
+    public String markAfter(CharSequence str, TextDirectionHeuristic heuristic) {
         final boolean isRtl = heuristic.isRtl(str, 0, str.length());
         // getExitDir() is called only if needed (short-circuit).
         if (!mIsRtlContext && (isRtl || getExitDir(str) == DIR_RTL)) {
@@ -322,7 +322,7 @@
      * entry directionality is determined by scanning the beginning of the string, the overall
      * directionality is given explicitly by a heuristic to estimate the {@code str}'s directionality.
      *
-     * @param str String before which the mark may need to appear.
+     * @param str CharSequence before which the mark may need to appear.
      * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s
      *                  directionality.
      * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context;
@@ -330,7 +330,7 @@
      *
      * @hide
      */
-    public String markBefore(String str, TextDirectionHeuristic heuristic) {
+    public String markBefore(CharSequence str, TextDirectionHeuristic heuristic) {
         final boolean isRtl = heuristic.isRtl(str, 0, str.length());
         // getEntryDir() is called only if needed (short-circuit).
         if (!mIsRtlContext && (isRtl || getEntryDir(str) == DIR_RTL)) {
@@ -350,6 +350,13 @@
      *          false.
      */
     public boolean isRtl(String str) {
+        return isRtl((CharSequence) str);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isRtl(CharSequence str) {
         return mDefaultTextDirectionHeuristic.isRtl(str, 0, str.length());
     }
 
@@ -384,9 +391,16 @@
      *     {@code null}.
      */
     public String unicodeWrap(String str, TextDirectionHeuristic heuristic, boolean isolate) {
+        return unicodeWrap((CharSequence) str, heuristic, isolate).toString();
+    }
+
+    /**
+     * @hide
+     */
+    public CharSequence unicodeWrap(CharSequence str, TextDirectionHeuristic heuristic, boolean isolate) {
         if (str == null) return null;
         final boolean isRtl = heuristic.isRtl(str, 0, str.length());
-        StringBuilder result = new StringBuilder();
+        SpannableStringBuilder result = new SpannableStringBuilder();
         if (getStereoReset() && isolate) {
             result.append(markBefore(str,
                     isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR));
@@ -402,7 +416,7 @@
             result.append(markAfter(str,
                     isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR));
         }
-        return result.toString();
+        return result;
     }
 
     /**
@@ -419,6 +433,14 @@
     }
 
     /**
+     * @hide
+     */
+    public CharSequence unicodeWrap(CharSequence str, TextDirectionHeuristic heuristic) {
+        return unicodeWrap(str, heuristic, true /* isolate */);
+    }
+
+
+    /**
      * Operates like {@link #unicodeWrap(String, TextDirectionHeuristic, boolean)}, but uses the
      * formatter's default direction estimation algorithm.
      *
@@ -432,6 +454,13 @@
     }
 
     /**
+     * @hide
+     */
+    public CharSequence unicodeWrap(CharSequence str, boolean isolate) {
+        return unicodeWrap(str, mDefaultTextDirectionHeuristic, isolate);
+    }
+
+    /**
      * Operates like {@link #unicodeWrap(String, TextDirectionHeuristic, boolean)}, but uses the
      * formatter's default direction estimation algorithm and assumes {@code isolate} is true.
      *
@@ -442,6 +471,13 @@
         return unicodeWrap(str, mDefaultTextDirectionHeuristic, true /* isolate */);
     }
 
+    /**
+     * @hide
+     */
+    public CharSequence unicodeWrap(CharSequence str) {
+        return unicodeWrap(str, mDefaultTextDirectionHeuristic, true /* isolate */);
+    }
+
     private static BidiFormatter getDefaultInstanceFromContext(boolean isRtlContext) {
         return isRtlContext ? DEFAULT_RTL_INSTANCE : DEFAULT_LTR_INSTANCE;
     }
@@ -477,7 +513,7 @@
      *
      * @param str the string to check.
      */
-    private static int getExitDir(String str) {
+    private static int getExitDir(CharSequence str) {
         return new DirectionalityEstimator(str, false /* isHtml */).getExitDir();
     }
 
@@ -494,7 +530,7 @@
      *
      * @param str the string to check.
      */
-    private static int getEntryDir(String str) {
+    private static int getEntryDir(CharSequence str) {
         return new DirectionalityEstimator(str, false /* isHtml */).getEntryDir();
     }
 
@@ -532,7 +568,7 @@
         /**
          * The text to be scanned.
          */
-        private final String text;
+        private final CharSequence text;
 
         /**
          * Whether the text to be scanned is to be treated as HTML, i.e. skipping over tags and
@@ -565,7 +601,7 @@
          * @param isHtml Whether the text to be scanned is to be treated as HTML, i.e. skipping over
          *     tags and entities.
          */
-        DirectionalityEstimator(String text, boolean isHtml) {
+        DirectionalityEstimator(CharSequence text, boolean isHtml) {
             this.text = text;
             this.isHtml = isHtml;
             length = text.length();
@@ -896,4 +932,4 @@
             return Character.DIRECTIONALITY_OTHER_NEUTRALS;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/android/text/style/LocaleSpan.java b/core/java/android/text/style/LocaleSpan.java
index 117de774..4f687c8 100644
--- a/core/java/android/text/style/LocaleSpan.java
+++ b/core/java/android/text/style/LocaleSpan.java
@@ -97,12 +97,12 @@
      * @return The {@link Locale} for this span.  If multiple locales are associated with this
      * span, only the first locale is returned.  {@code null} if no {@link Locale} is specified.
      *
-     * @see LocaleList#getPrimary()
+     * @see LocaleList#get()
      * @see #getLocales()
      */
     @Nullable
     public Locale getLocale() {
-        return mLocales.getPrimary();
+        return mLocales.get(0);
     }
 
     /**
diff --git a/core/java/android/util/LocaleList.java b/core/java/android/util/LocaleList.java
index 90a20bc..fc39004 100644
--- a/core/java/android/util/LocaleList.java
+++ b/core/java/android/util/LocaleList.java
@@ -47,12 +47,7 @@
     private static final LocaleList sEmptyLocaleList = new LocaleList();
 
     public Locale get(int location) {
-        return location < mList.length ? mList[location] : null;
-    }
-
-    @Nullable
-    public Locale getPrimary() {
-        return mList.length == 0 ? null : get(0);
+        return (0 <= location && location < mList.length) ? mList[location] : null;
     }
 
     public boolean isEmpty() {
@@ -464,7 +459,7 @@
                 // someone has called Locale.setDefault() since we last set or adjusted the default
                 // locale list. So let's recalculate the locale list.
                 if (sDefaultLocaleList != null
-                        && defaultLocale.equals(sDefaultLocaleList.getPrimary())) {
+                        && defaultLocale.equals(sDefaultLocaleList.get(0))) {
                     // The default Locale has changed, but it happens to be the first locale in the
                     // default locale list, so we don't need to construct a new locale list.
                     return sDefaultLocaleList;
diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java
index 501c6e9..b9a7421 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -21,7 +21,6 @@
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.RemoteViews;
 import android.widget.TextView;
 
@@ -35,7 +34,7 @@
 @RemoteViews.RemoteView
 public class NotificationHeaderView extends ViewGroup {
     public static final int NO_COLOR = -1;
-    private final int mHeaderMinWidth;
+    private final int mChildMinWidth;
     private final int mExpandTopPadding;
     private final int mContentEndMargin;
     private View mAppName;
@@ -46,6 +45,7 @@
     private View mIcon;
     private TextView mChildCount;
     private View mProfileBadge;
+    private View mInfo;
     private int mIconColor;
     private int mOriginalNotificationColor;
     private boolean mGroupHeader;
@@ -66,7 +66,7 @@
 
     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mHeaderMinWidth = getResources().getDimensionPixelSize(
+        mChildMinWidth = getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.notification_header_shrink_min_width);
         mContentEndMargin = getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.notification_content_margin_end);
@@ -82,6 +82,7 @@
         mIcon = findViewById(com.android.internal.R.id.icon);
         mChildCount = (TextView) findViewById(com.android.internal.R.id.number_of_children);
         mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
+        mInfo = findViewById(com.android.internal.R.id.header_content_info);
     }
 
     @Override
@@ -109,14 +110,23 @@
         }
         if (totalWidth > givenWidth) {
             int overFlow = totalWidth - givenWidth;
-            // We are overflowing, lets shrink
+            // We are overflowing, lets shrink the info first
+            final int infoWidth = mInfo.getMeasuredWidth();
+            if (mInfo.getVisibility() != GONE && infoWidth > mChildMinWidth) {
+                int newSize = infoWidth - Math.min(infoWidth - mChildMinWidth, overFlow);
+                int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
+                mInfo.measure(childWidthSpec, wrapContentHeightSpec);
+                overFlow -= infoWidth - newSize;
+            }
+            // still overflowing, lets shrink the app name now
             final int appWidth = mAppName.getMeasuredWidth();
-            if (mAppName.getVisibility() != GONE && appWidth > mHeaderMinWidth) {
-                int newSize = appWidth - Math.min(appWidth - mHeaderMinWidth, overFlow);
+            if (overFlow > 0 && mAppName.getVisibility() != GONE && appWidth > mChildMinWidth) {
+                int newSize = appWidth - Math.min(appWidth - mChildMinWidth, overFlow);
                 int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
                 mAppName.measure(childWidthSpec, wrapContentHeightSpec);
                 overFlow -= appWidth - newSize;
             }
+            // still overflowing, finaly we shrink the subtext
             if (overFlow > 0 && mSubTextView.getVisibility() != GONE) {
                 // we're still too big
                 final int subTextWidth = mSubTextView.getMeasuredWidth();
@@ -124,12 +134,8 @@
                 int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
                 mSubTextView.measure(childWidthSpec, wrapContentHeightSpec);
             }
-            totalWidth = givenWidth;
         }
-        if (mProfileBadge.getVisibility() != View.GONE) {
-            totalWidth = givenWidth;
-        }
-        setMeasuredDimension(totalWidth, givenHeight);
+        setMeasuredDimension(givenWidth, givenHeight);
     }
 
     @Override
@@ -275,18 +281,16 @@
             mTouchRects.clear();
             addRectAroundViewView(mIcon);
             addRectAroundViewView(mExpandButton);
-            addInBetweenRect();
+            addWidthRect();
             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
         }
 
-        private void addInBetweenRect() {
-            final Rect r = new Rect();
+        private void addWidthRect() {
+            Rect r = new Rect();
             r.top = 0;
             r.bottom = (int) (32 * getResources().getDisplayMetrics().density);
-            Rect leftRect = mTouchRects.get(0);
-            r.left = leftRect.right;
-            Rect rightRect = mTouchRects.get(1);
-            r.right = rightRect.left;
+            r.left = 0;
+            r.right = getWidth();
             mTouchRects.add(r);
         }
 
diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java
index 2aace0f..7017ff5 100644
--- a/core/java/android/view/RenderNode.java
+++ b/core/java/android/view/RenderNode.java
@@ -136,6 +136,9 @@
     private RenderNode(String name, View owningView) {
         mNativeRenderNode = nCreate(name);
         mOwningView = owningView;
+        if (mOwningView instanceof SurfaceView) {
+            nRequestPositionUpdates(mNativeRenderNode, (SurfaceView) mOwningView);
+        }
     }
 
     /**
@@ -863,6 +866,8 @@
     private static native void nOutput(long renderNode);
     private static native int nGetDebugSize(long renderNode);
 
+    private static native void nRequestPositionUpdates(long renderNode, SurfaceView callback);
+
     ///////////////////////////////////////////////////////////////////////////
     // Animations
     ///////////////////////////////////////////////////////////////////////////
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 5b48e28..a296051 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -135,7 +135,7 @@
         }
     };
 
-    final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener
+    private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener
             = new ViewTreeObserver.OnScrollChangedListener() {
                     @Override
                     public void onScrollChanged() {
@@ -143,6 +143,17 @@
                     }
             };
 
+    private final ViewTreeObserver.OnPreDrawListener mDrawListener =
+            new ViewTreeObserver.OnPreDrawListener() {
+                @Override
+                public boolean onPreDraw() {
+                    // reposition ourselves where the surface is
+                    mHaveFrame = getWidth() > 0 && getHeight() > 0;
+                    updateWindow(false, false);
+                    return true;
+                }
+            };
+
     boolean mRequestedVisible = false;
     boolean mWindowVisibility = false;
     boolean mViewVisibility = false;
@@ -168,17 +179,9 @@
     boolean mUpdateWindowNeeded;
     boolean mReportDrawNeeded;
     private Translator mTranslator;
+    private int mWindowInsetLeft;
+    private int mWindowInsetTop;
 
-    private final ViewTreeObserver.OnPreDrawListener mDrawListener =
-            new ViewTreeObserver.OnPreDrawListener() {
-                @Override
-                public boolean onPreDraw() {
-                    // reposition ourselves where the surface is
-                    mHaveFrame = getWidth() > 0 && getHeight() > 0;
-                    updateWindow(false, false);
-                    return true;
-                }
-            };
     private boolean mGlobalListenersAdded;
 
     public SurfaceView(Context context) {
@@ -443,17 +446,17 @@
         int myHeight = mRequestedHeight;
         if (myHeight <= 0) myHeight = getHeight();
 
-        getLocationInWindow(mLocation);
         final boolean creating = mWindow == null;
         final boolean formatChanged = mFormat != mRequestedFormat;
         final boolean sizeChanged = mWindowSpaceWidth != myWidth || mWindowSpaceHeight != myHeight;
         final boolean visibleChanged = mVisible != mRequestedVisible;
         final boolean layoutSizeChanged = getWidth() != mLayout.width
                 || getHeight() != mLayout.height;
-        final boolean positionChanged = mWindowSpaceLeft != mLocation[0] || mWindowSpaceTop != mLocation[1];
 
         if (force || creating || formatChanged || sizeChanged || visibleChanged
             || mUpdateWindowNeeded || mReportDrawNeeded || redrawNeeded) {
+            getLocationInWindow(mLocation);
+
             if (DEBUG) Log.i(TAG, "Changes: creating=" + creating
                     + " format=" + formatChanged + " size=" + sizeChanged
                     + " visible=" + visibleChanged
@@ -643,27 +646,69 @@
                 TAG, "Layout: x=" + mLayout.x + " y=" + mLayout.y +
                 " w=" + mLayout.width + " h=" + mLayout.height +
                 ", frame=" + mSurfaceFrame);
-        } else if (positionChanged || layoutSizeChanged) { // Only the position has changed
-            mWindowSpaceLeft = mLocation[0];
-            mWindowSpaceTop = mLocation[1];
-            // For our size changed check, we keep mLayout.width and mLayout.height
-            // in view local space.
-            mLocation[0] = mLayout.width = getWidth();
-            mLocation[1] = mLayout.height = getHeight();
+        } else if (!isHardwareAccelerated()) {
+            getLocationInWindow(mLocation);
+            final boolean positionChanged = mWindowSpaceLeft != mLocation[0]
+                    || mWindowSpaceTop != mLocation[1];
+            if (positionChanged || layoutSizeChanged) { // Only the position has changed
+                mWindowSpaceLeft = mLocation[0];
+                mWindowSpaceTop = mLocation[1];
+                // For our size changed check, we keep mLayout.width and mLayout.height
+                // in view local space.
+                mLocation[0] = mLayout.width = getWidth();
+                mLocation[1] = mLayout.height = getHeight();
 
-            transformFromViewToWindowSpace(mLocation);
+                transformFromViewToWindowSpace(mLocation);
 
-            try {
-                mSession.repositionChild(mWindow, mWindowSpaceLeft, mWindowSpaceTop,
-                        mLocation[0], mLocation[1],
-                        viewRoot != null ? viewRoot.getNextFrameNumber() : -1,
-                        mWinFrame);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Exception from relayout", ex);
+                try {
+                    Log.d(TAG, String.format("updateWindowPosition UI, " +
+                            "postion = [%d, %d, %d, %d]", mWindowSpaceLeft, mWindowSpaceTop,
+                            mLocation[0], mLocation[1]));
+                    mSession.repositionChild(mWindow, mWindowSpaceLeft, mWindowSpaceTop,
+                            mLocation[0], mLocation[1], -1, mWinFrame);
+                } catch (RemoteException ex) {
+                    Log.e(TAG, "Exception from relayout", ex);
+                }
             }
         }
     }
 
+    private Rect mRTLastReportedPosition = new Rect();
+
+    /**
+     * Called by native on RenderThread to update the window position
+     * @hide
+     */
+    public final void updateWindowPositionRT(long frameNumber,
+            int left, int top, int right, int bottom) {
+        IWindowSession session = mSession;
+        MyWindow window = mWindow;
+        if (session == null || window == null) {
+            // Guess we got detached, that sucks
+            return;
+        }
+        if (mRTLastReportedPosition.left == left
+                && mRTLastReportedPosition.top == top
+                && mRTLastReportedPosition.right == right
+                && mRTLastReportedPosition.bottom == bottom) {
+            return;
+        }
+        try {
+            if (DEBUG) {
+                Log.d(TAG, String.format("updateWindowPosition RT, frameNr = %d, " +
+                        "postion = [%d, %d, %d, %d]", frameNumber, left, top,
+                        right, bottom));
+            }
+            // Just using mRTLastReportedPosition as a dummy rect here
+            session.repositionChild(window, left, top, right, bottom, frameNumber,
+                    mRTLastReportedPosition);
+            // Now overwrite mRTLastReportedPosition with our values
+            mRTLastReportedPosition.set(left, top, right, bottom);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Exception from repositionChild", ex);
+        }
+    }
+
     private SurfaceHolder.Callback[] getSurfaceCallbacks() {
         SurfaceHolder.Callback callbacks[];
         synchronized (mCallbacks) {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 4a0a0b0..127157b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -4506,9 +4506,15 @@
                     }
                     break;
                 case R.styleable.View_pointerShape:
-                    final int pointerShape = a.getInt(attr, PointerIcon.STYLE_NOT_SPECIFIED);
-                    if (pointerShape != PointerIcon.STYLE_NOT_SPECIFIED) {
-                        setPointerIcon(PointerIcon.getSystemIcon(context, pointerShape));
+                    final int resourceId = a.getResourceId(attr, 0);
+                    if (resourceId != 0) {
+                        setPointerIcon(PointerIcon.loadCustomIcon(
+                                context.getResources(), resourceId));
+                    } else {
+                        final int pointerShape = a.getInt(attr, PointerIcon.STYLE_NOT_SPECIFIED);
+                        if (pointerShape != PointerIcon.STYLE_NOT_SPECIFIED) {
+                            setPointerIcon(PointerIcon.getSystemIcon(context, pointerShape));
+                        }
                     }
                     break;
             }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 98e3289..96853e0 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1811,7 +1811,9 @@
                 final boolean dragResizing = freeformResizing || dockedResizing;
                 if (mDragResizing != dragResizing) {
                     if (dragResizing) {
-                        startDragResizing(mPendingBackDropFrame);
+                        startDragResizing(mPendingBackDropFrame,
+                                mWinFrame.equals(mPendingBackDropFrame), mPendingVisibleInsets,
+                                mPendingStableInsets);
                         mResizeMode = freeformResizing
                                 ? RESIZE_MODE_FREEFORM
                                 : RESIZE_MODE_DOCKED_DIVIDER;
@@ -5845,9 +5847,11 @@
         // Tell all listeners that we are resizing the window so that the chrome can get
         // updated as fast as possible on a separate thread,
         if (mDragResizing) {
+            boolean fullscreen = frame.equals(backDropFrame);
             synchronized (mWindowCallbacks) {
                 for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
-                    mWindowCallbacks.get(i).onWindowSizeIsChanging(backDropFrame);
+                    mWindowCallbacks.get(i).onWindowSizeIsChanging(backDropFrame, fullscreen,
+                            visibleInsets, stableInsets);
                 }
             }
         }
@@ -6815,20 +6819,6 @@
         }
     }
 
-    long getNextFrameNumber() {
-        long frameNumber = -1;
-        if (mSurfaceHolder != null) {
-            mSurfaceHolder.mSurfaceLock.lock();
-        }
-        if (mSurface.isValid()) {
-            frameNumber =  mSurface.getNextFrameNumber();
-        }
-        if (mSurfaceHolder != null) {
-            mSurfaceHolder.mSurfaceLock.unlock();
-        }
-        return frameNumber;
-    }
-
     class TakenSurfaceHolder extends BaseSurfaceHolder {
         @Override
         public boolean onAllowLockCanvas() {
@@ -7060,12 +7050,14 @@
     /**
      * Start a drag resizing which will inform all listeners that a window resize is taking place.
      */
-    private void startDragResizing(Rect initialBounds) {
+    private void startDragResizing(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
         if (!mDragResizing) {
             mDragResizing = true;
             synchronized (mWindowCallbacks) {
                 for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
-                    mWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds);
+                    mWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds, fullscreen,
+                            systemInsets, stableInsets);
                 }
             }
             mFullRedrawNeeded = true;
diff --git a/core/java/android/view/WindowCallbacks.java b/core/java/android/view/WindowCallbacks.java
index def0236..d2bfca7 100644
--- a/core/java/android/view/WindowCallbacks.java
+++ b/core/java/android/view/WindowCallbacks.java
@@ -28,20 +28,30 @@
 public interface WindowCallbacks {
     /**
      * Called by the system when the window got changed by the user, before the layouter got called.
-     * It can be used to perform a "quick and dirty" resize which should never take more then 4ms to
-     * complete.
+     * It also gets called when the insets changed, or when the window switched between a fullscreen
+     * layout or a non-fullscreen layout. It can be used to perform a "quick and dirty" resize which
+     * should never take more then 4ms to complete.
      *
      * <p>At the time the layouting has not happened yet.
      *
      * @param newBounds The new window frame bounds.
+     * @param fullscreen Whether the window is currently drawing in fullscreen.
+     * @param systemInsets The current visible system insets for the window.
+     * @param stableInsets The stable insets for the window.
      */
-    void onWindowSizeIsChanging(Rect newBounds);
+    void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets);
 
     /**
      * Called when a drag resize starts.
+     *
      * @param initialBounds The initial bounds where the window will be.
+     * @param fullscreen Whether the window is currently drawing in fullscreen.
+     * @param systemInsets The current visible system insets for the window.
+     * @param stableInsets The stable insets for the window.
      */
-    void onWindowDragResizeStart(Rect initialBounds);
+    void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets);
 
     /**
      * Called when a drag resize ends.
diff --git a/core/java/android/view/WindowManagerInternal.java b/core/java/android/view/WindowManagerInternal.java
index 89b1eb9..c22d60d 100644
--- a/core/java/android/view/WindowManagerInternal.java
+++ b/core/java/android/view/WindowManagerInternal.java
@@ -268,4 +268,9 @@
 
     /** Returns true if the stack with the input Id is currently visible. */
     public abstract boolean isStackVisible(int stackId);
+
+    /**
+     * @return True if and only if the docked divider is currently in resize mode.
+     */
+    public abstract boolean isDockedDividerResizing();
 }
diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java
index 4ca8971..9a68593 100644
--- a/core/java/android/widget/AbsSeekBar.java
+++ b/core/java/android/widget/AbsSeekBar.java
@@ -955,7 +955,7 @@
                 if (!canUserSetProgress()) {
                     return false;
                 }
-                int increment = Math.max(1, Math.round((float) getMax() / 5));
+                int increment = Math.max(1, Math.round((float) getMax() / 20));
                 if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
                     increment = -increment;
                 }
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index ee73092..1321221 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -2404,7 +2404,7 @@
         }
 
         final LayoutParams lp = (LayoutParams) view.getLayoutParams();
-        final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+        final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
         final boolean isSelected = isItemChecked(position);
         final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(
                 row, 1, column, 1, isHeading, isSelected);
diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java
index 584df08..8fa71a2 100644
--- a/core/java/android/widget/PopupWindow.java
+++ b/core/java/android/widget/PopupWindow.java
@@ -43,6 +43,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
 import android.view.View.OnTouchListener;
 import android.view.ViewGroup;
 import android.view.ViewParent;
@@ -164,7 +165,20 @@
         com.android.internal.R.attr.state_above_anchor
     };
 
+    private final OnAttachStateChangeListener mOnAnchorRootDetachedListener =
+            new OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {}
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    mIsAnchorRootAttached = false;
+                }
+            };
+
     private WeakReference<View> mAnchor;
+    private WeakReference<View> mAnchorRoot;
+    private boolean mIsAnchorRootAttached;
 
     private final OnScrollChangedListener mOnScrollChangedListener = new OnScrollChangedListener() {
         @Override
@@ -1037,7 +1051,7 @@
 
         TransitionManager.endTransitions(mDecorView);
 
-        unregisterForScrollChanged();
+        unregisterForViewTreeChanges();
 
         mIsShowing = true;
         mIsDropdown = false;
@@ -1120,7 +1134,7 @@
 
         TransitionManager.endTransitions(mDecorView);
 
-        registerForScrollChanged(anchor, xoff, yoff, gravity);
+        registerForViewTreeChanges(anchor, xoff, yoff, gravity);
 
         mIsShowing = true;
         mIsDropdown = true;
@@ -1633,14 +1647,23 @@
         mIsShowing = false;
         mIsTransitioningToDismiss = true;
 
+        // This method may be called as part of window detachment, in which
+        // case the anchor view (and its root) will still return true from
+        // isAttachedToWindow() during execution of this method; however, we
+        // can expect the OnAttachStateChangeListener to have been called prior
+        // to executing this method, so we can rely on that instead.
         final Transition exitTransition = mExitTransition;
-        if (exitTransition != null && decorView.isLaidOut()) {
+        if (!mIsAnchorRootAttached && exitTransition != null && decorView.isLaidOut()) {
             // The decor view is non-interactive during exit transitions.
             final LayoutParams p = (LayoutParams) decorView.getLayoutParams();
             p.flags |= LayoutParams.FLAG_NOT_TOUCHABLE;
             p.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
             mWindowManager.updateViewLayout(decorView, p);
 
+            // Once we start dismissing the decor view, all state (including
+            // the anchor root) needs to be moved to the decor view since we
+            // may open another popup while it's busy exiting.
+            final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null;
             final Rect epicenter = getTransitionEpicenter();
             exitTransition.setEpicenterCallback(new EpicenterCallback() {
                 @Override
@@ -1648,18 +1671,19 @@
                     return epicenter;
                 }
             });
-            decorView.startExitTransition(exitTransition, new TransitionListenerAdapter() {
-                @Override
-                public void onTransitionEnd(Transition transition) {
-                    dismissImmediate(decorView, contentHolder, contentView);
-                }
-            });
+            decorView.startExitTransition(exitTransition, anchorRoot,
+                    new TransitionListenerAdapter() {
+                        @Override
+                        public void onTransitionEnd(Transition transition) {
+                            dismissImmediate(decorView, contentHolder, contentView);
+                        }
+                    });
         } else {
             dismissImmediate(decorView, contentHolder, contentView);
         }
 
         // Clears the anchor view.
-        unregisterForScrollChanged();
+        unregisterForViewTreeChanges();
 
         if (mOnDismissListener != null) {
             mOnDismissListener.onDismiss();
@@ -1925,7 +1949,7 @@
         final WeakReference<View> oldAnchor = mAnchor;
         final boolean needsUpdate = updateLocation && (mAnchorXoff != xoff || mAnchorYoff != yoff);
         if (oldAnchor == null || oldAnchor.get() != anchor || (needsUpdate && !mIsDropdown)) {
-            registerForScrollChanged(anchor, xoff, yoff, mAnchoredGravity);
+            registerForViewTreeChanges(anchor, xoff, yoff, mAnchoredGravity);
         } else if (needsUpdate) {
             // No need to register again if this is a DropDown, showAsDropDown already did.
             mAnchorXoff = xoff;
@@ -1969,27 +1993,38 @@
         public void onDismiss();
     }
 
-    private void unregisterForScrollChanged() {
-        final WeakReference<View> anchorRef = mAnchor;
-        final View anchor = anchorRef == null ? null : anchorRef.get();
+    private void unregisterForViewTreeChanges() {
+        final View anchor = mAnchor != null ? mAnchor.get() : null;
         if (anchor != null) {
             final ViewTreeObserver vto = anchor.getViewTreeObserver();
             vto.removeOnScrollChangedListener(mOnScrollChangedListener);
         }
 
+        final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null;
+        if (anchorRoot != null) {
+            anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+        }
+
         mAnchor = null;
+        mAnchorRoot = null;
+        mIsAnchorRootAttached = false;
     }
 
-    private void registerForScrollChanged(View anchor, int xoff, int yoff, int gravity) {
-        unregisterForScrollChanged();
-
-        mAnchor = new WeakReference<>(anchor);
+    private void registerForViewTreeChanges(View anchor, int xoff, int yoff, int gravity) {
+        unregisterForViewTreeChanges();
 
         final ViewTreeObserver vto = anchor.getViewTreeObserver();
         if (vto != null) {
             vto.addOnScrollChangedListener(mOnScrollChangedListener);
         }
 
+        final View anchorRoot = anchor.getRootView();
+        anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+
+        mAnchor = new WeakReference<>(anchor);
+        mAnchorRoot = new WeakReference<>(anchorRoot);
+        mIsAnchorRootAttached = anchorRoot.isAttachedToWindow();
+
         mAnchorXoff = xoff;
         mAnchorYoff = yoff;
         mAnchoredGravity = gravity;
@@ -2109,16 +2144,23 @@
          * its {@code onTransitionEnd} method called even if the transition
          * never starts; however, it may be called with a {@code null} argument.
          */
-        public void startExitTransition(Transition transition, final TransitionListener listener) {
+        public void startExitTransition(Transition transition, final View anchorRoot,
+                final TransitionListener listener) {
             if (transition == null) {
                 return;
             }
 
+            // The anchor view's window may go away while we're executing our
+            // transition, in which case we need to end the transition
+            // immediately and execute the listener to remove the popup.
+            anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+
             // The exit listener MUST be called for cleanup, even if the
             // transition never starts or ends. Stash it for later.
             mPendingExitListener = new TransitionListenerAdapter() {
                 @Override
                 public void onTransitionEnd(Transition transition) {
+                    anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
                     listener.onTransitionEnd(transition);
 
                     // The listener was called. Our job here is done.
@@ -2153,6 +2195,19 @@
                 mPendingExitListener.onTransitionEnd(null);
             }
         }
+
+        private final OnAttachStateChangeListener mOnAnchorRootDetachedListener =
+                new OnAttachStateChangeListener() {
+                    @Override
+                    public void onViewAttachedToWindow(View v) {}
+
+                    @Override
+                    public void onViewDetachedFromWindow(View v) {
+                        v.removeOnAttachStateChangeListener(this);
+
+                        TransitionManager.endTransitions(PopupDecorView.this);
+                    }
+                };
     }
 
     private class PopupBackgroundView extends FrameLayout {
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index cf13a13..b0fb93b 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -761,6 +761,7 @@
         public static final int TARGET_STANDARD = 2;
 
         private static final int MAX_SERVICE_TARGETS = 8;
+        private static final int MAX_TARGETS_PER_SERVICE = 4;
 
         private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
         private final List<TargetInfo> mCallerTargets = new ArrayList<>();
@@ -925,7 +926,7 @@
             final float parentScore = getScore(origTarget);
             Collections.sort(targets, mBaseTargetComparator);
             float lastScore = 0;
-            for (int i = 0, N = targets.size(); i < N; i++) {
+            for (int i = 0, N = Math.min(targets.size(), MAX_TARGETS_PER_SERVICE); i < N; i++) {
                 final ChooserTarget target = targets.get(i);
                 float targetScore = target.getScore();
                 targetScore *= parentScore;
diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl
index ec53a2e..74fe94f 100644
--- a/core/java/com/android/internal/app/IBatteryStats.aidl
+++ b/core/java/com/android/internal/app/IBatteryStats.aidl
@@ -124,4 +124,5 @@
 
     void noteBleScanStarted(in WorkSource ws);
     void noteBleScanStopped(in WorkSource ws);
+    void noteResetBleScan();
 }
diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java
index dbec740..015e60d 100644
--- a/core/java/com/android/internal/app/IntentForwarderActivity.java
+++ b/core/java/com/android/internal/app/IntentForwarderActivity.java
@@ -85,8 +85,10 @@
         int callingUserId = getUserId();
 
         if (canForward(newIntent, targetUserId)) {
-            if (newIntent.getAction().equals(Intent.ACTION_CHOOSER)) {
+            if (Intent.ACTION_CHOOSER.equals(newIntent.getAction())) {
                 Intent innerIntent = (Intent) newIntent.getParcelableExtra(Intent.EXTRA_INTENT);
+                // At this point, innerIntent is not null. Otherwise, canForward would have returned
+                // false.
                 innerIntent.prepareToLeaveUser(callingUserId);
             } else {
                 newIntent.prepareToLeaveUser(callingUserId);
@@ -124,7 +126,7 @@
                 Toast.makeText(this, getString(userMessageId), Toast.LENGTH_LONG).show();
             }
         } else {
-            Slog.wtf(TAG, "the intent: " + newIntent + "cannot be forwarded from user "
+            Slog.wtf(TAG, "the intent: " + newIntent + " cannot be forwarded from user "
                     + callingUserId + " to user " + targetUserId);
         }
         finish();
@@ -132,7 +134,7 @@
 
     boolean canForward(Intent intent, int targetUserId)  {
         IPackageManager ipm = AppGlobals.getPackageManager();
-        if (intent.getAction().equals(Intent.ACTION_CHOOSER)) {
+        if (Intent.ACTION_CHOOSER.equals(intent.getAction())) {
             // The EXTRA_INITIAL_INTENTS may not be allowed to be forwarded.
             if (intent.hasExtra(Intent.EXTRA_INITIAL_INTENTS)) {
                 Slog.wtf(TAG, "An chooser intent with extra initial intents cannot be forwarded to"
@@ -145,6 +147,11 @@
                 return false;
             }
             intent = (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
+            if (intent == null) {
+                Slog.wtf(TAG, "Cannot forward a chooser intent with no extra "
+                        + Intent.EXTRA_INTENT);
+                return false;
+            }
         }
         String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
         if (intent.getSelector() != null) {
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 53e329d..3fb768f 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -52,6 +52,7 @@
 import android.os.Bundle;
 import android.os.PatternMatcher;
 import android.os.RemoteException;
+import android.os.StrictMode;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
@@ -173,6 +174,10 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        // We're dispatching intents that might be coming from legacy apps, so
+        // don't kill ourselves.
+        StrictMode.disableDeathOnFileUriExposure();
+
         // Use a specialized prompt when we're handling the 'Home' app startActivity()
         final Intent intent = makeMyIntent();
         final Set<String> categories = intent.getCategories();
diff --git a/core/java/com/android/internal/app/ResolverComparator.java b/core/java/com/android/internal/app/ResolverComparator.java
index 0ae21c6..03a3a38 100644
--- a/core/java/com/android/internal/app/ResolverComparator.java
+++ b/core/java/com/android/internal/app/ResolverComparator.java
@@ -52,7 +52,7 @@
 
     private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12;
 
-    private static final float RECENCY_MULTIPLIER = 3.f;
+    private static final float RECENCY_MULTIPLIER = 2.f;
 
     private final Collator mCollator;
     private final boolean mHttp;
diff --git a/core/java/com/android/internal/os/BatterySipper.java b/core/java/com/android/internal/os/BatterySipper.java
index 049d3eb..d92e596 100644
--- a/core/java/com/android/internal/os/BatterySipper.java
+++ b/core/java/com/android/internal/os/BatterySipper.java
@@ -44,6 +44,7 @@
     public long wakeLockTimeMs;
     public long cameraTimeMs;
     public long flashlightTimeMs;
+    public long bluetoothRunningTimeMs;
 
     public long mobileRxPackets;
     public long mobileTxPackets;
@@ -56,6 +57,8 @@
     public long mobileTxBytes;
     public long wifiRxBytes;
     public long wifiTxBytes;
+    public long btRxBytes;
+    public long btTxBytes;
     public double percent;
     public double noCoveragePercent;
     public String[] mPackages;
@@ -71,6 +74,7 @@
     public double sensorPowerMah;
     public double cameraPowerMah;
     public double flashlightPowerMah;
+    public double bluetoothPowerMah;
 
     public enum DrainType {
         IDLE,
@@ -142,6 +146,7 @@
         wakeLockTimeMs += other.wakeLockTimeMs;
         cameraTimeMs += other.cameraTimeMs;
         flashlightTimeMs += other.flashlightTimeMs;
+        bluetoothRunningTimeMs += other.bluetoothRunningTimeMs;
         mobileRxPackets += other.mobileRxPackets;
         mobileTxPackets += other.mobileTxPackets;
         mobileActive += other.mobileActive;
@@ -152,6 +157,8 @@
         mobileTxBytes += other.mobileTxBytes;
         wifiRxBytes += other.wifiRxBytes;
         wifiTxBytes += other.wifiTxBytes;
+        btRxBytes += other.btRxBytes;
+        btTxBytes += other.btTxBytes;
         wifiPowerMah += other.wifiPowerMah;
         gpsPowerMah += other.gpsPowerMah;
         cpuPowerMah += other.cpuPowerMah;
@@ -160,6 +167,7 @@
         wakeLockPowerMah += other.wakeLockPowerMah;
         cameraPowerMah += other.cameraPowerMah;
         flashlightPowerMah += other.flashlightPowerMah;
+        bluetoothPowerMah += other.bluetoothPowerMah;
     }
 
     /**
@@ -169,6 +177,6 @@
     public double sumPower() {
         return totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah +
                 sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah +
-                flashlightPowerMah;
+                flashlightPowerMah + bluetoothPowerMah;
     }
 }
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index e2ccaae..648b1a5 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -107,7 +107,7 @@
     private static final int MAGIC = 0xBA757475; // 'BATSTATS'
 
     // Current on-disk Parcel version
-    private static final int VERSION = 140 + (USE_OLD_HISTORY ? 1000 : 0);
+    private static final int VERSION = 141 + (USE_OLD_HISTORY ? 1000 : 0);
 
     // Maximum number of items we will record in the history.
     private static final int MAX_HISTORY_ITEMS = 2000;
@@ -186,8 +186,13 @@
     }
 
     public interface ExternalStatsSync {
-        void scheduleSync(String reason);
-        void scheduleWifiSync(String reason);
+        public static final int UPDATE_CPU = 0x01;
+        public static final int UPDATE_WIFI = 0x02;
+        public static final int UPDATE_RADIO = 0x04;
+        public static final int UPDATE_BT = 0x08;
+        public static final int UPDATE_ALL = UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT;
+
+        void scheduleSync(String reason, int flags);
         void scheduleCpuSyncDueToRemovedUid(int uid);
     }
 
@@ -224,6 +229,7 @@
     final ArrayList<StopwatchTimer> mVideoTurnedOnTimers = new ArrayList<>();
     final ArrayList<StopwatchTimer> mFlashlightTurnedOnTimers = new ArrayList<>();
     final ArrayList<StopwatchTimer> mCameraTurnedOnTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mBluetoothScanOnTimers = new ArrayList<>();
 
     // Last partial timers we use for distributing CPU usage.
     final ArrayList<StopwatchTimer> mLastPartialTimers = new ArrayList<>();
@@ -435,6 +441,9 @@
     final StopwatchTimer[] mWifiSignalStrengthsTimer =
             new StopwatchTimer[NUM_WIFI_SIGNAL_STRENGTH_BINS];
 
+    int mBluetoothScanNesting;
+    StopwatchTimer mBluetoothScanTimer;
+
     int mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
     long mMobileRadioActiveStartTime;
     StopwatchTimer mMobileRadioActiveTimer;
@@ -3714,7 +3723,7 @@
             addHistoryRecordLocked(elapsedRealtime, uptime);
             mWifiOn = true;
             mWifiOnTimer.startRunningLocked(elapsedRealtime);
-            scheduleSyncExternalWifiStatsLocked("wifi-off");
+            scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI);
         }
     }
 
@@ -3728,7 +3737,7 @@
             addHistoryRecordLocked(elapsedRealtime, uptime);
             mWifiOn = false;
             mWifiOnTimer.stopRunningLocked(elapsedRealtime);
-            scheduleSyncExternalWifiStatsLocked("wifi-on");
+            scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI);
         }
     }
 
@@ -3946,6 +3955,65 @@
         }
     }
 
+    private void noteBluetoothScanStartedLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
+        final long uptime = SystemClock.uptimeMillis();
+        if (mBluetoothScanNesting == 0) {
+            mHistoryCur.states2 |= HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan started for: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        mBluetoothScanNesting++;
+        getUidStatsLocked(uid).noteBluetoothScanStartedLocked(elapsedRealtime);
+    }
+
+    public void noteBluetoothScanStartedFromSourceLocked(WorkSource ws) {
+        final int N = ws.size();
+        for (int i = 0; i < N; i++) {
+            noteBluetoothScanStartedLocked(ws.get(i));
+        }
+    }
+
+    private void noteBluetoothScanStoppedLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
+        final long uptime = SystemClock.uptimeMillis();
+        mBluetoothScanNesting--;
+        if (mBluetoothScanNesting == 0) {
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan stopped for: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        getUidStatsLocked(uid).noteBluetoothScanStoppedLocked(elapsedRealtime);
+    }
+
+    public void noteBluetoothScanStoppedFromSourceLocked(WorkSource ws) {
+        final int N = ws.size();
+        for (int i = 0; i < N; i++) {
+            noteBluetoothScanStoppedLocked(ws.get(i));
+        }
+    }
+
+    public void noteResetBluetoothScanLocked() {
+        if (mBluetoothScanNesting > 0) {
+            final long elapsedRealtime = SystemClock.elapsedRealtime();
+            final long uptime = SystemClock.uptimeMillis();
+            mBluetoothScanNesting = 0;
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "BLE can stopped for: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtime);
+            for (int i=0; i<mUidStats.size(); i++) {
+                BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                uid.noteResetBluetoothScanLocked(elapsedRealtime);
+            }
+        }
+    }
+
     public void noteWifiRadioPowerState(int powerState, long timestampNs) {
         final long elapsedRealtime = SystemClock.elapsedRealtime();
         final long uptime = SystemClock.uptimeMillis();
@@ -3980,7 +4048,7 @@
                 int uid = mapUid(ws.get(i));
                 getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
             }
-            scheduleSyncExternalWifiStatsLocked("wifi-running");
+            scheduleSyncExternalStatsLocked("wifi-running", ExternalStatsSync.UPDATE_WIFI);
         } else {
             Log.w(TAG, "noteWifiRunningLocked -- called while WIFI running");
         }
@@ -4019,7 +4087,7 @@
                 int uid = mapUid(ws.get(i));
                 getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
             }
-            scheduleSyncExternalWifiStatsLocked("wifi-stopped");
+            scheduleSyncExternalStatsLocked("wifi-stopped", ExternalStatsSync.UPDATE_WIFI);
         } else {
             Log.w(TAG, "noteWifiStoppedLocked -- called while WIFI not running");
         }
@@ -4034,7 +4102,7 @@
             }
             mWifiState = wifiState;
             mWifiStateTimer[wifiState].startRunningLocked(elapsedRealtime);
-            scheduleSyncExternalWifiStatsLocked("wifi-state");
+            scheduleSyncExternalStatsLocked("wifi-state", ExternalStatsSync.UPDATE_WIFI);
         }
     }
 
@@ -4530,6 +4598,11 @@
     }
 
     @Override
+    public long getBluetoothScanTime(long elapsedRealtimeUs, int which) {
+        return mBluetoothScanTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override
     public long getNetworkActivityBytes(int type, int which) {
         if (type >= 0 && type < mNetworkByteActivityCounters.length) {
             return mNetworkByteActivityCounters[type].getCountLocked(which);
@@ -4603,9 +4676,8 @@
         StopwatchTimer mVideoTurnedOnTimer;
         StopwatchTimer mFlashlightTurnedOnTimer;
         StopwatchTimer mCameraTurnedOnTimer;
-
-
         StopwatchTimer mForegroundActivityTimer;
+        StopwatchTimer mBluetoothScanTimer;
 
         int mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
         StopwatchTimer[] mProcessStateTimer;
@@ -4997,6 +5069,30 @@
             return mForegroundActivityTimer;
         }
 
+        public StopwatchTimer createBluetoothScanTimerLocked() {
+            if (mBluetoothScanTimer == null) {
+                mBluetoothScanTimer = new StopwatchTimer(Uid.this, BLUETOOTH_SCAN_ON,
+                        mBluetoothScanOnTimers, mOnBatteryTimeBase);
+            }
+            return mBluetoothScanTimer;
+        }
+
+        public void noteBluetoothScanStartedLocked(long elapsedRealtimeMs) {
+            createBluetoothScanTimerLocked().startRunningLocked(elapsedRealtimeMs);
+        }
+
+        public void noteBluetoothScanStoppedLocked(long elapsedRealtimeMs) {
+            if (mBluetoothScanTimer != null) {
+                mBluetoothScanTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteResetBluetoothScanLocked(long elapsedRealtimeMs) {
+            if (mBluetoothScanTimer != null) {
+                mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
         @Override
         public void noteActivityResumedLocked(long elapsedRealtimeMs) {
             // We always start, since we want multiple foreground PIDs to nest
@@ -5110,6 +5206,11 @@
             return mForegroundActivityTimer;
         }
 
+        @Override
+        public Timer getBluetoothScanTimer() {
+            return mBluetoothScanTimer;
+        }
+
         void makeProcessState(int i, Parcel in) {
             if (i < 0 || i >= NUM_PROCESS_STATE) return;
 
@@ -5335,6 +5436,9 @@
             if (mForegroundActivityTimer != null) {
                 active |= !mForegroundActivityTimer.reset(false);
             }
+            if (mBluetoothScanTimer != null) {
+                active |= !mBluetoothScanTimer.reset(false);
+            }
             if (mProcessStateTimer != null) {
                 for (int i = 0; i < NUM_PROCESS_STATE; i++) {
                     if (mProcessStateTimer[i] != null) {
@@ -5509,6 +5613,10 @@
                     mForegroundActivityTimer.detach();
                     mForegroundActivityTimer = null;
                 }
+                if (mBluetoothScanTimer != null) {
+                    mBluetoothScanTimer.detach();
+                    mBluetoothScanTimer = null;
+                }
                 if (mUserActivityCounters != null) {
                     for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) {
                         mUserActivityCounters[i].detach();
@@ -5669,6 +5777,12 @@
             } else {
                 out.writeInt(0);
             }
+            if (mBluetoothScanTimer != null) {
+                out.writeInt(1);
+                mBluetoothScanTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
             for (int i = 0; i < NUM_PROCESS_STATE; i++) {
                 if (mProcessStateTimer[i] != null) {
                     out.writeInt(1);
@@ -5874,6 +5988,12 @@
             } else {
                 mForegroundActivityTimer = null;
             }
+            if (in.readInt() != 0) {
+                mBluetoothScanTimer = new StopwatchTimer(Uid.this, BLUETOOTH_SCAN_ON,
+                        mBluetoothScanOnTimers, mOnBatteryTimeBase, in);
+            } else {
+                mBluetoothScanTimer = null;
+            }
             mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
             for (int i = 0; i < NUM_PROCESS_STATE; i++) {
                 if (in.readInt() != 0) {
@@ -7133,6 +7253,7 @@
         mVideoOnTimer = new StopwatchTimer(null, -8, null, mOnBatteryTimeBase);
         mFlashlightOnTimer = new StopwatchTimer(null, -9, null, mOnBatteryTimeBase);
         mCameraOnTimer = new StopwatchTimer(null, -13, null, mOnBatteryTimeBase);
+        mBluetoothScanTimer = new StopwatchTimer(null, -14, null, mOnBatteryTimeBase);
         mOnBattery = mOnBatteryInternal = false;
         long uptime = SystemClock.uptimeMillis() * 1000;
         long realtime = SystemClock.elapsedRealtime() * 1000;
@@ -7732,6 +7853,7 @@
         mVideoOnTimer.reset(false);
         mFlashlightOnTimer.reset(false);
         mCameraOnTimer.reset(false);
+        mBluetoothScanTimer.reset(false);
         for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
             mPhoneSignalStrengthsTimer[i].reset(false);
         }
@@ -8264,41 +8386,168 @@
             Slog.d(TAG, "Updating bluetooth stats: " + info);
         }
 
-        if (info != null && mOnBatteryInternal) {
-            mHasBluetoothReporting = true;
-            mBluetoothActivity.getRxTimeCounter().addCountLocked(
-                    info.getControllerRxTimeMillis());
-            mBluetoothActivity.getTxTimeCounters()[0].addCountLocked(
-                    info.getControllerTxTimeMillis());
-            mBluetoothActivity.getIdleTimeCounter().addCountLocked(
-                    info.getControllerIdleTimeMillis());
+        if (info == null || !mOnBatteryInternal) {
+            return;
+        }
 
-            // POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
-            final double opVolt = mPowerProfile.getAveragePower(
-                    PowerProfile.POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
-            if (opVolt != 0) {
-                // We store the power drain as mAms.
-                mBluetoothActivity.getPowerCounter().addCountLocked(
-                        (long) (info.getControllerEnergyUsed() / opVolt));
+        mHasBluetoothReporting = true;
+
+        final long elapsedRealtimeMs = SystemClock.elapsedRealtime();
+        final long rxTimeMs = info.getControllerRxTimeMillis();
+        final long txTimeMs = info.getControllerTxTimeMillis();
+
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "------ BEGIN BLE power blaming ------");
+            Slog.d(TAG, "  Tx Time:    " + txTimeMs + " ms");
+            Slog.d(TAG, "  Rx Time:    " + rxTimeMs + " ms");
+            Slog.d(TAG, "  Idle Time:  " + info.getControllerIdleTimeMillis() + " ms");
+        }
+
+        long totalScanTimeMs = 0;
+
+        final int uidCount = mUidStats.size();
+        for (int i = 0; i < uidCount; i++) {
+            final Uid u = mUidStats.valueAt(i);
+            if (u.mBluetoothScanTimer == null) {
+                continue;
             }
 
-            final UidTraffic[] uidTraffic = info.getUidTraffic();
-            final int numUids = uidTraffic != null ? uidTraffic.length : 0;
+            totalScanTimeMs += u.mBluetoothScanTimer.getTimeSinceMarkLocked(
+                    elapsedRealtimeMs * 1000) / 1000;
+        }
+
+        final boolean normalizeScanRxTime = (totalScanTimeMs > rxTimeMs);
+        final boolean normalizeScanTxTime = (totalScanTimeMs > txTimeMs);
+
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "Normalizing scan power for RX=" + normalizeScanRxTime
+                    + " TX=" + normalizeScanTxTime);
+        }
+
+        long leftOverRxTimeMs = rxTimeMs;
+        long leftOverTxTimeMs = txTimeMs;
+
+        for (int i = 0; i < uidCount; i++) {
+            final Uid u = mUidStats.valueAt(i);
+            if (u.mBluetoothScanTimer == null) {
+                continue;
+            }
+
+            long scanTimeSinceMarkMs = u.mBluetoothScanTimer.getTimeSinceMarkLocked(
+                    elapsedRealtimeMs * 1000) / 1000;
+            if (scanTimeSinceMarkMs > 0) {
+                // Set the new mark so that next time we get new data since this point.
+                u.mBluetoothScanTimer.setMark(elapsedRealtimeMs);
+
+                long scanTimeRxSinceMarkMs = scanTimeSinceMarkMs;
+                long scanTimeTxSinceMarkMs = scanTimeSinceMarkMs;
+
+                if (normalizeScanRxTime) {
+                    // Scan time is longer than the total rx time in the controller,
+                    // so distribute the scan time proportionately. This means regular traffic
+                    // will not blamed, but scans are more expensive anyways.
+                    scanTimeRxSinceMarkMs = (rxTimeMs * scanTimeRxSinceMarkMs) / totalScanTimeMs;
+                }
+
+                if (normalizeScanTxTime) {
+                    // Scan time is longer than the total tx time in the controller,
+                    // so distribute the scan time proportionately. This means regular traffic
+                    // will not blamed, but scans are more expensive anyways.
+                    scanTimeTxSinceMarkMs = (txTimeMs * scanTimeTxSinceMarkMs) / totalScanTimeMs;
+                }
+
+                final ControllerActivityCounterImpl counter =
+                        u.getOrCreateBluetoothControllerActivityLocked();
+                counter.getRxTimeCounter().addCountLocked(scanTimeRxSinceMarkMs);
+                counter.getTxTimeCounters()[0].addCountLocked(scanTimeTxSinceMarkMs);
+
+                leftOverRxTimeMs -= scanTimeRxSinceMarkMs;
+                leftOverTxTimeMs -= scanTimeTxSinceMarkMs;
+            }
+        }
+
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "Left over time for traffic RX=" + leftOverRxTimeMs
+                    + " TX=" + leftOverTxTimeMs);
+        }
+
+        //
+        // Now distribute blame to apps that did bluetooth traffic.
+        //
+
+        long totalTxBytes = 0;
+        long totalRxBytes = 0;
+
+        final UidTraffic[] uidTraffic = info.getUidTraffic();
+        final int numUids = uidTraffic != null ? uidTraffic.length : 0;
+        for (int i = 0; i < numUids; i++) {
+            final UidTraffic traffic = uidTraffic[i];
+
+            // Add to the global counters.
+            mNetworkByteActivityCounters[NETWORK_BT_RX_DATA].addCountLocked(
+                    traffic.getRxBytes());
+            mNetworkByteActivityCounters[NETWORK_BT_TX_DATA].addCountLocked(
+                    traffic.getTxBytes());
+
+            // Add to the UID counters.
+            final Uid u = getUidStatsLocked(mapUid(traffic.getUid()));
+            u.noteNetworkActivityLocked(NETWORK_BT_RX_DATA, traffic.getRxBytes(), 0);
+            u.noteNetworkActivityLocked(NETWORK_BT_TX_DATA, traffic.getTxBytes(), 0);
+
+            // Calculate the total traffic.
+            totalTxBytes += traffic.getTxBytes();
+            totalRxBytes += traffic.getRxBytes();
+        }
+
+        if ((totalTxBytes != 0 || totalRxBytes != 0) &&
+                (leftOverRxTimeMs != 0 || leftOverTxTimeMs != 0)) {
             for (int i = 0; i < numUids; i++) {
                 final UidTraffic traffic = uidTraffic[i];
 
-                // Add to the global counters.
-                mNetworkByteActivityCounters[NETWORK_BT_RX_DATA].addCountLocked(
-                        traffic.getRxBytes());
-                mNetworkByteActivityCounters[NETWORK_BT_TX_DATA].addCountLocked(
-                        traffic.getTxBytes());
-
-                // Add to the UID counters.
                 final Uid u = getUidStatsLocked(mapUid(traffic.getUid()));
-                u.noteNetworkActivityLocked(NETWORK_BT_RX_DATA, traffic.getRxBytes(), 0);
-                u.noteNetworkActivityLocked(NETWORK_BT_TX_DATA, traffic.getTxBytes(), 0);
+                final ControllerActivityCounterImpl counter =
+                        u.getOrCreateBluetoothControllerActivityLocked();
+
+                if (totalRxBytes > 0 && traffic.getRxBytes() > 0) {
+                    final long timeRxMs = (leftOverRxTimeMs * traffic.getRxBytes()) / totalRxBytes;
+
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "UID=" + traffic.getUid() + " rx_bytes=" + traffic.getRxBytes()
+                                + " rx_time=" + timeRxMs);
+                    }
+                    counter.getRxTimeCounter().addCountLocked(timeRxMs);
+                    leftOverRxTimeMs -= timeRxMs;
+                }
+
+                if (totalTxBytes > 0 && traffic.getTxBytes() > 0) {
+                    final long timeTxMs = (leftOverTxTimeMs * traffic.getTxBytes()) / totalTxBytes;
+
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "UID=" + traffic.getUid() + " tx_bytes=" + traffic.getTxBytes()
+                                + " tx_time=" + timeTxMs);
+                    }
+
+                    counter.getTxTimeCounters()[0].addCountLocked(timeTxMs);
+                    leftOverTxTimeMs -= timeTxMs;
+                }
             }
         }
+
+        mBluetoothActivity.getRxTimeCounter().addCountLocked(
+                info.getControllerRxTimeMillis());
+        mBluetoothActivity.getTxTimeCounters()[0].addCountLocked(
+                info.getControllerTxTimeMillis());
+        mBluetoothActivity.getIdleTimeCounter().addCountLocked(
+                info.getControllerIdleTimeMillis());
+
+        // POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
+        final double opVolt = mPowerProfile.getAveragePower(
+                PowerProfile.POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+        if (opVolt != 0) {
+            // We store the power drain as mAms.
+            mBluetoothActivity.getPowerCounter().addCountLocked(
+                    (long) (info.getControllerEnergyUsed() / opVolt));
+        }
     }
 
     /**
@@ -8739,15 +8988,9 @@
         }
     }
 
-    private void scheduleSyncExternalStatsLocked(String reason) {
+    private void scheduleSyncExternalStatsLocked(String reason, int updateFlags) {
         if (mExternalSync != null) {
-            mExternalSync.scheduleSync(reason);
-        }
-    }
-
-    private void scheduleSyncExternalWifiStatsLocked(String reason) {
-        if (mExternalSync != null) {
-            mExternalSync.scheduleWifiSync(reason);
+            mExternalSync.scheduleSync(reason, updateFlags);
         }
     }
 
@@ -8815,7 +9058,7 @@
 
                 // TODO(adamlesinski): Schedule the creation of a HistoryStepDetails record
                 // which will pull external stats.
-                scheduleSyncExternalStatsLocked("battery-level");
+                scheduleSyncExternalStatsLocked("battery-level", ExternalStatsSync.UPDATE_ALL);
             }
             if (mHistoryCur.batteryStatus != status) {
                 mHistoryCur.batteryStatus = (byte)status;
@@ -9596,6 +9839,8 @@
         mFlashlightOnTimer.readSummaryFromParcelLocked(in);
         mCameraOnNesting = 0;
         mCameraOnTimer.readSummaryFromParcelLocked(in);
+        mBluetoothScanNesting = 0;
+        mBluetoothScanTimer.readSummaryFromParcelLocked(in);
 
         int NKW = in.readInt();
         if (NKW > 10000) {
@@ -9666,6 +9911,9 @@
             if (in.readInt() != 0) {
                 u.createForegroundActivityTimerLocked().readSummaryFromParcelLocked(in);
             }
+            if (in.readInt() != 0) {
+                u.createBluetoothScanTimerLocked().readSummaryFromParcelLocked(in);
+            }
             u.mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
             for (int i = 0; i < Uid.NUM_PROCESS_STATE; i++) {
                 if (in.readInt() != 0) {
@@ -9928,6 +10176,7 @@
         out.writeInt(mNumConnectivityChange);
         mFlashlightOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
         mCameraOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mBluetoothScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
 
         out.writeInt(mKernelWakelockStats.size());
         for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) {
@@ -10021,6 +10270,12 @@
             } else {
                 out.writeInt(0);
             }
+            if (u.mBluetoothScanTimer != null) {
+                out.writeInt(1);
+                u.mBluetoothScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
             for (int i = 0; i < Uid.NUM_PROCESS_STATE; i++) {
                 if (u.mProcessStateTimer[i] != null) {
                     out.writeInt(1);
@@ -10289,6 +10544,8 @@
         mFlashlightOnTimer = new StopwatchTimer(null, -9, null, mOnBatteryTimeBase, in);
         mCameraOnNesting = 0;
         mCameraOnTimer = new StopwatchTimer(null, -13, null, mOnBatteryTimeBase, in);
+        mBluetoothScanNesting = 0;
+        mBluetoothScanTimer = new StopwatchTimer(null, -14, null, mOnBatteryTimeBase, in);
         mDischargeUnplugLevel = in.readInt();
         mDischargePlugLevel = in.readInt();
         mDischargeCurrentLevel = in.readInt();
@@ -10436,6 +10693,7 @@
         out.writeInt(mUnpluggedNumConnectivityChange);
         mFlashlightOnTimer.writeToParcel(out, uSecRealtime);
         mCameraOnTimer.writeToParcel(out, uSecRealtime);
+        mBluetoothScanTimer.writeToParcel(out, uSecRealtime);
         out.writeInt(mDischargeUnplugLevel);
         out.writeInt(mDischargePlugLevel);
         out.writeInt(mDischargeCurrentLevel);
diff --git a/core/java/com/android/internal/os/BluetoothPowerCalculator.java b/core/java/com/android/internal/os/BluetoothPowerCalculator.java
index 531d1fa..2f383eac 100644
--- a/core/java/com/android/internal/os/BluetoothPowerCalculator.java
+++ b/core/java/com/android/internal/os/BluetoothPowerCalculator.java
@@ -24,6 +24,8 @@
     private final double mIdleMa;
     private final double mRxMa;
     private final double mTxMa;
+    private double mAppTotalPowerMah = 0;
+    private long mAppTotalTimeMs = 0;
 
     public BluetoothPowerCalculator(PowerProfile profile) {
         mIdleMa = profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE);
@@ -34,7 +36,31 @@
     @Override
     public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                              long rawUptimeUs, int statsType) {
-        // No per-app distribution yet.
+
+        final BatteryStats.ControllerActivityCounter counter = u.getBluetoothControllerActivity();
+        if (counter == null) {
+            return;
+        }
+
+        final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
+        final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
+        final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
+        final long totalTimeMs = idleTimeMs + txTimeMs + rxTimeMs;
+        double powerMah = counter.getPowerCounter().getCountLocked(statsType)
+                / (double)(1000*60*60);
+
+        if (powerMah == 0) {
+            powerMah = ((idleTimeMs * mIdleMa) + (rxTimeMs * mRxMa) + (txTimeMs * mTxMa))
+                    / (1000*60*60);
+        }
+
+        app.bluetoothPowerMah = powerMah;
+        app.bluetoothRunningTimeMs = totalTimeMs;
+        app.btRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_BT_RX_DATA, statsType);
+        app.btTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_BT_TX_DATA, statsType);
+
+        mAppTotalPowerMah += powerMah;
+        mAppTotalTimeMs += totalTimeMs;
     }
 
     @Override
@@ -56,12 +82,21 @@
                     / (1000*60*60);
         }
 
+        // Subtract what the apps used, but clamp to 0.
+        powerMah = Math.max(0, powerMah - mAppTotalPowerMah);
+
         if (DEBUG && powerMah != 0) {
             Log.d(TAG, "Bluetooth active: time=" + (totalTimeMs)
                     + " power=" + BatteryStatsHelper.makemAh(powerMah));
         }
 
-        app.usagePowerMah = powerMah;
-        app.usageTimeMs = totalTimeMs;
+        app.bluetoothPowerMah = powerMah;
+        app.bluetoothRunningTimeMs = Math.max(0, totalTimeMs - mAppTotalTimeMs);
+    }
+
+    @Override
+    public void reset() {
+        mAppTotalPowerMah = 0;
+        mAppTotalTimeMs = 0;
     }
 }
diff --git a/core/java/com/android/internal/os/WifiPowerCalculator.java b/core/java/com/android/internal/os/WifiPowerCalculator.java
index 2a27f70..b447039 100644
--- a/core/java/com/android/internal/os/WifiPowerCalculator.java
+++ b/core/java/com/android/internal/os/WifiPowerCalculator.java
@@ -29,6 +29,7 @@
     private final double mTxCurrentMa;
     private final double mRxCurrentMa;
     private double mTotalAppPowerDrain = 0;
+    private long mTotalAppRunningTime = 0;
 
     public WifiPowerCalculator(PowerProfile profile) {
         mIdleCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
@@ -48,6 +49,8 @@
         final long txTime = counter.getTxTimeCounters()[0].getCountLocked(statsType);
         final long rxTime = counter.getRxTimeCounter().getCountLocked(statsType);
         app.wifiRunningTimeMs = idleTime + rxTime + txTime;
+        mTotalAppRunningTime += app.wifiRunningTimeMs;
+
         app.wifiPowerMah =
                 ((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
                 / (1000*60*60);
@@ -76,7 +79,9 @@
         final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
         final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
         final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
-        app.wifiRunningTimeMs = idleTimeMs + rxTimeMs + txTimeMs;
+
+        app.wifiRunningTimeMs = Math.max(0,
+                (idleTimeMs + rxTimeMs + txTimeMs) - mTotalAppRunningTime);
 
         double powerDrainMah = counter.getPowerCounter().getCountLocked(statsType)
                 / (double)(1000*60*60);
@@ -95,5 +100,6 @@
     @Override
     public void reset() {
         mTotalAppPowerDrain = 0;
+        mTotalAppRunningTime = 0;
     }
 }
diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java
index d23f26d..919254a 100644
--- a/core/java/com/android/internal/os/Zygote.java
+++ b/core/java/com/android/internal/os/Zygote.java
@@ -41,6 +41,10 @@
     public static final int DEBUG_ENABLE_JNI_LOGGING = 1 << 4;
     /** Force generation of native debugging information. */
     public static final int DEBUG_GENERATE_DEBUG_INFO = 1 << 5;
+    /** Always use JIT-ed code. */
+    public static final int DEBUG_ALWAYS_JIT = 1 << 6;
+    /** Make the code debuggable with turning off some optimizations. */
+    public static final int DEBUG_NATIVE_DEBUGGABLE = 1 << 7;
 
     /** No external storage should be mounted. */
     public static final int MOUNT_EXTERNAL_NONE = 0;
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index a40f9a8..85d84bb 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -434,6 +434,10 @@
                     debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;
                 } else if (arg.equals("--generate-debug-info")) {
                     debugFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO;
+                } else if (arg.equals("--always-jit")) {
+                    debugFlags |= Zygote.DEBUG_ALWAYS_JIT;
+                } else if (arg.equals("--native-debuggable")) {
+                    debugFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE;
                 } else if (arg.equals("--enable-jni-logging")) {
                     debugFlags |= Zygote.DEBUG_ENABLE_JNI_LOGGING;
                 } else if (arg.equals("--enable-assert")) {
diff --git a/core/java/com/android/internal/policy/BackdropFrameRenderer.java b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
index 5047c4c..6931193 100644
--- a/core/java/com/android/internal/policy/BackdropFrameRenderer.java
+++ b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
@@ -24,7 +24,6 @@
 import android.view.DisplayListCanvas;
 import android.view.RenderNode;
 import android.view.ThreadedRenderer;
-import android.view.View;
 
 /**
  * The thread which draws a fill in background while the app is resizing in areas where the app
@@ -49,6 +48,7 @@
 
     private final Rect mOldTargetRect = new Rect();
     private final Rect mNewTargetRect = new Rect();
+
     private Choreographer mChoreographer;
 
     // Cached size values from the last render for the case that the view hierarchy is gone
@@ -66,15 +66,23 @@
     private Drawable mUserCaptionBackgroundDrawable;
     private Drawable mResizingBackgroundDrawable;
     private ColorDrawable mStatusBarColor;
+    private ColorDrawable mNavigationBarColor;
+    private boolean mOldFullscreen;
+    private boolean mFullscreen;
+    private final Rect mOldSystemInsets = new Rect();
+    private final Rect mOldStableInsets = new Rect();
+    private final Rect mSystemInsets = new Rect();
+    private final Rect mStableInsets = new Rect();
 
     public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
             Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
-            Drawable userCaptionBackgroundDrawable, int statusBarColor) {
+            Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor,
+            boolean fullscreen, Rect systemInsets, Rect stableInsets) {
         setName("ResizeFrame");
 
         mRenderer = renderer;
         onResourcesLoaded(decorView, resizingBackgroundDrawable, captionBackgroundDrawable,
-                userCaptionBackgroundDrawable, statusBarColor);
+                userCaptionBackgroundDrawable, statusBarColor, navigationBarColor);
 
         // Create a render node for the content and frame backdrop
         // which can be resized independently from the content.
@@ -84,8 +92,14 @@
 
         // Set the initial bounds and draw once so that we do not get a broken frame.
         mTargetRect.set(initialBounds);
+        mFullscreen = fullscreen;
+        mOldFullscreen = fullscreen;
+        mSystemInsets.set(systemInsets);
+        mStableInsets.set(stableInsets);
+        mOldSystemInsets.set(systemInsets);
+        mOldStableInsets.set(stableInsets);
         synchronized (this) {
-            changeWindowSizeLocked(initialBounds);
+            redrawLocked(initialBounds, fullscreen, mSystemInsets, mStableInsets);
         }
 
         // Kick off our draw thread.
@@ -94,7 +108,7 @@
 
     void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
             Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
-            int statusBarColor) {
+            int statusBarColor, int navigationBarColor) {
         mDecorView = decorView;
         mResizingBackgroundDrawable = resizingBackgroundDrawable;
         mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable;
@@ -108,6 +122,12 @@
         } else {
             mStatusBarColor = null;
         }
+        if (navigationBarColor != 0) {
+            mNavigationBarColor = new ColorDrawable(navigationBarColor);
+            addSystemBarNodeIfNeeded();
+        } else {
+            mNavigationBarColor = null;
+        }
     }
 
     private void addSystemBarNodeIfNeeded() {
@@ -119,13 +139,22 @@
     }
 
     /**
-     * Call this function asynchronously when the window size has been changed. The change will
-     * be picked up once per frame and the frame will be re-rendered accordingly.
+     * Call this function asynchronously when the window size has been changed or when the insets
+     * have changed or whether window switched between a fullscreen or non-fullscreen layout.
+     * The change will be picked up once per frame and the frame will be re-rendered accordingly.
+     *
      * @param newTargetBounds The new target bounds.
+     * @param fullscreen Whether the window is currently drawing in fullscreen.
+     * @param systemInsets The current visible system insets for the window.
+     * @param stableInsets The stable insets for the window.
      */
-    public void setTargetRect(Rect newTargetBounds) {
+    public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
         synchronized (this) {
+            mFullscreen = fullscreen;
             mTargetRect.set(newTargetBounds);
+            mSystemInsets.set(systemInsets);
+            mStableInsets.set(stableInsets);
             // Notify of a bounds change.
             pingRenderLocked();
         }
@@ -204,16 +233,23 @@
                 return;
             }
             mNewTargetRect.set(mTargetRect);
-            if (!mNewTargetRect.equals(mOldTargetRect) || mReportNextDraw) {
+            if (!mNewTargetRect.equals(mOldTargetRect)
+                    || mOldFullscreen != mFullscreen
+                    || !mStableInsets.equals(mOldStableInsets)
+                    || !mSystemInsets.equals(mOldSystemInsets)
+                    || mReportNextDraw) {
+                mOldFullscreen = mFullscreen;
                 mOldTargetRect.set(mNewTargetRect);
-                changeWindowSizeLocked(mNewTargetRect);
+                mOldSystemInsets.set(mSystemInsets);
+                mOldStableInsets.set(mStableInsets);
+                redrawLocked(mNewTargetRect, mFullscreen, mSystemInsets, mStableInsets);
             }
         }
     }
 
     /**
      * The content is about to be drawn and we got the location of where it will be shown.
-     * If a "changeWindowSizeLocked" call has already been processed, we will re-issue the call
+     * If a "redrawLocked" call has already been processed, we will re-issue the call
      * if the previous call was ignored since the size was unknown.
      * @param xOffset The x offset where the content is drawn to.
      * @param yOffset The y offset where the content is drawn to.
@@ -235,8 +271,8 @@
                     mLastYOffset,
                     mLastXOffset + mLastContentWidth,
                     mLastYOffset + mLastCaptionHeight + mLastContentHeight);
-            // If this was the first call and changeWindowSizeLocked got already called prior
-            // to us, we should re-issue a changeWindowSizeLocked now.
+            // If this was the first call and redrawLocked got already called prior
+            // to us, we should re-issue a redrawLocked now.
             return firstCall
                     && (mLastCaptionHeight != 0 || !mDecorView.isShowingCaption());
         }
@@ -251,16 +287,20 @@
     }
 
     /**
-     * Resizing the frame to fit the new window size.
+     * Redraws the background, the caption and the system inset backgrounds if something changed.
+     *
      * @param newBounds The window bounds which needs to be drawn.
+     * @param fullscreen Whether the window is currently drawing in fullscreen.
+     * @param systemInsets The current visible system insets for the window.
+     * @param stableInsets The stable insets for the window.
      */
-    private void changeWindowSizeLocked(Rect newBounds) {
+    private void redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
 
         // While a configuration change is taking place the view hierarchy might become
         // inaccessible. For that case we remember the previous metrics to avoid flashes.
         // Note that even when there is no visible caption, the caption child will exist.
         final int captionHeight = mDecorView.getCaptionHeight();
-        final int statusBarHeight = mDecorView.getStatusBarHeight();
 
         // The caption height will probably never dynamically change while we are resizing.
         // Once set to something other then 0 it should be kept that way.
@@ -302,14 +342,7 @@
         }
         mFrameAndBackdropNode.end(canvas);
 
-        if (mSystemBarBackgroundNode != null && mStatusBarColor != null) {
-            canvas = mSystemBarBackgroundNode.start(width, height);
-            mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
-            mStatusBarColor.setBounds(0, 0, left + width, statusBarHeight);
-            mStatusBarColor.draw(canvas);
-            mSystemBarBackgroundNode.end(canvas);
-            mRenderer.drawRenderNode(mSystemBarBackgroundNode);
-        }
+        drawColorViews(left, top, width, height, fullscreen, systemInsets, stableInsets);
 
         // We need to render the node explicitly
         mRenderer.drawRenderNode(mFrameAndBackdropNode);
@@ -317,6 +350,39 @@
         reportDrawIfNeeded();
     }
 
+    private void drawColorViews(int left, int top, int width, int height,
+            boolean fullscreen, Rect systemInsets, Rect stableInsets) {
+        if (mSystemBarBackgroundNode == null) {
+            return;
+        }
+        DisplayListCanvas canvas = mSystemBarBackgroundNode.start(width, height);
+        mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
+        final int topInset = DecorView.getColorViewTopInset(mStableInsets.top, mSystemInsets.top);
+        final int bottomInset = DecorView.getColorViewBottomInset(stableInsets.bottom,
+                systemInsets.bottom);
+        final int rightInset = DecorView.getColorViewRightInset(stableInsets.right,
+                systemInsets.right);
+        if (mStatusBarColor != null) {
+            mStatusBarColor.setBounds(0, 0, left + width, topInset);
+            mStatusBarColor.draw(canvas);
+        }
+
+        // We only want to draw the navigation bar if our window is currently fullscreen because we
+        // don't want the navigation bar background be moving around when resizing in docked mode.
+        // However, we need it for the transitions into/out of docked mode.
+        if (mNavigationBarColor != null && fullscreen) {
+            final int size = DecorView.getNavBarSize(bottomInset, rightInset);
+            if (DecorView.isNavBarToRightEdge(bottomInset, rightInset)) {
+                mNavigationBarColor.setBounds(width - size, 0, width, height);
+            } else {
+                mNavigationBarColor.setBounds(0, height - size, width, height);
+            }
+            mNavigationBarColor.draw(canvas);
+        }
+        mSystemBarBackgroundNode.end(canvas);
+        mRenderer.drawRenderNode(mSystemBarBackgroundNode);
+    }
+
     /** Notify view root that a frame has been drawn by us, if it has requested so. */
     private void reportDrawIfNeeded() {
         if (mReportNextDraw) {
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 1a20e5c..d4ada95 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -922,6 +922,26 @@
         return false;
     }
 
+    static int getColorViewTopInset(int stableTop, int systemTop) {
+        return Math.min(stableTop, systemTop);
+    }
+
+    static int getColorViewBottomInset(int stableBottom, int systemBottom) {
+        return Math.min(stableBottom, systemBottom);
+    }
+
+    static int getColorViewRightInset(int stableRight, int systemRight) {
+        return Math.min(stableRight, systemRight);
+    }
+
+    static boolean isNavBarToRightEdge(int bottomInset, int rightInset) {
+        return bottomInset == 0 && rightInset > 0;
+    }
+
+    static int getNavBarSize(int bottomInset, int rightInset) {
+        return isNavBarToRightEdge(bottomInset, rightInset) ? rightInset : bottomInset;
+    }
+
     WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
         WindowManager.LayoutParams attrs = mWindow.getAttributes();
         int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
@@ -933,11 +953,11 @@
             mLastWindowFlags = attrs.flags;
 
             if (insets != null) {
-                mLastTopInset = Math.min(insets.getStableInsetTop(),
+                mLastTopInset = getColorViewTopInset(insets.getStableInsetTop(),
                         insets.getSystemWindowInsetTop());
-                mLastBottomInset = Math.min(insets.getStableInsetBottom(),
+                mLastBottomInset = getColorViewBottomInset(insets.getStableInsetBottom(),
                         insets.getSystemWindowInsetBottom());
-                mLastRightInset = Math.min(insets.getStableInsetRight(),
+                mLastRightInset = getColorViewRightInset(insets.getStableInsetRight(),
                         insets.getSystemWindowInsetRight());
 
                 // Don't animate if the presence of stable insets has changed, because that
@@ -956,8 +976,8 @@
                 mLastHasRightStableInset = hasRightStableInset;
             }
 
-            boolean navBarToRightEdge = mLastBottomInset == 0 && mLastRightInset > 0;
-            int navBarSize = navBarToRightEdge ? mLastRightInset : mLastBottomInset;
+            boolean navBarToRightEdge = isNavBarToRightEdge(mLastBottomInset, mLastRightInset);
+            int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset);
             updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
                     mWindow.mNavigationBarColor, navBarSize, navBarToRightEdge,
                     0 /* rightInset */, animate && !disallowAnimate, false /* force */);
@@ -1041,14 +1061,14 @@
      */
     private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
             int size, boolean verticalBar, int rightMargin, boolean animate, boolean force) {
-        state.present = size > 0 && (sysUiVis & state.systemUiHideFlag) == 0
+        state.present = (sysUiVis & state.systemUiHideFlag) == 0
                 && (mWindow.getAttributes().flags & state.hideWindowFlag) == 0
                 && ((mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
                         || force);
         boolean show = state.present
                 && (color & Color.BLACK) != 0
                 && ((mWindow.getAttributes().flags & state.translucentFlag) == 0  || force);
-        boolean showView = show && !isResizing();
+        boolean showView = show && !isResizing() && size > 0;
 
         boolean visibilityChanged = false;
         View view = state.view;
@@ -1672,7 +1692,8 @@
             loadBackgroundDrawablesIfNeeded();
             mBackdropFrameRenderer.onResourcesLoaded(
                     this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
-                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState));
+                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
+                    getCurrentColor(mNavigationColorViewState));
         }
 
         mDecorCaptionView = createDecorCaptionView(inflater);
@@ -1854,14 +1875,16 @@
     }
 
     @Override
-    public void onWindowSizeIsChanging(Rect newBounds) {
+    public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
         if (mBackdropFrameRenderer != null) {
-            mBackdropFrameRenderer.setTargetRect(newBounds);
+            mBackdropFrameRenderer.setTargetRect(newBounds, fullscreen, systemInsets, stableInsets);
         }
     }
 
     @Override
-    public void onWindowDragResizeStart(Rect initialBounds) {
+    public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
         if (mWindow.isDestroyed()) {
             // If the owner's window is gone, we should not be able to come here anymore.
             releaseThreadedRenderer();
@@ -1875,7 +1898,9 @@
             loadBackgroundDrawablesIfNeeded();
             mBackdropFrameRenderer = new BackdropFrameRenderer(this, renderer,
                     initialBounds, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
-                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState));
+                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
+                    getCurrentColor(mNavigationColorViewState), fullscreen, systemInsets,
+                    stableInsets);
 
             // Get rid of the shadow while we are resizing. Shadow drawing takes considerable time.
             // If we want to get the shadow shown while resizing, we would need to elevate a new
@@ -1971,10 +1996,6 @@
         return isShowingCaption() ? mDecorCaptionView.getCaptionHeight() : 0;
     }
 
-    int getStatusBarHeight() {
-        return mStatusColorViewState.view != null ? mStatusColorViewState.view.getHeight() : 0;
-    }
-
     /**
      * Converts a DIP measure into physical pixels.
      * @param dip The dip value.
diff --git a/core/java/com/android/internal/widget/FloatingToolbar.java b/core/java/com/android/internal/widget/FloatingToolbar.java
index 02efcc6..ce03bb8 100644
--- a/core/java/com/android/internal/widget/FloatingToolbar.java
+++ b/core/java/com/android/internal/widget/FloatingToolbar.java
@@ -322,7 +322,7 @@
         /* View components */
         private final ViewGroup mContentContainer;  // holds all contents.
         private final ViewGroup mMainPanel;  // holds menu items that are initially displayed.
-        private final ListView mOverflowPanel;  // holds menu items hidden in the overflow.
+        private final OverflowPanel mOverflowPanel;  // holds menu items hidden in the overflow.
         private final ImageButton mOverflowButton;  // opens/closes the overflow.
         /* overflow button drawables. */
         private final Drawable mArrow;
@@ -895,6 +895,7 @@
 
         private void setPanelsStatesAtRestingPosition() {
             mOverflowButton.setEnabled(true);
+            mOverflowPanel.awakenScrollBars();
 
             if (mIsOverflowOpen) {
                 // Set open state.
@@ -1333,27 +1334,8 @@
             return overflowButton;
         }
 
-        private ListView createOverflowPanel() {
-            final ListView overflowPanel = new ListView(FloatingToolbarPopup.this.mContext) {
-                @Override
-                protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-                    // Update heightMeasureSpec to make sure that this view is not clipped
-                    // as we offset it's coordinates with respect to it's parent.
-                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(
-                            mOverflowPanelSize.getHeight() - mOverflowButtonSize.getHeight(),
-                            MeasureSpec.EXACTLY);
-                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-                }
-
-                @Override
-                public boolean dispatchTouchEvent(MotionEvent ev) {
-                    if (isOverflowAnimating()) {
-                        // Eat the touch event.
-                        return true;
-                    }
-                    return super.dispatchTouchEvent(ev);
-                }
-            };
+        private OverflowPanel createOverflowPanel() {
+            final OverflowPanel overflowPanel = new OverflowPanel(this);
             overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
             overflowPanel.setDivider(null);
@@ -1464,6 +1446,43 @@
         }
 
         /**
+         * A custom ListView for the overflow panel.
+         */
+        private static final class OverflowPanel extends ListView {
+
+            private final FloatingToolbarPopup mPopup;
+
+            OverflowPanel(FloatingToolbarPopup popup) {
+                super(Preconditions.checkNotNull(popup).mContext);
+                this.mPopup = popup;
+            }
+
+            @Override
+            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+                // Update heightMeasureSpec to make sure that this view is not clipped
+                // as we offset it's coordinates with respect to it's parent.
+                int height = mPopup.mOverflowPanelSize.getHeight()
+                        - mPopup.mOverflowButtonSize.getHeight();
+                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            }
+
+            @Override
+            public boolean dispatchTouchEvent(MotionEvent ev) {
+                if (mPopup.isOverflowAnimating()) {
+                    // Eat the touch event.
+                    return true;
+                }
+                return super.dispatchTouchEvent(ev);
+            }
+
+            @Override
+            protected boolean awakenScrollBars() {
+                return super.awakenScrollBars();
+            }
+        }
+
+        /**
          * A custom interpolator used for various floating toolbar animations.
          */
         private static final class LogAccelerateInterpolator implements Interpolator {
diff --git a/core/jni/android/graphics/Graphics.cpp b/core/jni/android/graphics/Graphics.cpp
index 3d5091a..bd01c73 100644
--- a/core/jni/android/graphics/Graphics.cpp
+++ b/core/jni/android/graphics/Graphics.cpp
@@ -746,10 +746,12 @@
     if (mNeedsCopy) {
         SkPixelRef* recycledPixels = mRecycledBitmap->refPixelRef();
         void* dst = recycledPixels->pixels();
-        size_t dstRowBytes = mRecycledBitmap->rowBytes();
-        size_t bytesToCopy = SkTMin(mRecycledBitmap->info().minRowBytes(),
+        const size_t dstRowBytes = mRecycledBitmap->rowBytes();
+        const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(),
                 mSkiaBitmap->info().minRowBytes());
-        for (int y = 0; y < mRecycledBitmap->info().height(); y++) {
+        const int rowsToCopy = std::min(mRecycledBitmap->info().height(),
+                mSkiaBitmap->info().height());
+        for (int y = 0; y < rowsToCopy; y++) {
             memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy);
             dst = SkTAddOffset<void>(dst, dstRowBytes);
         }
diff --git a/core/jni/android_graphics_Canvas.cpp b/core/jni/android_graphics_Canvas.cpp
index 35b5016..cf73316 100644
--- a/core/jni/android_graphics_Canvas.cpp
+++ b/core/jni/android_graphics_Canvas.cpp
@@ -510,7 +510,7 @@
 
         size_t glyphCount = end - start;
 
-        if (CC_UNLIKELY(canvas->isHighContrastText())) {
+        if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) {
             // high contrast draw path
             int color = paint.getColor();
             int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
diff --git a/core/jni/android_graphics_drawable_VectorDrawable.cpp b/core/jni/android_graphics_drawable_VectorDrawable.cpp
index e882876..7314fbc 100644
--- a/core/jni/android_graphics_drawable_VectorDrawable.cpp
+++ b/core/jni/android_graphics_drawable_VectorDrawable.cpp
@@ -138,11 +138,6 @@
     return reinterpret_cast<jlong>(newGroup);
 }
 
-static void deleteNode(JNIEnv*, jobject, jlong nodePtr) {
-    VectorDrawable::Node* node = reinterpret_cast<VectorDrawable::Node*>(nodePtr);
-    delete node;
-}
-
 static void setNodeName(JNIEnv* env, jobject, jlong nodePtr, jstring nameStr) {
     VectorDrawable::Node* node = reinterpret_cast<VectorDrawable::Node*>(nodePtr);
     const char* nodeName = env->GetStringUTFChars(nameStr, NULL);
@@ -346,7 +341,6 @@
         {"nCreateClipPath", "!(J)J", (void*)createClipPath},
         {"nCreateGroup", "!()J", (void*)createEmptyGroup},
         {"nCreateGroup", "!(J)J", (void*)createGroup},
-        {"nDestroy", "!(J)V", (void*)deleteNode},
         {"nSetName", "(JLjava/lang/String;)V", (void*)setNodeName},
         {"nUpdateGroupProperties", "!(JFFFFFFF)V", (void*)updateGroupProperties},
 
diff --git a/core/jni/android_hardware_SensorManager.cpp b/core/jni/android_hardware_SensorManager.cpp
index 4842e1b..e39bb1c 100644
--- a/core/jni/android_hardware_SensorManager.cpp
+++ b/core/jni/android_hardware_SensorManager.cpp
@@ -205,8 +205,8 @@
     SensorManager* mgr = reinterpret_cast<SensorManager*>(sensorManager);
 
     Sensor const* const* sensorList;
-    size_t count = mgr->getSensorList(&sensorList);
-    if (size_t(index) >= count) {
+    ssize_t count = mgr->getSensorList(&sensorList);
+    if (ssize_t(index) >= count) {
         return false;
     }
 
diff --git a/core/jni/android_view_RenderNode.cpp b/core/jni/android_view_RenderNode.cpp
index b1d4e26..a9003c1 100644
--- a/core/jni/android_view_RenderNode.cpp
+++ b/core/jni/android_view_RenderNode.cpp
@@ -15,6 +15,7 @@
  */
 
 #define LOG_TAG "OpenGLRenderer"
+#define ATRACE_TAG ATRACE_TAG_VIEW
 
 #include <EGL/egl.h>
 
@@ -24,7 +25,10 @@
 #include <android_runtime/AndroidRuntime.h>
 
 #include <Animator.h>
+#include <DamageAccumulator.h>
+#include <Matrix.h>
 #include <RenderNode.h>
+#include <TreeInfo.h>
 #include <Paint.h>
 
 #include "core_jni_helpers.h"
@@ -462,6 +466,69 @@
 }
 
 // ----------------------------------------------------------------------------
+// SurfaceView position callback
+// ----------------------------------------------------------------------------
+
+jmethodID gSurfaceViewPositionUpdateMethod;
+
+static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
+        jlong renderNodePtr, jobject surfaceview) {
+    class SurfaceViewPositionUpdater : public RenderNode::PositionListener {
+    public:
+        SurfaceViewPositionUpdater(JNIEnv* env, jobject surfaceview) {
+            env->GetJavaVM(&mVm);
+            mWeakRef = env->NewWeakGlobalRef(surfaceview);
+        }
+
+        virtual ~SurfaceViewPositionUpdater() {
+            jnienv()->DeleteWeakGlobalRef(mWeakRef);
+            mWeakRef = nullptr;
+        }
+
+        virtual void onPositionUpdated(RenderNode& node, const TreeInfo& info) override {
+            if (CC_UNLIKELY(!mWeakRef || !info.updateWindowPositions)) return;
+            ATRACE_NAME("Update SurfaceView position");
+
+            JNIEnv* env = jnienv();
+            jobject localref = env->NewLocalRef(mWeakRef);
+            if (CC_UNLIKELY(!localref)) {
+                jnienv()->DeleteWeakGlobalRef(mWeakRef);
+                mWeakRef = nullptr;
+                return;
+            }
+            Matrix4 transform;
+            info.damageAccumulator->computeCurrentTransform(&transform);
+            const RenderProperties& props = node.properties();
+            uirenderer::Rect bounds(props.getWidth(), props.getHeight());
+            transform.mapRect(bounds);
+            bounds.left -= info.windowInsetLeft;
+            bounds.right -= info.windowInsetLeft;
+            bounds.top -= info.windowInsetTop;
+            bounds.bottom -= info.windowInsetTop;
+            env->CallVoidMethod(localref, gSurfaceViewPositionUpdateMethod,
+                    (jlong) info.frameNumber, (jint) bounds.left, (jint) bounds.top,
+                    (jint) bounds.right, (jint) bounds.bottom);
+            env->DeleteLocalRef(localref);
+        }
+
+    private:
+        JNIEnv* jnienv() {
+            JNIEnv* env;
+            if (mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+                LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", mVm);
+            }
+            return env;
+        }
+
+        JavaVM* mVm;
+        jobject mWeakRef;
+    };
+
+    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
+    renderNode->setPositionListener(new SurfaceViewPositionUpdater(env, surfaceview));
+}
+
+// ----------------------------------------------------------------------------
 // JNI Glue
 // ----------------------------------------------------------------------------
 
@@ -539,9 +606,14 @@
 
     { "nAddAnimator",              "(JJ)V", (void*) android_view_RenderNode_addAnimator },
     { "nEndAllAnimators",          "(J)V", (void*) android_view_RenderNode_endAllAnimators },
+
+    { "nRequestPositionUpdates",   "(JLandroid/view/SurfaceView;)V", (void*) android_view_RenderNode_requestPositionUpdates },
 };
 
 int register_android_view_RenderNode(JNIEnv* env) {
+    jclass clazz = FindClassOrDie(env, "android/view/SurfaceView");
+    gSurfaceViewPositionUpdateMethod = GetMethodIDOrDie(env, clazz,
+            "updateWindowPositionRT", "(JIIII)V");
     return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
 }
 
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index 8c907dd..acd0501 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -134,7 +134,14 @@
 
     virtual void prepareTree(TreeInfo& info) {
         info.errorHandler = this;
+        // TODO: This is hacky
+        info.windowInsetLeft = -stagingProperties().getLeft();
+        info.windowInsetTop = -stagingProperties().getTop();
+        info.updateWindowPositions = true;
         RenderNode::prepareTree(info);
+        info.updateWindowPositions = false;
+        info.windowInsetLeft = 0;
+        info.windowInsetTop = 0;
         info.errorHandler = NULL;
     }
 
@@ -369,28 +376,28 @@
 static void android_view_ThreadedRenderer_initialize(JNIEnv* env, jobject clazz,
         jlong proxyPtr, jobject jsurface) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    sp<ANativeWindow> window = android_view_Surface_getNativeWindow(env, jsurface);
-    proxy->initialize(window);
+    sp<Surface> surface = android_view_Surface_getSurface(env, jsurface);
+    proxy->initialize(surface);
 }
 
 static void android_view_ThreadedRenderer_updateSurface(JNIEnv* env, jobject clazz,
         jlong proxyPtr, jobject jsurface) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    sp<ANativeWindow> window;
+    sp<Surface> surface;
     if (jsurface) {
-        window = android_view_Surface_getNativeWindow(env, jsurface);
+        surface = android_view_Surface_getSurface(env, jsurface);
     }
-    proxy->updateSurface(window);
+    proxy->updateSurface(surface);
 }
 
 static jboolean android_view_ThreadedRenderer_pauseSurface(JNIEnv* env, jobject clazz,
         jlong proxyPtr, jobject jsurface) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    sp<ANativeWindow> window;
+    sp<Surface> surface;
     if (jsurface) {
-        window = android_view_Surface_getNativeWindow(env, jsurface);
+        surface = android_view_Surface_getSurface(env, jsurface);
     }
-    return proxy->pauseSurface(window);
+    return proxy->pauseSurface(surface);
 }
 
 static void android_view_ThreadedRenderer_setup(JNIEnv* env, jobject clazz, jlong proxyPtr,
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index cc48902..4cddb6c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2739,6 +2739,7 @@
          android.service.notification.NotificationAssistantService},
          to ensure that only the system can bind to it.
          <p>Protection level: signature
+         @hide This is not a third-party API (intended for system apps). -->
     -->
     <permission android:name="android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE"
         android:protectionLevel="signature" />
diff --git a/core/res/res/drawable/ic_close.xml b/core/res/res/drawable/ic_close.xml
new file mode 100644
index 0000000..7086959
--- /dev/null
+++ b/core/res/res/drawable/ic_close.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2016 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:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+        android:fillColor="#FF000000"/>
+</vector>
diff --git a/core/res/res/drawable/ic_feedback.xml b/core/res/res/drawable/ic_feedback.xml
new file mode 100644
index 0000000..b2d1cb8
--- /dev/null
+++ b/core/res/res/drawable/ic_feedback.xml
@@ -0,0 +1,27 @@
+<!--
+Copyright (C) 2016 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:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:pathData="M0 0h24v24H0z"
+        android:fillColor="#00000000"/>
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M20.0,2.0L4.0,2.0c-1.1,0.0 -1.9,0.9 -1.99,2.0L2.0,22.0l4.0,-4.0l14.0,0.0c1.1,0.0 2.0,-0.9 2.0,-2.0L22.0,4.0c0.0,-1.1 -0.9,-2.0 -2.0,-2.0zm-7.0,12.0l-2.0,0.0l0.0,-2.0l2.0,0.0l0.0,2.0zm0.0,-4.0l-2.0,0.0L11.0,6.0l2.0,0.0l0.0,4.0z"/>
+</vector>
diff --git a/core/res/res/drawable/ic_refresh.xml b/core/res/res/drawable/ic_refresh.xml
new file mode 100644
index 0000000..1f67168
--- /dev/null
+++ b/core/res/res/drawable/ic_refresh.xml
@@ -0,0 +1,27 @@
+<!--
+Copyright (C) 2016 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:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M17.65,6.35C16.2,4.9 14.21,4.0 12.0,4.0c-4.42,0.0 -7.99,3.58 -7.99,8.0s3.57,8.0 7.99,8.0c3.73,0.0 6.84,-2.55 7.73,-6.0l-2.08,0.0c-0.82,2.33 -3.04,4.0 -5.65,4.0 -3.31,0.0 -6.0,-2.69 -6.0,-6.0s2.69,-6.0 6.0,-6.0c1.66,0.0 3.1,0.69 4.22,1.78L13.0,11.0l7.0,0.0L20.0,4.0l-2.35,2.35z"/>
+    <path
+        android:pathData="M0 0h24v24H0z"
+        android:fillColor="#00000000"/>
+</vector>
diff --git a/core/res/res/drawable/ic_schedule.xml b/core/res/res/drawable/ic_schedule.xml
new file mode 100644
index 0000000..899dc82
--- /dev/null
+++ b/core/res/res/drawable/ic_schedule.xml
@@ -0,0 +1,30 @@
+<!--
+Copyright (C) 2016 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:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M11.99,2.0C6.47,2.0 2.0,6.48 2.0,12.0s4.47,10.0 9.99,10.0C17.52,22.0 22.0,17.52 22.0,12.0S17.52,2.0 11.99,2.0zM12.0,20.0c-4.42,0.0 -8.0,-3.58 -8.0,-8.0s3.58,-8.0 8.0,-8.0 8.0,3.58 8.0,8.0 -3.58,8.0 -8.0,8.0z"/>
+    <path
+        android:pathData="M0 0h24v24H0z"
+        android:fillColor="#00000000"/>
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12.5,7.0L11.0,7.0l0.0,6.0l5.25,3.1 0.75,-1.23 -4.5,-2.67z"/>
+</vector>
diff --git a/core/res/res/layout/app_anr_dialog.xml b/core/res/res/layout/app_anr_dialog.xml
index e8169ee..8bef116 100644
--- a/core/res/res/layout/app_anr_dialog.xml
+++ b/core/res/res/layout/app_anr_dialog.xml
@@ -18,28 +18,31 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical"
-        android:paddingTop="@dimen/dialog_list_padding_vertical_material"
+        android:paddingTop="@dimen/aerr_padding_list_top"
         android:paddingBottom="@dimen/dialog_list_padding_vertical_material">
 
-    <TextView
+    <Button
             android:id="@+id/aerr_close"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_close_app"
+            android:drawableStart="@drawable/ic_close"
             style="@style/aerr_list_item" />
 
-    <TextView
+    <Button
             android:id="@+id/aerr_wait"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_wait"
+            android:drawableStart="@drawable/ic_schedule"
             style="@style/aerr_list_item" />
 
-    <TextView
+    <Button
             android:id="@+id/aerr_report"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_report"
+            android:drawableStart="@drawable/ic_feedback"
             style="@style/aerr_list_item" />
 
 </LinearLayout>
diff --git a/core/res/res/layout/app_error_dialog.xml b/core/res/res/layout/app_error_dialog.xml
index 46a2b2a..824f97f 100644
--- a/core/res/res/layout/app_error_dialog.xml
+++ b/core/res/res/layout/app_error_dialog.xml
@@ -21,9 +21,8 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical"
-        android:paddingTop="@dimen/dialog_list_padding_vertical_material"
-        android:paddingBottom="@dimen/dialog_list_padding_vertical_material"
->
+        android:paddingTop="@dimen/aerr_padding_list_top"
+        android:paddingBottom="@dimen/dialog_list_padding_vertical_material">
 
 
     <Button
@@ -31,6 +30,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_restart"
+            android:drawableStart="@drawable/ic_refresh"
             style="@style/aerr_list_item"
     />
 
@@ -39,6 +39,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_reset"
+            android:drawableStart="@drawable/ic_refresh"
             style="@style/aerr_list_item"
     />
 
@@ -47,6 +48,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_report"
+            android:drawableStart="@drawable/ic_feedback"
             style="@style/aerr_list_item"
     />
 
@@ -55,6 +57,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_close"
+            android:drawableStart="@drawable/ic_close"
             style="@style/aerr_list_item"
     />
 
@@ -63,6 +66,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/aerr_mute"
+            android:drawableStart="@drawable/ic_close"
             style="@style/aerr_list_item"
     />
 
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index b45f8bb..6669bae 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -87,7 +87,6 @@
         android:layout_marginStart="2dp"
         android:layout_marginEnd="2dp"
         android:visibility="gone"
-        android:maxWidth="72dp"
         android:singleLine="true"/>
     <TextView
         android:id="@+id/time_divider"
diff --git a/core/res/res/layout/resolve_grid_item.xml b/core/res/res/layout/resolve_grid_item.xml
index 0a7ac77..305c8b0 100644
--- a/core/res/res/layout/resolve_grid_item.xml
+++ b/core/res/res/layout/resolve_grid_item.xml
@@ -25,6 +25,7 @@
               android:gravity="center"
               android:paddingTop="8dp"
               android:paddingBottom="8dp"
+              android:focusable="true"
               android:background="?attr/selectableItemBackgroundBorderless">
 
     <FrameLayout android:layout_width="wrap_content"
diff --git a/core/res/res/values/colors_material.xml b/core/res/res/values/colors_material.xml
index dd18544..7399fa9 100644
--- a/core/res/res/values/colors_material.xml
+++ b/core/res/res/values/colors_material.xml
@@ -48,8 +48,6 @@
     <color name="primary_text_default_material_light">#de000000</color>
     <!-- 54% black -->
     <color name="secondary_text_default_material_light">#8a000000</color>
-    <!-- 38% black -->
-    <color name="tertiary_text_default_material_light">#61000000</color>
 
     <!-- 100% white -->
     <color name="primary_text_default_material_dark">#ffffffff</color>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 7c6f338..8e86f78 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -449,5 +449,7 @@
     <item type="dimen" format="integer" name="time_picker_column_start_material">0</item>
     <item type="dimen" format="integer" name="time_picker_column_end_material">1</item>
 
+    <item type="dimen" name="aerr_padding_list_top">15dp</item>
+
     <item type="fraction" name="docked_stack_divider_fixed_ratio">34.15%</item>
 </resources>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index f0960c7..86b9f1d 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1403,8 +1403,12 @@
         <item name="textAppearance">?attr/textAppearanceListItemSmall</item>
         <item name="textColor">?attr/textColorAlertDialogListItem</item>
         <item name="gravity">center_vertical</item>
-        <item name="paddingStart">?attr/listPreferredItemPaddingStart</item>
-        <item name="paddingEnd">?attr/listPreferredItemPaddingEnd</item>
+        <item name="paddingStart">?attr/dialogPreferredPadding</item>
+        <item name="paddingEnd">?attr/dialogPreferredPadding</item>
+        <item name="background">?attr/selectableItemBackground</item>
+        <item name="drawablePadding">32dp</item>
+        <item name="drawableTint">@color/accent_material_light</item>
+        <item name="drawableTintMode">src_atop</item>
     </style>
 
     <!-- Wifi dialog styles -->
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index 0a52f41..9efcfda 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -444,7 +444,7 @@
     </style>
 
     <style name="TextAppearance.Material.Notification.Info">
-        <item name="textColor">@color/tertiary_text_default_material_light</item>
+        <item name="textColor">@color/secondary_text_default_material_light</item>
         <item name="textSize">@dimen/notification_subtext_size</item>
     </style>
 
diff --git a/core/tests/coretests/src/android/print/BasePrintTest.java b/core/tests/coretests/src/android/print/BasePrintTest.java
index 19ce44a..3feb0e9 100644
--- a/core/tests/coretests/src/android/print/BasePrintTest.java
+++ b/core/tests/coretests/src/android/print/BasePrintTest.java
@@ -141,7 +141,7 @@
         // Set to US locale.
         Resources resources = getInstrumentation().getTargetContext().getResources();
         Configuration oldConfiguration = resources.getConfiguration();
-        if (!oldConfiguration.getLocales().getPrimary().equals(Locale.US)) {
+        if (!oldConfiguration.getLocales().get(0).equals(Locale.US)) {
             mOldLocale = oldConfiguration.getLocales();
             DisplayMetrics displayMetrics = resources.getDisplayMetrics();
             Configuration newConfiguration = new Configuration(oldConfiguration);
diff --git a/data/fonts/Android.mk b/data/fonts/Android.mk
index d2bd106..de741b3 100644
--- a/data/fonts/Android.mk
+++ b/data/fonts/Android.mk
@@ -89,10 +89,7 @@
 endef
 
 font_src_files := \
-    Clockopia.ttf \
-    AndroidClock.ttf \
-    AndroidClock_Highlight.ttf \
-    AndroidClock_Solid.ttf
+    AndroidClock.ttf
 
 $(foreach f, $(font_src_files), $(call build-one-font-module, $(f)))
 
diff --git a/data/fonts/AndroidClock_Highlight.ttf b/data/fonts/AndroidClock_Highlight.ttf
deleted file mode 100644
index 923bb30..0000000
--- a/data/fonts/AndroidClock_Highlight.ttf
+++ /dev/null
Binary files differ
diff --git a/data/fonts/AndroidClock_Solid.ttf b/data/fonts/AndroidClock_Solid.ttf
deleted file mode 100644
index 923bb30..0000000
--- a/data/fonts/AndroidClock_Solid.ttf
+++ /dev/null
Binary files differ
diff --git a/data/fonts/Clockopia.ttf b/data/fonts/Clockopia.ttf
deleted file mode 100644
index 3f7b6aa..0000000
--- a/data/fonts/Clockopia.ttf
+++ /dev/null
Binary files differ
diff --git a/data/fonts/MTLc3m.ttf b/data/fonts/MTLc3m.ttf
deleted file mode 100644
index e9018f6..0000000
--- a/data/fonts/MTLc3m.ttf
+++ /dev/null
Binary files differ
diff --git a/data/fonts/MTLmr3m.ttf b/data/fonts/MTLmr3m.ttf
deleted file mode 100644
index 14f27d4..0000000
--- a/data/fonts/MTLmr3m.ttf
+++ /dev/null
Binary files differ
diff --git a/data/fonts/fonts.mk b/data/fonts/fonts.mk
index 597a122..acd785e 100644
--- a/data/fonts/fonts.mk
+++ b/data/fonts/fonts.mk
@@ -20,7 +20,4 @@
 PRODUCT_PACKAGES := \
     DroidSansFallback.ttf \
     DroidSansMono.ttf \
-    Clockopia.ttf \
     AndroidClock.ttf \
-    AndroidClock_Highlight.ttf \
-    AndroidClock_Solid.ttf \
diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml
index 961d0eb..dc302c7 100644
--- a/data/fonts/fonts.xml
+++ b/data/fonts/fonts.xml
@@ -344,6 +344,9 @@
     <family lang="und-Zsye">
         <font weight="400" style="normal">NotoColorEmoji.ttf</font>
     </family>
+    <family>
+        <font weight="400" style="normal">DroidSansFallback.ttf</font>
+    </family>
     <!--
         Tai Le and Mongolian are intentionally kept last, to make sure they don't override
         the East Asian punctuation for Chinese.
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index dfb8bb8..534121a 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -459,7 +459,7 @@
         // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
         //        ? HINTING_OFF : HINTING_ON);
         mCompatScaling = mInvCompatScaling = 1;
-        setTextLocales(LocaleList.getDefault());
+        setTextLocales(LocaleList.getAdjustedDefault());
     }
 
     /**
@@ -500,7 +500,7 @@
         mInvCompatScaling = 1;
 
         mBidiFlags = BIDI_DEFAULT_LTR;
-        setTextLocales(LocaleList.getDefault());
+        setTextLocales(LocaleList.getAdjustedDefault());
         setElegantTextHeight(false);
         mFontFeatureSettings = null;
     }
@@ -1292,7 +1292,7 @@
      */
     @NonNull
     public Locale getTextLocale() {
-        return mLocales.getPrimary();
+        return mLocales.get(0);
     }
 
     /**
@@ -1317,7 +1317,7 @@
         if (locale == null) {
             throw new IllegalArgumentException("locale cannot be null");
         }
-        if (mLocales != null && mLocales.size() == 1 && locale.equals(mLocales.getPrimary())) {
+        if (mLocales != null && mLocales.size() == 1 && locale.equals(mLocales.get(0))) {
             return;
         }
         mLocales = new LocaleList(locale);
@@ -1340,8 +1340,8 @@
      * each language.
      *
      * By default, the text locale list is initialized to a one-member list just containing the
-     * system locale (as returned by {@link LocaleList#getDefault()}). This assumes that the text to
-     * be rendered will most likely be in the user's preferred language.
+     * system locales. This assumes that the text to be rendered will most likely be in the user's
+     * preferred language.
      *
      * If the actual language or languages of the text is/are known, then they can be provided to
      * the text renderer using this method. The text renderer may attempt to guess the
diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java
index 40dbe27..0cde0b9 100644
--- a/graphics/java/android/graphics/Rect.java
+++ b/graphics/java/android/graphics/Rect.java
@@ -335,6 +335,21 @@
     }
 
     /**
+     * Insets the rectangle on all sides specified by the insets.
+     * @hide
+     * @param left The amount to add from the rectangle's left
+     * @param top The amount to add from the rectangle's top
+     * @param right The amount to subtract from the rectangle's right
+     * @param bottom The amount to subtract from the rectangle's bottom
+     */
+    public void inset(int left, int top, int right, int bottom) {
+        this.left += left;
+        this.top += top;
+        this.right -= right;
+        this.bottom -= bottom;
+    }
+
+    /**
      * Returns true if (x,y) is inside the rectangle. The left and top are
      * considered to be inside, while the right and bottom are not. This means
      * that for a x,y to be contained: left <= x < right and top <= y < bottom.
diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
index 21ed7dd..af8ccf5 100644
--- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -25,6 +25,8 @@
 import android.animation.ObjectAnimator;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.app.Application;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.content.res.Resources.Theme;
@@ -35,6 +37,7 @@
 import android.graphics.Outline;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
+import android.os.Build;
 import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -200,6 +203,24 @@
         mMutated = false;
     }
 
+    /**
+     * In order to avoid breaking old apps, we only throw exception on invalid VectorDrawable
+     * animations * for apps targeting N and later. For older apps, we ignore (i.e. quietly skip)
+     * these animations.
+     *
+     * @return whether invalid animations for vector drawable should be ignored.
+     */
+    private static boolean shouldIgnoreInvalidAnimation() {
+        Application app = ActivityThread.currentApplication();
+        if (app == null || app.getApplicationInfo() == null) {
+            return true;
+        }
+        if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public ConstantState getConstantState() {
         mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
@@ -763,6 +784,8 @@
         private boolean mInitialized = false;
         private boolean mAnimationPending = false;
         private boolean mIsReversible = false;
+        // This needs to be set before parsing starts.
+        private boolean mShouldIgnoreInvalidAnim;
         // TODO: Consider using NativeAllocationRegistery to track native allocation
         private final VirtualRefBasePtr mSetRefBasePtr;
         private WeakReference<RenderNode> mTarget = null;
@@ -782,6 +805,7 @@
                 throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " +
                         "re-initialized");
             }
+            mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation();
             parseAnimatorSet(set, 0);
             mInitialized = true;
 
@@ -841,7 +865,7 @@
                     }  else if (target instanceof VectorDrawable.VFullPath) {
                         createRTAnimatorForFullPath(animator, (VectorDrawable.VFullPath) target,
                                 startTime);
-                    } else {
+                    } else if (!mShouldIgnoreInvalidAnim) {
                         throw new IllegalArgumentException("ClipPath only supports PathData " +
                                 "property");
                     }
@@ -850,10 +874,11 @@
             } else if (target instanceof VectorDrawable.VectorDrawableState) {
                 createRTAnimatorForRootGroup(values, animator,
                         (VectorDrawable.VectorDrawableState) target, startTime);
-            } else {
+            } else if (!mShouldIgnoreInvalidAnim) {
                 // Should never get here
-                throw new UnsupportedOperationException("Target should be group, path or vector. " +
-                        target == null ? "Null target" : target.getClass() + " is not supported");
+                throw new UnsupportedOperationException("Target should be either VGroup, VPath, " +
+                        "or ConstantState, " + target == null ? "Null target" : target.getClass() +
+                        " is not supported");
             }
         }
 
@@ -912,8 +937,12 @@
             long nativePtr = target.getNativePtr();
             if (mTmpValues.type == Float.class || mTmpValues.type == float.class) {
                 if (propertyId < 0) {
-                    throw new IllegalArgumentException("Property: " + mTmpValues
-                            .propertyName + " is not supported for FullPath");
+                    if (mShouldIgnoreInvalidAnim) {
+                        return;
+                    } else {
+                        throw new IllegalArgumentException("Property: " + mTmpValues.propertyName
+                                + " is not supported for FullPath");
+                    }
                 }
                 propertyPtr = nCreatePathPropertyHolder(nativePtr, propertyId,
                         (Float) mTmpValues.startValue, (Float) mTmpValues.endValue);
@@ -922,9 +951,13 @@
                 propertyPtr = nCreatePathColorPropertyHolder(nativePtr, propertyId,
                         (Integer) mTmpValues.startValue, (Integer) mTmpValues.endValue);
             } else {
-                throw new UnsupportedOperationException("Unsupported type: " +
-                        mTmpValues.type + ". Only float, int or PathData value is " +
-                        "supported for Paths.");
+                if (mShouldIgnoreInvalidAnim) {
+                    return;
+                } else {
+                    throw new UnsupportedOperationException("Unsupported type: " +
+                            mTmpValues.type + ". Only float, int or PathData value is " +
+                            "supported for Paths.");
+                }
             }
             if (mTmpValues.dataSource != null) {
                 float[] dataPoints = createDataPoints(mTmpValues.dataSource, animator
@@ -939,8 +972,12 @@
                 long startTime) {
                 long nativePtr = target.getNativeRenderer();
                 if (!animator.getPropertyName().equals("alpha")) {
-                    throw new UnsupportedOperationException("Only alpha is supported for root " +
-                            "group");
+                    if (mShouldIgnoreInvalidAnim) {
+                        return;
+                    } else {
+                        throw new UnsupportedOperationException("Only alpha is supported for root "
+                                + "group");
+                    }
                 }
                 Float startValue = null;
                 Float endValue = null;
@@ -953,7 +990,11 @@
                     }
                 }
                 if (startValue == null && endValue == null) {
-                    throw new UnsupportedOperationException("No alpha values are specified");
+                    if (mShouldIgnoreInvalidAnim) {
+                        return;
+                    } else {
+                        throw new UnsupportedOperationException("No alpha values are specified");
+                    }
                 }
                 long propertyPtr = nCreateRootAlphaPropertyHolder(nativePtr, startValue, endValue);
                 createNativeChildAnimator(propertyPtr, startTime, animator);
diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java
index f4bbc8c..bdbf3c0 100644
--- a/graphics/java/android/graphics/drawable/VectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/VectorDrawable.java
@@ -924,8 +924,11 @@
         private int mChangingConfigurations;
         private int[] mThemeAttrs;
         private String mGroupName = null;
-        private long mNativePtr = 0;
 
+        // The native object will be created in the constructor and will be destroyed in native
+        // when the neither java nor native has ref to the tree. This pointer should be valid
+        // throughout this VGroup Java object's life.
+        private final long mNativePtr;
         public VGroup(VGroup copy, ArrayMap<String, Object> targetsMap) {
 
             mIsStateful = copy.mIsStateful;
@@ -1065,16 +1068,6 @@
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            if (mNativePtr != 0) {
-                nDestroy(mNativePtr);
-                mNativePtr = 0;
-            }
-            super.finalize();
-        }
-
-
-        @Override
         public void applyTheme(Theme t) {
             if (mThemeAttrs != null) {
                 final TypedArray a = t.resolveAttributes(mThemeAttrs,
@@ -1208,10 +1201,10 @@
      * Clip path, which only has name and pathData.
      */
     private static class VClipPath extends VPath {
-        long mNativePtr = 0;
+        private final long mNativePtr;
+
         public VClipPath() {
             mNativePtr = nCreateClipPath();
-            // Empty constructor.
         }
 
         public VClipPath(VClipPath copy) {
@@ -1225,14 +1218,6 @@
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            if (mNativePtr != 0) {
-                nDestroy(mNativePtr);
-                mNativePtr = 0;
-            }
-            super.finalize();
-        }
-        @Override
         public void inflate(Resources r, AttributeSet attrs, Theme theme) {
             final TypedArray a = obtainAttributes(r, theme, attrs,
                     R.styleable.VectorDrawableClipPath);
@@ -1317,10 +1302,9 @@
 
         ComplexColor mStrokeColors = null;
         ComplexColor mFillColors = null;
-        private long mNativePtr = 0;
+        private final long mNativePtr;
 
         public VFullPath() {
-            // Empty constructor.
             mNativePtr = nCreateFullPath();
         }
 
@@ -1384,15 +1368,6 @@
             a.recycle();
         }
 
-        @Override
-        protected void finalize() throws Throwable {
-            if (mNativePtr != 0) {
-                nDestroy(mNativePtr);
-                mNativePtr = 0;
-            }
-            super.finalize();
-        }
-
         private void updateStateFromTypedArray(TypedArray a) {
             int byteCount = TOTAL_PROPERTY_COUNT * 4;
             if (mPropertyData == null) {
@@ -1647,7 +1622,7 @@
     private static native void nDraw(long rendererPtr, long canvasWrapperPtr,
             long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache);
     private static native long nCreateFullPath();
-    private static native long nCreateFullPath(long mNativeFullPathPtr);
+    private static native long nCreateFullPath(long nativeFullPathPtr);
     private static native boolean nGetFullPathProperties(long pathPtr, byte[] properties,
             int length);
 
@@ -1663,7 +1638,6 @@
 
     private static native long nCreateGroup();
     private static native long nCreateGroup(long groupPtr);
-    private static native void nDestroy(long nodePtr);
     private static native void nSetName(long nodePtr, String name);
     private static native boolean nGetGroupProperties(long groupPtr, float[] properties,
             int length);
diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java
index 7cf4b04..d726880 100644
--- a/keystore/java/android/security/keystore/KeyInfo.java
+++ b/keystore/java/android/security/keystore/KeyInfo.java
@@ -269,7 +269,7 @@
 
     /**
      * Returns {@code true} if the requirement that this key can only be used if the user has been
-     * authenticated if enforced by secure hardware (e.g., Trusted Execution Environment (TEE) or
+     * authenticated is enforced by secure hardware (e.g., Trusted Execution Environment (TEE) or
      * Secure Element (SE)).
      *
      * @see #isUserAuthenticationRequired()
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 6988b02..7b43947 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -238,6 +238,7 @@
 
 LOCAL_SRC_FILES += \
     $(hwui_test_common_src_files) \
+    tests/unit/BufferPoolTests.cpp \
     tests/unit/CanvasStateTests.cpp \
     tests/unit/ClipAreaTests.cpp \
     tests/unit/CrashHandlerInjector.cpp \
@@ -247,11 +248,12 @@
     tests/unit/GpuMemoryTrackerTests.cpp \
     tests/unit/LayerUpdateQueueTests.cpp \
     tests/unit/LinearAllocatorTests.cpp \
-    tests/unit/VectorDrawableTests.cpp \
     tests/unit/OffscreenBufferPoolTests.cpp \
+    tests/unit/SkiaBehaviorTests.cpp \
     tests/unit/StringUtilsTests.cpp \
-    tests/unit/BufferPoolTests.cpp \
-    tests/unit/TextDropShadowCacheTests.cpp
+    tests/unit/TextDropShadowCacheTests.cpp \
+    tests/unit/VectorDrawableTests.cpp \
+    tests/unit/GradientCacheTests.cpp
 
 ifeq (true, $(HWUI_NEW_OPS))
     LOCAL_SRC_FILES += \
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
index 2184755..e3a5f3e 100644
--- a/libs/hwui/BakedOpDispatcher.cpp
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -663,7 +663,7 @@
 }
 
 void BakedOpDispatcher::onShadowOp(BakedOpRenderer& renderer, const ShadowOp& op, const BakedOpState& state) {
-    TessellationCache::vertexBuffer_pair_t buffers = *(op.shadowTask->getResult());
+    TessellationCache::vertexBuffer_pair_t buffers = op.shadowTask->getResult();
     renderShadow(renderer, state, op.casterAlpha, buffers.first, buffers.second);
 }
 
diff --git a/libs/hwui/ClipArea.cpp b/libs/hwui/ClipArea.cpp
index 160090d..9c08b4d 100644
--- a/libs/hwui/ClipArea.cpp
+++ b/libs/hwui/ClipArea.cpp
@@ -213,6 +213,7 @@
 
 void ClipArea::clipRectWithTransform(const Rect& r, const mat4* transform,
         SkRegion::Op op) {
+    if (!mPostViewportClipObserved && op == SkRegion::kIntersect_Op) op = SkRegion::kReplace_Op;
     onClipUpdated();
     switch (mMode) {
     case ClipMode::Rectangle:
@@ -228,6 +229,7 @@
 }
 
 void ClipArea::clipRegion(const SkRegion& region, SkRegion::Op op) {
+    if (!mPostViewportClipObserved && op == SkRegion::kIntersect_Op) op = SkRegion::kReplace_Op;
     onClipUpdated();
     enterRegionMode();
     mClipRegion.op(region, op);
@@ -236,6 +238,7 @@
 
 void ClipArea::clipPathWithTransform(const SkPath& path, const mat4* transform,
         SkRegion::Op op) {
+    if (!mPostViewportClipObserved && op == SkRegion::kIntersect_Op) op = SkRegion::kReplace_Op;
     onClipUpdated();
     SkMatrix skTransform;
     transform->copyTo(skTransform);
diff --git a/libs/hwui/DamageAccumulator.h b/libs/hwui/DamageAccumulator.h
index e44fc20..250296e 100644
--- a/libs/hwui/DamageAccumulator.h
+++ b/libs/hwui/DamageAccumulator.h
@@ -57,7 +57,7 @@
     // Returns the current dirty area, *NOT* transformed by pushed transforms
     void peekAtDirty(SkRect* dest) const;
 
-    void computeCurrentTransform(Matrix4* outMatrix) const;
+    ANDROID_API void computeCurrentTransform(Matrix4* outMatrix) const;
 
     void finish(SkRect* totalDirty);
 
diff --git a/libs/hwui/FboCache.cpp b/libs/hwui/FboCache.cpp
index cca3cb7..b2181b6 100644
--- a/libs/hwui/FboCache.cpp
+++ b/libs/hwui/FboCache.cpp
@@ -27,15 +27,8 @@
 // Constructors/destructor
 ///////////////////////////////////////////////////////////////////////////////
 
-FboCache::FboCache(): mMaxSize(DEFAULT_FBO_CACHE_SIZE) {
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_FBO_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting fbo cache size to %s", property);
-        mMaxSize = atoi(property);
-    } else {
-        INIT_LOGD("  Using default fbo cache size of %d", DEFAULT_FBO_CACHE_SIZE);
-    }
-}
+FboCache::FboCache()
+        : mMaxSize(Properties::fboCacheSize) {}
 
 FboCache::~FboCache() {
     clear();
diff --git a/libs/hwui/FrameBuilder.cpp b/libs/hwui/FrameBuilder.cpp
index 57e5b9d..185acce 100644
--- a/libs/hwui/FrameBuilder.cpp
+++ b/libs/hwui/FrameBuilder.cpp
@@ -203,7 +203,9 @@
         mCanvasState.setClippingOutline(mAllocator, &(properties.getOutline()));
     }
 
-    if (!mCanvasState.quickRejectConservative(0, 0, width, height)) {
+    bool quickRejected = properties.getClipToBounds()
+            && mCanvasState.quickRejectConservative(0, 0, width, height);
+    if (!quickRejected) {
         // not rejected, so defer render as either Layer, or direct (possibly wrapped in saveLayer)
         if (node.getLayer()) {
             // HW layer
diff --git a/libs/hwui/GradientCache.cpp b/libs/hwui/GradientCache.cpp
index e899ac7..11293d6 100644
--- a/libs/hwui/GradientCache.cpp
+++ b/libs/hwui/GradientCache.cpp
@@ -65,17 +65,9 @@
 GradientCache::GradientCache(Extensions& extensions)
         : mCache(LruCache<GradientCacheEntry, Texture*>::kUnlimitedCapacity)
         , mSize(0)
-        , mMaxSize(MB(DEFAULT_GRADIENT_CACHE_SIZE))
+        , mMaxSize(Properties::gradientCacheSize)
         , mUseFloatTexture(extensions.hasFloatTextures())
         , mHasNpot(extensions.hasNPot()){
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_GRADIENT_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting gradient cache size to %sMB", property);
-        setMaxSize(MB(atof(property)));
-    } else {
-        INIT_LOGD("  Using default gradient cache size of %.2fMB", DEFAULT_GRADIENT_CACHE_SIZE);
-    }
-
     glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize);
 
     mCache.setOnEntryRemovedListener(this);
@@ -97,13 +89,6 @@
     return mMaxSize;
 }
 
-void GradientCache::setMaxSize(uint32_t maxSize) {
-    mMaxSize = maxSize;
-    while (mSize > mMaxSize) {
-        mCache.removeOldest();
-    }
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Callbacks
 ///////////////////////////////////////////////////////////////////////////////
@@ -168,10 +153,13 @@
     texture->blend = info.hasAlpha;
     texture->generation = 1;
 
-    // Asume the cache is always big enough
+    // Assume the cache is always big enough
     const uint32_t size = info.width * 2 * bytesPerPixel();
     while (getSize() + size > mMaxSize) {
-        mCache.removeOldest();
+        LOG_ALWAYS_FATAL_IF(!mCache.removeOldest(),
+                "Ran out of things to remove from the cache? getSize() = %" PRIu32
+                ", size = %" PRIu32 ", mMaxSize = %" PRIu32 ", width = %" PRIu32,
+                getSize(), size, mMaxSize, info.width);
     }
 
     generateTexture(colors, positions, info.width, 2, texture);
diff --git a/libs/hwui/GradientCache.h b/libs/hwui/GradientCache.h
index b762ca7..dccd450 100644
--- a/libs/hwui/GradientCache.h
+++ b/libs/hwui/GradientCache.h
@@ -123,10 +123,6 @@
     void clear();
 
     /**
-     * Sets the maximum size of the cache in bytes.
-     */
-    void setMaxSize(uint32_t maxSize);
-    /**
      * Returns the maximum size of the cache in bytes.
      */
     uint32_t getMaxSize();
@@ -177,7 +173,7 @@
     LruCache<GradientCacheEntry, Texture*> mCache;
 
     uint32_t mSize;
-    uint32_t mMaxSize;
+    const uint32_t mMaxSize;
 
     GLint mMaxTextureSize;
     bool mUseFloatTexture;
diff --git a/libs/hwui/PatchCache.cpp b/libs/hwui/PatchCache.cpp
index 9881280..bd6feb9 100644
--- a/libs/hwui/PatchCache.cpp
+++ b/libs/hwui/PatchCache.cpp
@@ -32,20 +32,12 @@
 
 PatchCache::PatchCache(RenderState& renderState)
         : mRenderState(renderState)
+        , mMaxSize(Properties::patchCacheSize)
         , mSize(0)
         , mCache(LruCache<PatchDescription, Patch*>::kUnlimitedCapacity)
         , mMeshBuffer(0)
         , mFreeBlocks(nullptr)
-        , mGenerationId(0) {
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_PATCH_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting patch cache size to %skB", property);
-        mMaxSize = KB(atoi(property));
-    } else {
-        INIT_LOGD("  Using default patch cache size of %.2fkB", DEFAULT_PATCH_CACHE_SIZE);
-        mMaxSize = KB(DEFAULT_PATCH_CACHE_SIZE);
-    }
-}
+        , mGenerationId(0) {}
 
 PatchCache::~PatchCache() {
     clear();
diff --git a/libs/hwui/PatchCache.h b/libs/hwui/PatchCache.h
index 387f79a..66ef6a0 100644
--- a/libs/hwui/PatchCache.h
+++ b/libs/hwui/PatchCache.h
@@ -169,7 +169,7 @@
 #endif
 
     RenderState& mRenderState;
-    uint32_t mMaxSize;
+    const uint32_t mMaxSize;
     uint32_t mSize;
 
     LruCache<PatchDescription, Patch*> mCache;
diff --git a/libs/hwui/PathCache.cpp b/libs/hwui/PathCache.cpp
index bfabc1d..8f914ac 100644
--- a/libs/hwui/PathCache.cpp
+++ b/libs/hwui/PathCache.cpp
@@ -135,17 +135,10 @@
 // Cache constructor/destructor
 ///////////////////////////////////////////////////////////////////////////////
 
-PathCache::PathCache():
-        mCache(LruCache<PathDescription, PathTexture*>::kUnlimitedCapacity),
-        mSize(0), mMaxSize(MB(DEFAULT_PATH_CACHE_SIZE)) {
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_PATH_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting path cache size to %sMB", property);
-        mMaxSize = MB(atof(property));
-    } else {
-        INIT_LOGD("  Using default path cache size of %.2fMB", DEFAULT_PATH_CACHE_SIZE);
-    }
-
+PathCache::PathCache()
+        : mCache(LruCache<PathDescription, PathTexture*>::kUnlimitedCapacity)
+        , mSize(0)
+        , mMaxSize(Properties::pathCacheSize) {
     mCache.setOnEntryRemovedListener(this);
 
     GLint maxTextureSize;
diff --git a/libs/hwui/PathCache.h b/libs/hwui/PathCache.h
index 18f380f..d2633aa 100644
--- a/libs/hwui/PathCache.h
+++ b/libs/hwui/PathCache.h
@@ -300,7 +300,7 @@
 
     LruCache<PathDescription, PathTexture*> mCache;
     uint32_t mSize;
-    uint32_t mMaxSize;
+    const uint32_t mMaxSize;
     GLuint mMaxTextureSize;
 
     bool mDebugEnabled;
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 083aeb7..bbd8c72 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -37,7 +37,18 @@
 bool Properties::enablePartialUpdates = true;
 
 float Properties::textGamma = DEFAULT_TEXT_GAMMA;
-int Properties::layerPoolSize = DEFAULT_LAYER_CACHE_SIZE;
+
+int Properties::fboCacheSize = DEFAULT_FBO_CACHE_SIZE;
+int Properties::gradientCacheSize = MB(DEFAULT_GRADIENT_CACHE_SIZE);
+int Properties::layerPoolSize = MB(DEFAULT_LAYER_CACHE_SIZE);
+int Properties::patchCacheSize = KB(DEFAULT_PATCH_CACHE_SIZE);
+int Properties::pathCacheSize = MB(DEFAULT_PATH_CACHE_SIZE);
+int Properties::renderBufferCacheSize = MB(DEFAULT_RENDER_BUFFER_CACHE_SIZE);
+int Properties::tessellationCacheSize = MB(DEFAULT_VERTEX_CACHE_SIZE);
+int Properties::textDropShadowCacheSize = MB(DEFAULT_DROP_SHADOW_CACHE_SIZE);
+int Properties::textureCacheSize = MB(DEFAULT_TEXTURE_CACHE_SIZE);
+
+float Properties::textureCacheFlushRate = DEFAULT_TEXTURE_CACHE_FLUSH_RATE;
 
 DebugLevel Properties::debugLevel = kDebugDisabled;
 OverdrawColorSet Properties::overdrawColorSet = OverdrawColorSet::Default;
@@ -79,7 +90,6 @@
     bool prevDebugOverdraw = debugOverdraw;
     StencilClipDebug prevDebugStencilClip = debugStencilClip;
 
-
     debugOverdraw = false;
     if (property_get(PROPERTY_DEBUG_OVERDRAW, property, nullptr) > 0) {
         INIT_LOGD("  Overdraw debug enabled: %s", property);
@@ -133,7 +143,18 @@
     enablePartialUpdates = property_get_bool(PROPERTY_ENABLE_PARTIAL_UPDATES, true);
 
     textGamma = property_get_float(PROPERTY_TEXT_GAMMA, DEFAULT_TEXT_GAMMA);
+
+    fboCacheSize = property_get_int(PROPERTY_FBO_CACHE_SIZE, DEFAULT_FBO_CACHE_SIZE);
+    gradientCacheSize = MB(property_get_float(PROPERTY_GRADIENT_CACHE_SIZE, DEFAULT_GRADIENT_CACHE_SIZE));
     layerPoolSize = MB(property_get_float(PROPERTY_LAYER_CACHE_SIZE, DEFAULT_LAYER_CACHE_SIZE));
+    patchCacheSize = KB(property_get_float(PROPERTY_PATCH_CACHE_SIZE, DEFAULT_PATCH_CACHE_SIZE));
+    pathCacheSize = MB(property_get_float(PROPERTY_PATH_CACHE_SIZE, DEFAULT_PATH_CACHE_SIZE));
+    renderBufferCacheSize = MB(property_get_float(PROPERTY_RENDER_BUFFER_CACHE_SIZE, DEFAULT_RENDER_BUFFER_CACHE_SIZE));
+    tessellationCacheSize = MB(property_get_float(PROPERTY_VERTEX_CACHE_SIZE, DEFAULT_VERTEX_CACHE_SIZE));
+    textDropShadowCacheSize = MB(property_get_float(PROPERTY_DROP_SHADOW_CACHE_SIZE, DEFAULT_DROP_SHADOW_CACHE_SIZE));
+    textureCacheSize = MB(property_get_float(PROPERTY_TEXTURE_CACHE_SIZE, DEFAULT_TEXTURE_CACHE_SIZE));
+    textureCacheFlushRate = std::max(0.0f, std::min(1.0f,
+            property_get_float(PROPERTY_TEXTURE_CACHE_FLUSH_RATE, DEFAULT_TEXTURE_CACHE_FLUSH_RATE)));
 
     return (prevDebugLayersUpdates != debugLayersUpdates)
             || (prevDebugOverdraw != debugOverdraw)
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index 88f1dbc..3e11151 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -267,7 +267,16 @@
 
     static float textGamma;
 
+    static int fboCacheSize;
+    static int gradientCacheSize;
     static int layerPoolSize;
+    static int patchCacheSize;
+    static int pathCacheSize;
+    static int renderBufferCacheSize;
+    static int tessellationCacheSize;
+    static int textDropShadowCacheSize;
+    static int textureCacheSize;
+    static float textureCacheFlushRate;
 
     static DebugLevel debugLevel;
     static OverdrawColorSet overdrawColorSet;
diff --git a/libs/hwui/RenderBufferCache.cpp b/libs/hwui/RenderBufferCache.cpp
index 11d7a6a..1ac57cd 100644
--- a/libs/hwui/RenderBufferCache.cpp
+++ b/libs/hwui/RenderBufferCache.cpp
@@ -40,16 +40,9 @@
 // Constructors/destructor
 ///////////////////////////////////////////////////////////////////////////////
 
-RenderBufferCache::RenderBufferCache(): mSize(0), mMaxSize(MB(DEFAULT_RENDER_BUFFER_CACHE_SIZE)) {
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_RENDER_BUFFER_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting render buffer cache size to %sMB", property);
-        setMaxSize(MB(atof(property)));
-    } else {
-        INIT_LOGD("  Using default render buffer cache size of %.2fMB",
-                DEFAULT_RENDER_BUFFER_CACHE_SIZE);
-    }
-}
+RenderBufferCache::RenderBufferCache()
+        : mSize(0)
+        , mMaxSize(Properties::renderBufferCacheSize) {}
 
 RenderBufferCache::~RenderBufferCache() {
     clear();
@@ -67,11 +60,6 @@
     return mMaxSize;
 }
 
-void RenderBufferCache::setMaxSize(uint32_t maxSize) {
-    clear();
-    mMaxSize = maxSize;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Caching
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/RenderBufferCache.h b/libs/hwui/RenderBufferCache.h
index 7f59ec1..f77f4c9 100644
--- a/libs/hwui/RenderBufferCache.h
+++ b/libs/hwui/RenderBufferCache.h
@@ -64,10 +64,6 @@
     void clear();
 
     /**
-     * Sets the maximum size of the cache in bytes.
-     */
-    void setMaxSize(uint32_t maxSize);
-    /**
      * Returns the maximum size of the cache in bytes.
      */
     uint32_t getMaxSize();
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index d4588ed..bade216 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -381,6 +381,10 @@
     bool childFunctorsNeedLayer = mProperties.prepareForFunctorPresence(
             willHaveFunctor, functorsNeedLayer);
 
+    if (CC_UNLIKELY(mPositionListener.get())) {
+        mPositionListener->onPositionUpdated(*this, info);
+    }
+
     prepareLayer(info, animatorDirtyMask);
     if (info.mode == TreeInfo::MODE_FULL) {
         pushStagingDisplayListChanges(info);
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 8e4a3df..f248de54 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -209,6 +209,19 @@
     OffscreenBuffer** getLayerHandle() { return &mLayer; } // ugh...
 #endif
 
+    class ANDROID_API PositionListener {
+    public:
+        virtual ~PositionListener() {}
+        virtual void onPositionUpdated(RenderNode& node, const TreeInfo& info) = 0;
+    };
+
+    // Note this is not thread safe, this needs to be called
+    // before the RenderNode is used for drawing.
+    // RenderNode takes ownership of the pointer
+    ANDROID_API void setPositionListener(PositionListener* listener) {
+        mPositionListener.reset(listener);
+    }
+
 private:
     typedef key_value_pair_t<float, DrawRenderNodeOp*> ZDrawRenderNodeOpPair;
 
@@ -317,6 +330,8 @@
     // This is *NOT* thread-safe, and should therefore only be tracking
     // mDisplayList, not mStagingDisplayList.
     uint32_t mParentCount;
+
+    std::unique_ptr<PositionListener> mPositionListener;
 }; // class RenderNode
 
 } /* namespace uirenderer */
diff --git a/libs/hwui/TessellationCache.cpp b/libs/hwui/TessellationCache.cpp
index 461e819..14c8f39 100644
--- a/libs/hwui/TessellationCache.cpp
+++ b/libs/hwui/TessellationCache.cpp
@@ -242,23 +242,21 @@
             spotBuffer);
 }
 
-class ShadowProcessor : public TaskProcessor<TessellationCache::vertexBuffer_pair_t*> {
+class ShadowProcessor : public TaskProcessor<TessellationCache::vertexBuffer_pair_t> {
 public:
     ShadowProcessor(Caches& caches)
-            : TaskProcessor<TessellationCache::vertexBuffer_pair_t*>(&caches.tasks) {}
+            : TaskProcessor<TessellationCache::vertexBuffer_pair_t>(&caches.tasks) {}
     ~ShadowProcessor() {}
 
-    virtual void onProcess(const sp<Task<TessellationCache::vertexBuffer_pair_t*> >& task) override {
+    virtual void onProcess(const sp<Task<TessellationCache::vertexBuffer_pair_t> >& task) override {
         TessellationCache::ShadowTask* t = static_cast<TessellationCache::ShadowTask*>(task.get());
         ATRACE_NAME("shadow tessellation");
 
-        VertexBuffer* ambientBuffer = new VertexBuffer;
-        VertexBuffer* spotBuffer = new VertexBuffer;
         tessellateShadows(&t->drawTransform, &t->localClip, t->opaque, &t->casterPerimeter,
                 &t->transformXY, &t->transformZ, t->lightCenter, t->lightRadius,
-                *ambientBuffer, *spotBuffer);
+                t->ambientBuffer, t->spotBuffer);
 
-        t->setResult(new TessellationCache::vertexBuffer_pair_t(ambientBuffer, spotBuffer));
+        t->setResult(TessellationCache::vertexBuffer_pair_t(&t->ambientBuffer, &t->spotBuffer));
     }
 };
 
@@ -267,18 +265,9 @@
 ///////////////////////////////////////////////////////////////////////////////
 
 TessellationCache::TessellationCache()
-        : mSize(0)
-        , mMaxSize(MB(DEFAULT_VERTEX_CACHE_SIZE))
+        : mMaxSize(Properties::tessellationCacheSize)
         , mCache(LruCache<Description, Buffer*>::kUnlimitedCapacity)
         , mShadowCache(LruCache<ShadowDescription, Task<vertexBuffer_pair_t*>*>::kUnlimitedCapacity) {
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_VERTEX_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting tessellation cache size to %sMB", property);
-        setMaxSize(MB(atof(property)));
-    } else {
-        INIT_LOGD("  Using default tessellation cache size of %.2fMB", DEFAULT_VERTEX_CACHE_SIZE);
-    }
-
     mCache.setOnEntryRemovedListener(&mBufferRemovedListener);
     mShadowCache.setOnEntryRemovedListener(&mBufferPairRemovedListener);
     mDebugEnabled = Properties::debugLevel & kDebugCaches;
@@ -305,13 +294,6 @@
     return mMaxSize;
 }
 
-void TessellationCache::setMaxSize(uint32_t maxSize) {
-    mMaxSize = maxSize;
-    while (mSize > mMaxSize) {
-        mCache.removeOldest();
-    }
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Caching
 ///////////////////////////////////////////////////////////////////////////////
@@ -373,7 +355,7 @@
         task = static_cast<ShadowTask*>(mShadowCache.get(key));
     }
     LOG_ALWAYS_FATAL_IF(task == nullptr, "shadow not precached");
-    outBuffers = *(task->getResult());
+    outBuffers = task->getResult();
 }
 
 sp<TessellationCache::ShadowTask> TessellationCache::getShadowTask(
@@ -392,13 +374,6 @@
     return task;
 }
 
-TessellationCache::ShadowTask::~ShadowTask() {
-    TessellationCache::vertexBuffer_pair_t* bufferPair = getResult();
-    delete bufferPair->getFirst();
-    delete bufferPair->getSecond();
-    delete bufferPair;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Tessellation precaching
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/TessellationCache.h b/libs/hwui/TessellationCache.h
index 977c2d9e..0bd6365 100644
--- a/libs/hwui/TessellationCache.h
+++ b/libs/hwui/TessellationCache.h
@@ -21,6 +21,7 @@
 #include "Matrix.h"
 #include "Rect.h"
 #include "Vector.h"
+#include "VertexBuffer.h"
 #include "thread/TaskProcessor.h"
 #include "utils/Macros.h"
 #include "utils/Pair.h"
@@ -89,7 +90,7 @@
         hash_t hash() const;
     };
 
-    class ShadowTask : public Task<TessellationCache::vertexBuffer_pair_t*> {
+    class ShadowTask : public Task<vertexBuffer_pair_t> {
     public:
         ShadowTask(const Matrix4* drawTransform, const Rect& localClip, bool opaque,
                 const SkPath* casterPerimeter, const Matrix4* transformXY, const Matrix4* transformZ,
@@ -104,13 +105,11 @@
             , lightRadius(lightRadius) {
         }
 
-        ~ShadowTask();
-
         /* Note - we deep copy all task parameters, because *even though* pointers into Allocator
          * controlled objects (like the SkPath and Matrix4s) should be safe for the entire frame,
          * certain Allocators are destroyed before trim() is called to flush incomplete tasks.
          *
-         * These deep copies could be avoided, long term, by cancelling or flushing outstanding
+         * These deep copies could be avoided, long term, by canceling or flushing outstanding
          * tasks before tearing down single-frame LinearAllocators.
          */
         const Matrix4 drawTransform;
@@ -121,6 +120,8 @@
         const Matrix4 transformZ;
         const Vector3 lightCenter;
         const float lightRadius;
+        VertexBuffer ambientBuffer;
+        VertexBuffer spotBuffer;
     };
 
     TessellationCache();
@@ -130,11 +131,6 @@
      * Clears the cache. This causes all TessellationBuffers to be deleted.
      */
     void clear();
-
-    /**
-     * Sets the maximum size of the cache in bytes.
-     */
-    void setMaxSize(uint32_t maxSize);
     /**
      * Returns the maximum size of the cache in bytes.
      */
@@ -197,8 +193,7 @@
 
     Buffer* getOrCreateBuffer(const Description& entry, Tessellator tessellator);
 
-    uint32_t mSize;
-    uint32_t mMaxSize;
+    const uint32_t mMaxSize;
 
     bool mDebugEnabled;
 
@@ -217,12 +212,12 @@
     ///////////////////////////////////////////////////////////////////////////////
     // Shadow tessellation caching
     ///////////////////////////////////////////////////////////////////////////////
-    sp<TaskProcessor<vertexBuffer_pair_t*> > mShadowProcessor;
+    sp<TaskProcessor<vertexBuffer_pair_t> > mShadowProcessor;
 
     // holds a pointer, and implicit strong ref to each shadow task of the frame
-    LruCache<ShadowDescription, Task<vertexBuffer_pair_t*>*> mShadowCache;
-    class BufferPairRemovedListener : public OnEntryRemoved<ShadowDescription, Task<vertexBuffer_pair_t*>*> {
-        void operator()(ShadowDescription& description, Task<vertexBuffer_pair_t*>*& bufferPairTask) override {
+    LruCache<ShadowDescription, Task<vertexBuffer_pair_t>*> mShadowCache;
+    class BufferPairRemovedListener : public OnEntryRemoved<ShadowDescription, Task<vertexBuffer_pair_t>*> {
+        void operator()(ShadowDescription& description, Task<vertexBuffer_pair_t>*& bufferPairTask) override {
             bufferPairTask->decStrong(nullptr);
         }
     };
diff --git a/libs/hwui/TextDropShadowCache.cpp b/libs/hwui/TextDropShadowCache.cpp
index 1707468..fe4b3d75 100644
--- a/libs/hwui/TextDropShadowCache.cpp
+++ b/libs/hwui/TextDropShadowCache.cpp
@@ -93,36 +93,21 @@
 // Constructors/destructor
 ///////////////////////////////////////////////////////////////////////////////
 
-TextDropShadowCache::TextDropShadowCache():
-        mCache(LruCache<ShadowText, ShadowTexture*>::kUnlimitedCapacity),
-        mSize(0), mMaxSize(MB(DEFAULT_DROP_SHADOW_CACHE_SIZE)) {
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_DROP_SHADOW_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting drop shadow cache size to %sMB", property);
-        setMaxSize(MB(atof(property)));
-    } else {
-        INIT_LOGD("  Using default drop shadow cache size of %.2fMB",
-                DEFAULT_DROP_SHADOW_CACHE_SIZE);
-    }
+TextDropShadowCache::TextDropShadowCache()
+        : TextDropShadowCache(Properties::textDropShadowCacheSize) {}
 
-    init();
-}
-
-TextDropShadowCache::TextDropShadowCache(uint32_t maxByteSize):
-        mCache(LruCache<ShadowText, ShadowTexture*>::kUnlimitedCapacity),
-        mSize(0), mMaxSize(maxByteSize) {
-    init();
+TextDropShadowCache::TextDropShadowCache(uint32_t maxByteSize)
+        : mCache(LruCache<ShadowText, ShadowTexture*>::kUnlimitedCapacity)
+        , mSize(0)
+        , mMaxSize(maxByteSize) {
+    mCache.setOnEntryRemovedListener(this);
+    mDebugEnabled = Properties::debugLevel & kDebugMoreCaches;
 }
 
 TextDropShadowCache::~TextDropShadowCache() {
     mCache.clear();
 }
 
-void TextDropShadowCache::init() {
-    mCache.setOnEntryRemovedListener(this);
-    mDebugEnabled = Properties::debugLevel & kDebugMoreCaches;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Size management
 ///////////////////////////////////////////////////////////////////////////////
@@ -135,13 +120,6 @@
     return mMaxSize;
 }
 
-void TextDropShadowCache::setMaxSize(uint32_t maxSize) {
-    mMaxSize = maxSize;
-    while (mSize > mMaxSize) {
-        mCache.removeOldest();
-    }
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Callbacks
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/TextDropShadowCache.h b/libs/hwui/TextDropShadowCache.h
index c4f3c5d..cf64788 100644
--- a/libs/hwui/TextDropShadowCache.h
+++ b/libs/hwui/TextDropShadowCache.h
@@ -148,10 +148,6 @@
     }
 
     /**
-     * Sets the maximum size of the cache in bytes.
-     */
-    void setMaxSize(uint32_t maxSize);
-    /**
      * Returns the maximum size of the cache in bytes.
      */
     uint32_t getMaxSize();
@@ -161,13 +157,11 @@
     uint32_t getSize();
 
 private:
-    void init();
-
     LruCache<ShadowText, ShadowTexture*> mCache;
 
     uint32_t mSize;
-    uint32_t mMaxSize;
-    FontRenderer* mRenderer;
+    const uint32_t mMaxSize;
+    FontRenderer* mRenderer = nullptr;
     bool mDebugEnabled;
 }; // class TextDropShadowCache
 
diff --git a/libs/hwui/TextureCache.cpp b/libs/hwui/TextureCache.cpp
index 31bfa3a..ade8600 100644
--- a/libs/hwui/TextureCache.cpp
+++ b/libs/hwui/TextureCache.cpp
@@ -35,26 +35,9 @@
 TextureCache::TextureCache()
         : mCache(LruCache<uint32_t, Texture*>::kUnlimitedCapacity)
         , mSize(0)
-        , mMaxSize(MB(DEFAULT_TEXTURE_CACHE_SIZE))
-        , mFlushRate(DEFAULT_TEXTURE_CACHE_FLUSH_RATE)
+        , mMaxSize(Properties::textureCacheSize)
+        , mFlushRate(Properties::textureCacheFlushRate)
         , mAssetAtlas(nullptr) {
-    char property[PROPERTY_VALUE_MAX];
-    if (property_get(PROPERTY_TEXTURE_CACHE_SIZE, property, nullptr) > 0) {
-        INIT_LOGD("  Setting texture cache size to %sMB", property);
-        setMaxSize(MB(atof(property)));
-    } else {
-        INIT_LOGD("  Using default texture cache size of %.2fMB", DEFAULT_TEXTURE_CACHE_SIZE);
-    }
-
-    if (property_get(PROPERTY_TEXTURE_CACHE_FLUSH_RATE, property, nullptr) > 0) {
-        float flushRate = atof(property);
-        INIT_LOGD("  Setting texture cache flush rate to %.2f%%", flushRate * 100.0f);
-        setFlushRate(flushRate);
-    } else {
-        INIT_LOGD("  Using default texture cache flush rate of %.2f%%",
-                DEFAULT_TEXTURE_CACHE_FLUSH_RATE * 100.0f);
-    }
-
     mCache.setOnEntryRemovedListener(this);
 
     glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize);
@@ -79,17 +62,6 @@
     return mMaxSize;
 }
 
-void TextureCache::setMaxSize(uint32_t maxSize) {
-    mMaxSize = maxSize;
-    while (mSize > mMaxSize) {
-        mCache.removeOldest();
-    }
-}
-
-void TextureCache::setFlushRate(float flushRate) {
-    mFlushRate = std::max(0.0f, std::min(1.0f, flushRate));
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Callbacks
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/TextureCache.h b/libs/hwui/TextureCache.h
index 463450c..a4317ce 100644
--- a/libs/hwui/TextureCache.h
+++ b/libs/hwui/TextureCache.h
@@ -109,10 +109,6 @@
     void clear();
 
     /**
-     * Sets the maximum size of the cache in bytes.
-     */
-    void setMaxSize(uint32_t maxSize);
-    /**
      * Returns the maximum size of the cache in bytes.
      */
     uint32_t getMaxSize();
@@ -126,11 +122,6 @@
      * is defined by the flush rate.
      */
     void flush();
-    /**
-     * Indicates the percentage of the cache to retain when a
-     * memory trim is requested (see Caches::flush).
-     */
-    void setFlushRate(float flushRate);
 
     void setAssetAtlas(AssetAtlas* assetAtlas);
 
@@ -148,10 +139,10 @@
     LruCache<uint32_t, Texture*> mCache;
 
     uint32_t mSize;
-    uint32_t mMaxSize;
+    const uint32_t mMaxSize;
     GLint mMaxTextureSize;
 
-    float mFlushRate;
+    const float mFlushRate;
 
     bool mDebugEnabled;
 
diff --git a/libs/hwui/TreeInfo.h b/libs/hwui/TreeInfo.h
index be25516..accd303 100644
--- a/libs/hwui/TreeInfo.h
+++ b/libs/hwui/TreeInfo.h
@@ -86,6 +86,12 @@
 #endif
     ErrorHandler* errorHandler = nullptr;
 
+    // Frame number for use with synchronized surfaceview position updating
+    int64_t frameNumber = -1;
+    int32_t windowInsetLeft = 0;
+    int32_t windowInsetTop = 0;
+    bool updateWindowPositions = false;
+
     struct Out {
         bool hasFunctors = false;
         // This is only updated if evaluateAnimations is true
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index 541c799..2e3856f 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -324,7 +324,7 @@
     // Save the current clip information, which is local to this group.
     outCanvas->save();
     // Draw the group tree in the same order as the XML file.
-    for (Node* child : mChildren) {
+    for (auto& child : mChildren) {
         child->draw(outCanvas, stackedMatrix, scaleX, scaleY);
     }
     // Restore the previous clip information.
@@ -361,7 +361,7 @@
 }
 
 void Group::addChild(Node* child) {
-    mChildren.push_back(child);
+    mChildren.emplace_back(child);
 }
 
 bool Group::getProperties(float* outProperties, int length) {
diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h
index f8f1ea6..36a8aeb 100644
--- a/libs/hwui/VectorDrawable.h
+++ b/libs/hwui/VectorDrawable.h
@@ -316,7 +316,7 @@
         // Count of the properties, must be at the end.
         Count,
     };
-    std::vector<Node*> mChildren;
+    std::vector< std::unique_ptr<Node> > mChildren;
     Properties mProperties;
 };
 
@@ -360,7 +360,7 @@
     float mViewportHeight = 0;
     float mRootAlpha = 1.0f;
 
-    Group* mRootNode;
+    std::unique_ptr<Group> mRootNode;
     SkRect mBounds;
     SkMatrix mCanvasMatrix;
     SkPaint mPaint;
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index d411621..ea702c0 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -92,18 +92,18 @@
     }
 }
 
-void CanvasContext::setSurface(ANativeWindow* window) {
+void CanvasContext::setSurface(Surface* surface) {
     ATRACE_CALL();
 
-    mNativeWindow = window;
+    mNativeSurface = surface;
 
     if (mEglSurface != EGL_NO_SURFACE) {
         mEglManager.destroySurface(mEglSurface);
         mEglSurface = EGL_NO_SURFACE;
     }
 
-    if (window) {
-        mEglSurface = mEglManager.createSurface(window);
+    if (surface) {
+        mEglSurface = mEglManager.createSurface(surface);
     }
 
     if (mEglSurface != EGL_NO_SURFACE) {
@@ -127,8 +127,8 @@
     mSwapBehavior = swapBehavior;
 }
 
-void CanvasContext::initialize(ANativeWindow* window) {
-    setSurface(window);
+void CanvasContext::initialize(Surface* surface) {
+    setSurface(surface);
 #if !HWUI_NEW_OPS
     if (mCanvas) return;
     mCanvas = new OpenGLRenderer(mRenderThread.renderState());
@@ -136,11 +136,11 @@
 #endif
 }
 
-void CanvasContext::updateSurface(ANativeWindow* window) {
-    setSurface(window);
+void CanvasContext::updateSurface(Surface* surface) {
+    setSurface(surface);
 }
 
-bool CanvasContext::pauseSurface(ANativeWindow* window) {
+bool CanvasContext::pauseSurface(Surface* surface) {
     return mRenderThread.removeFrameCallback(this);
 }
 
@@ -204,6 +204,10 @@
     info.renderer = mCanvas;
 #endif
 
+    if (CC_LIKELY(mNativeSurface.get())) {
+        info.frameNumber = static_cast<int64_t>(mNativeSurface->getNextFrameNumber());
+    }
+
     mAnimationContext->startFrame(info.mode);
     for (const sp<RenderNode>& node : mRenderNodes) {
         // Only the primary target node will be drawn full - all other nodes would get drawn in
@@ -219,7 +223,7 @@
     freePrefetechedLayers();
     GL_CHECKPOINT(MODERATE);
 
-    if (CC_UNLIKELY(!mNativeWindow.get())) {
+    if (CC_UNLIKELY(!mNativeSurface.get())) {
         mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
         info.out.canDrawThisFrame = false;
         return;
@@ -242,8 +246,9 @@
         } else {
             // We're maybe behind? Find out for sure
             int runningBehind = 0;
-            mNativeWindow->query(mNativeWindow.get(),
-                    NATIVE_WINDOW_CONSUMER_RUNNING_BEHIND, &runningBehind);
+            // TODO: Have this method be on Surface, too, not just ANativeWindow...
+            ANativeWindow* window = mNativeSurface.get();
+            window->query(window, NATIVE_WINDOW_CONSUMER_RUNNING_BEHIND, &runningBehind);
             info.out.canDrawThisFrame = !runningBehind;
         }
     } else {
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 63a7977..168166e 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -39,6 +39,7 @@
 #include <SkBitmap.h>
 #include <SkRect.h>
 #include <utils/Functor.h>
+#include <gui/Surface.h>
 
 #include <set>
 #include <string>
@@ -75,10 +76,10 @@
     // Won't take effect until next EGLSurface creation
     void setSwapBehavior(SwapBehavior swapBehavior);
 
-    void initialize(ANativeWindow* window);
-    void updateSurface(ANativeWindow* window);
-    bool pauseSurface(ANativeWindow* window);
-    bool hasSurface() { return mNativeWindow.get(); }
+    void initialize(Surface* surface);
+    void updateSurface(Surface* surface);
+    bool pauseSurface(Surface* surface);
+    bool hasSurface() { return mNativeSurface.get(); }
 
     void setup(int width, int height, float lightRadius,
             uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
@@ -172,7 +173,7 @@
     // lifecycle tracking
     friend class android::uirenderer::RenderState;
 
-    void setSurface(ANativeWindow* window);
+    void setSurface(Surface* window);
     void requireSurface();
 
     void freePrefetechedLayers();
@@ -182,7 +183,7 @@
 
     RenderThread& mRenderThread;
     EglManager& mEglManager;
-    sp<ANativeWindow> mNativeWindow;
+    sp<Surface> mNativeSurface;
     EGLSurface mEglSurface = EGL_NO_SURFACE;
     bool mBufferPreserved = false;
     SwapBehavior mSwapBehavior = kSwap_default;
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 466fef9..364d4dd 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -228,6 +228,13 @@
     LOG_ALWAYS_FATAL_IF(surface == EGL_NO_SURFACE,
             "Failed to create EGLSurface for window %p, eglErr = %s",
             (void*) window, egl_error_str());
+
+    if (mSwapBehavior != SwapBehavior::Preserved) {
+        LOG_ALWAYS_FATAL_IF(eglSurfaceAttrib(mEglDisplay, surface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_DESTROYED) == EGL_FALSE,
+                            "Failed to set swap behavior to destroyed for window %p, eglErr = %s",
+                            (void*) window, egl_error_str());
+    }
+
     return surface;
 }
 
@@ -337,8 +344,8 @@
         // For some reason our surface was destroyed out from under us
         // This really shouldn't happen, but if it does we can recover easily
         // by just not trying to use the surface anymore
-        ALOGW("swapBuffers encountered EGL_BAD_SURFACE on %p, halting rendering...",
-                frame.mSurface);
+        ALOGW("swapBuffers encountered EGL error %d on %p, halting rendering...",
+                err, frame.mSurface);
         return false;
     }
     LOG_ALWAYS_FATAL("Encountered EGL error %d %s during rendering",
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 1d1b144..7c6cd7e 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -139,38 +139,38 @@
     postAndWait(task); // block since name/value pointers owned by caller
 }
 
-CREATE_BRIDGE2(initialize, CanvasContext* context, ANativeWindow* window) {
-    args->context->initialize(args->window);
+CREATE_BRIDGE2(initialize, CanvasContext* context, Surface* surface) {
+    args->context->initialize(args->surface);
     return nullptr;
 }
 
-void RenderProxy::initialize(const sp<ANativeWindow>& window) {
+void RenderProxy::initialize(const sp<Surface>& surface) {
     SETUP_TASK(initialize);
     args->context = mContext;
-    args->window = window.get();
+    args->surface = surface.get();
     post(task);
 }
 
-CREATE_BRIDGE2(updateSurface, CanvasContext* context, ANativeWindow* window) {
-    args->context->updateSurface(args->window);
+CREATE_BRIDGE2(updateSurface, CanvasContext* context, Surface* surface) {
+    args->context->updateSurface(args->surface);
     return nullptr;
 }
 
-void RenderProxy::updateSurface(const sp<ANativeWindow>& window) {
+void RenderProxy::updateSurface(const sp<Surface>& surface) {
     SETUP_TASK(updateSurface);
     args->context = mContext;
-    args->window = window.get();
+    args->surface = surface.get();
     postAndWait(task);
 }
 
-CREATE_BRIDGE2(pauseSurface, CanvasContext* context, ANativeWindow* window) {
-    return (void*) args->context->pauseSurface(args->window);
+CREATE_BRIDGE2(pauseSurface, CanvasContext* context, Surface* surface) {
+    return (void*) args->context->pauseSurface(args->surface);
 }
 
-bool RenderProxy::pauseSurface(const sp<ANativeWindow>& window) {
+bool RenderProxy::pauseSurface(const sp<Surface>& surface) {
     SETUP_TASK(pauseSurface);
     args->context = mContext;
-    args->window = window.get();
+    args->surface = surface.get();
     return (bool) postAndWait(task);
 }
 
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 4180d802..178724a 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -67,9 +67,9 @@
     ANDROID_API bool loadSystemProperties();
     ANDROID_API void setName(const char* name);
 
-    ANDROID_API void initialize(const sp<ANativeWindow>& window);
-    ANDROID_API void updateSurface(const sp<ANativeWindow>& window);
-    ANDROID_API bool pauseSurface(const sp<ANativeWindow>& window);
+    ANDROID_API void initialize(const sp<Surface>& surface);
+    ANDROID_API void updateSurface(const sp<Surface>& surface);
+    ANDROID_API bool pauseSurface(const sp<Surface>& surface);
     ANDROID_API void setup(int width, int height, float lightRadius,
             uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
     ANDROID_API void setLightCenter(const Vector3& lightCenter);
diff --git a/libs/hwui/tests/unit/GradientCacheTests.cpp b/libs/hwui/tests/unit/GradientCacheTests.cpp
new file mode 100644
index 0000000..0ee9647
--- /dev/null
+++ b/libs/hwui/tests/unit/GradientCacheTests.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include <gtest/gtest.h>
+
+#include "Extensions.h"
+#include "GradientCache.h"
+#include "tests/common/TestUtils.h"
+
+using namespace android;
+using namespace android::uirenderer;
+
+RENDERTHREAD_TEST(GradientCache, addRemove) {
+    Extensions extensions;
+    GradientCache cache(extensions);
+    ASSERT_LT(1000u, cache.getMaxSize()) << "Expect non-trivial size";
+
+    SkColor colors[] = { 0xFF00FF00, 0xFFFF0000, 0xFF0000FF };
+    float positions[] = { 1, 2, 3 };
+    Texture* texture = cache.get(colors, positions, 3);
+    ASSERT_TRUE(texture);
+    ASSERT_FALSE(texture->cleanup);
+    ASSERT_EQ((uint32_t) texture->objectSize(), cache.getSize());
+    ASSERT_TRUE(cache.getSize());
+    cache.clear();
+    ASSERT_EQ(cache.getSize(), 0u);
+}
diff --git a/libs/hwui/tests/unit/RecordingCanvasTests.cpp b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
index 01bfc5a..20d2f1f 100644
--- a/libs/hwui/tests/unit/RecordingCanvasTests.cpp
+++ b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
@@ -455,6 +455,23 @@
     }
 }
 
+TEST(RecordingCanvas, firstClipWillReplace) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.save(SaveFlags::MatrixClip);
+        // since no explicit clip set on canvas, this should be the one observed on op:
+        canvas.clipRect(-100, -100, 300, 300, SkRegion::kIntersect_Op);
+
+        SkPaint paint;
+        paint.setColor(SK_ColorWHITE);
+        canvas.drawRect(0, 0, 100, 100, paint);
+
+        canvas.restore();
+    });
+    ASSERT_EQ(1u, dl->getOps().size()) << "Must have one op";
+    // first clip must be preserved, even if it extends beyond canvas bounds
+    EXPECT_CLIP_RECT(Rect(-100, -100, 300, 300), dl->getOps()[0]->localClip);
+}
+
 TEST(RecordingCanvas, insertReorderBarrier) {
     auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
         canvas.drawRect(0, 0, 400, 400, SkPaint());
diff --git a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
new file mode 100644
index 0000000..586625b
--- /dev/null
+++ b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+#include "tests/common/TestUtils.h"
+
+#include <gtest/gtest.h>
+#include <SkShader.h>
+
+using namespace android;
+using namespace android::uirenderer;
+
+/**
+ * 1x1 bitmaps must not be optimized into solid color shaders, since HWUI can't
+ * compose/render color shaders
+ */
+TEST(SkiaBehavior, CreateBitmapShader1x1) {
+    SkBitmap origBitmap = TestUtils::createSkBitmap(1, 1);
+    std::unique_ptr<SkShader> s(SkShader::CreateBitmapShader(
+            origBitmap,
+            SkShader::kClamp_TileMode,
+            SkShader::kRepeat_TileMode));
+
+    SkBitmap bitmap;
+    SkShader::TileMode xy[2];
+    ASSERT_TRUE(s->isABitmap(&bitmap, nullptr, xy))
+        << "1x1 bitmap shader must query as bitmap shader";
+    EXPECT_EQ(SkShader::kClamp_TileMode, xy[0]);
+    EXPECT_EQ(SkShader::kRepeat_TileMode, xy[1]);
+    EXPECT_EQ(origBitmap.pixelRef(), bitmap.pixelRef());
+}
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 3007d86..b26b310 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1784,9 +1784,9 @@
      * Note that the actual playback of this data might occur after this function returns.
      *
      * @param audioData the array that holds the data to play.
-     * @param offsetInBytes the offset expressed in bytes in audioData where the data to play
+     * @param offsetInBytes the offset expressed in bytes in audioData where the data to write
      *    starts.
-     * @param sizeInBytes the number of bytes to read in audioData after the offset.
+     * @param sizeInBytes the number of bytes to write in audioData after the offset.
      * @return zero or the positive number of bytes that were written, or
      *    {@link #ERROR_INVALID_OPERATION}
      *    if the track isn't properly initialized, or {@link #ERROR_BAD_VALUE} if
@@ -1821,9 +1821,9 @@
      * Note that the actual playback of this data might occur after this function returns.
      *
      * @param audioData the array that holds the data to play.
-     * @param offsetInBytes the offset expressed in bytes in audioData where the data to play
+     * @param offsetInBytes the offset expressed in bytes in audioData where the data to write
      *    starts.
-     * @param sizeInBytes the number of bytes to read in audioData after the offset.
+     * @param sizeInBytes the number of bytes to write in audioData after the offset.
      * @param writeMode one of {@link #WRITE_BLOCKING}, {@link #WRITE_NON_BLOCKING}. It has no
      *     effect in static mode.
      *     <br>With {@link #WRITE_BLOCKING}, the write will block until all data has been written
@@ -1920,8 +1920,8 @@
      * In static buffer mode, copies the data to the buffer starting at offset 0.
      * Note that the actual playback of this data might occur after this function returns.
      *
-     * @param audioData the array that holds the data to play.
-     * @param offsetInShorts the offset expressed in shorts in audioData where the data to play
+     * @param audioData the array that holds the data to write.
+     * @param offsetInShorts the offset expressed in shorts in audioData where the data to write
      *     starts.
      * @param sizeInShorts the number of shorts to read in audioData after the offset.
      * @param writeMode one of {@link #WRITE_BLOCKING}, {@link #WRITE_NON_BLOCKING}. It has no
@@ -1987,7 +1987,7 @@
      * and the write mode is ignored.
      * Note that the actual playback of this data might occur after this function returns.
      *
-     * @param audioData the array that holds the data to play.
+     * @param audioData the array that holds the data to write.
      *     The implementation does not clip for sample values within the nominal range
      *     [-1.0f, 1.0f], provided that all gains in the audio pipeline are
      *     less than or equal to unity (1.0f), and in the absence of post-processing effects
@@ -1998,8 +1998,8 @@
      *     and later processing in the audio path.  Therefore applications are encouraged
      *     to provide samples values within the nominal range.
      * @param offsetInFloats the offset, expressed as a number of floats,
-     *     in audioData where the data to play starts.
-     * @param sizeInFloats the number of floats to read in audioData after the offset.
+     *     in audioData where the data to write starts.
+     * @param sizeInFloats the number of floats to write in audioData after the offset.
      * @param writeMode one of {@link #WRITE_BLOCKING}, {@link #WRITE_NON_BLOCKING}. It has no
      *     effect in static mode.
      *     <br>With {@link #WRITE_BLOCKING}, the write will block until all data has been written
@@ -2070,7 +2070,7 @@
      * and the write mode is ignored.
      * Note that the actual playback of this data might occur after this function returns.
      *
-     * @param audioData the buffer that holds the data to play, starting at the position reported
+     * @param audioData the buffer that holds the data to write, starting at the position reported
      *     by <code>audioData.position()</code>.
      *     <BR>Note that upon return, the buffer position (<code>audioData.position()</code>) will
      *     have been advanced to reflect the amount of data that was successfully written to
@@ -2137,7 +2137,7 @@
     /**
      * Writes the audio data to the audio sink for playback in streaming mode on a HW_AV_SYNC track.
      * The blocking behavior will depend on the write mode.
-     * @param audioData the buffer that holds the data to play, starting at the position reported
+     * @param audioData the buffer that holds the data to write, starting at the position reported
      *     by <code>audioData.position()</code>.
      *     <BR>Note that upon return, the buffer position (<code>audioData.position()</code>) will
      *     have been advanced to reflect the amount of data that was successfully written to
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index f9fdd8d..54543ec 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -428,8 +428,17 @@
                     }
                 }
                 // rule didn't exist, add it
-                // FIXME doesn't work with RULE_MATCH_UID yet
-                mCriteria.add(new AttributeMatchCriterion(attrToMatch, rule));
+                switch (match_rule) {
+                    case RULE_MATCH_ATTRIBUTE_USAGE:
+                    case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
+                        mCriteria.add(new AttributeMatchCriterion(attrToMatch, rule));
+                        break;
+                    case RULE_MATCH_UID:
+                        mCriteria.add(new AttributeMatchCriterion(intProp, rule));
+                        break;
+                    default:
+                        throw new IllegalStateException("Unreachable code in addRuleInternal()");
+                }
             }
             return this;
         }
diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java
index 80b3ffc..56b2514 100644
--- a/media/java/android/media/browse/MediaBrowser.java
+++ b/media/java/android/media/browse/MediaBrowser.java
@@ -117,6 +117,9 @@
      * to the media browse service when connecting and retrieving the root id
      * for browsing, or null if none. The contents of this bundle may affect
      * the information returned when browsing.
+     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT
+     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
+     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
      */
     public MediaBrowser(Context context, ComponentName serviceComponent,
             ConnectionCallback callback, Bundle rootHints) {
diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java
index 0393c94..3372524 100644
--- a/media/java/android/service/media/MediaBrowserService.java
+++ b/media/java/android/service/media/MediaBrowserService.java
@@ -351,6 +351,9 @@
      *            root id for browsing, or null if none. The contents of this
      *            bundle may affect the information returned when browsing.
      * @return The {@link BrowserRoot} for accessing this app's content or null.
+     * @see BrowserRoot#EXTRA_RECENT
+     * @see BrowserRoot#EXTRA_OFFLINE
+     * @see BrowserRoot#EXTRA_SUGGESTED
      */
     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
             int clientUid, @Nullable Bundle rootHints);
@@ -667,6 +670,57 @@
      * when first connected.
      */
     public static final class BrowserRoot {
+        /**
+         * The lookup key for a boolean that indicates whether the browser service should return a
+         * browser root for recently played media items.
+         *
+         * <p>When creating a media browser for a given media browser service, this key can be
+         * supplied as a root hint for retrieving media items that are recently played.
+         * If the media browser service can provide such media items, the implementation must return
+         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
+         *
+         * <p>The root hint may contain multiple keys.
+         *
+         * @see #EXTRA_OFFLINE
+         * @see #EXTRA_SUGGESTED
+         */
+        public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
+
+        /**
+         * The lookup key for a boolean that indicates whether the browser service should return a
+         * browser root for offline media items.
+         *
+         * <p>When creating a media browser for a given media browser service, this key can be
+         * supplied as a root hint for retrieving media items that are can be played without an
+         * internet connection.
+         * If the media browser service can provide such media items, the implementation must return
+         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
+         *
+         * <p>The root hint may contain multiple keys.
+         *
+         * @see #EXTRA_RECENT
+         * @see #EXTRA_SUGGESTED
+         */
+        public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+
+        /**
+         * The lookup key for a boolean that indicates whether the browser service should return a
+         * browser root for suggested media items.
+         *
+         * <p>When creating a media browser for a given media browser service, this key can be
+         * supplied as a root hint for retrieving the media items suggested by the media browser
+         * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
+         * is considered ordered by relevance, first being the top suggestion.
+         * If the media browser service can provide such media items, the implementation must return
+         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
+         *
+         * <p>The root hint may contain multiple keys.
+         *
+         * @see #EXTRA_RECENT
+         * @see #EXTRA_OFFLINE
+         */
+        public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
+
         final private String mRootId;
         final private Bundle mExtras;
 
diff --git a/packages/DocumentsUI/res/drawable/cabinet.png b/packages/DocumentsUI/res/drawable/cabinet.png
deleted file mode 100644
index da44023..0000000
--- a/packages/DocumentsUI/res/drawable/cabinet.png
+++ /dev/null
Binary files differ
diff --git a/packages/DocumentsUI/res/drawable/cabinet.xml b/packages/DocumentsUI/res/drawable/cabinet.xml
new file mode 100644
index 0000000..843ffc7
--- /dev/null
+++ b/packages/DocumentsUI/res/drawable/cabinet.xml
@@ -0,0 +1,81 @@
+<!--
+Copyright (C) 2016 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:width="672dp"
+        android:height="921dp"
+        android:viewportWidth="672.0"
+        android:viewportHeight="921.0">
+    <path
+        android:pathData="M286,0c5,0,10,0,15,0c0.1,1.8,1.5,1.8,2.8,2.1c11.1,2,22.1,4,33.2,6.1c31.8,6.1,63.7,12.3,95.5,18.5  c16.1,3.1,32.1,6.2,48.2,9.3c26,4.9,52.1,9.3,78,14.6c10.8,2.2,21.6,4.6,32.3,6.5c11.3,2,22.6,4.7,34,6c7.9,0.9,7.9,1.1,7.9,9.2  c0,237.3,0,474.5,0,711.8c-1.5,0.9,-3,2,-4.6,2.8c-18.3,8.3,-36.6,16.6,-54.8,25c-29.3,13.4,-58.5,26.8,-87.8,40.3  c-23.5,10.9,-47,21.8,-70.4,32.8c-2.1,1,-4.2,1.5,-6.3,1.1c-6.8,-1.3,-13.6,-2.5,-20.1,-4.9c5.9,-0.3,11.4,1.9,17.1,2.9c5.9,1.1,5.9,1,5.9,-4.9  c0,-17.1,0.1,-34.3,0,-51.4c-0.3,-68.9,-0.7,-137.8,-1,-206.7c0,-35.8,0,-71.6,0.1,-107.4c0,-3.8,-0.6,-5.2,-4.7,-3.7c-7.9,2.9,-16,5.4,-24.1,7.8  c-14.1,4.3,-27.8,10,-42.2,13.2c0,-64,0,-127.9,-0.1,-191.9c0,-4.1,1.3,-5.9,5.1,-7c21,-6.6,42,-13.4,63,-20.2c2.3,-0.8,4.4,-1.8,4,-4.9  c0,-59.3,0,-118.7,0,-178c0,-1.3,-0.7,-2,-2,-2c-2.6,-0.4,-5.2,-0.7,-7.8,-1.2c-30.2,-5.3,-60.5,-10.6,-90.7,-16c-31.9,-5.6,-63.7,-11.3,-95.6,-16.9  c-24.9,-4.4,-49.8,-8.7,-74.6,-13.1C117.1,75.6,93.1,71.3,69,67c-0.3,-0.3,-0.7,-0.7,-1,-1c17.4,-5.3,34.8,-10.7,52.3,-15.9  c29.4,-8.7,58.8,-17.2,88.2,-25.8c24.3,-7.1,48.6,-14.2,72.9,-21.4C283.1,2.4,285.6,2.7,286,0z"
+        android:fillColor="#EFEFEE"/>
+    <path
+        android:pathData="M412,307c0.4,3,-1.7,4.1,-4,4.9c-21,6.8,-42,13.6,-63,20.2c-3.8,1.2,-5.1,3,-5.1,7C340,403.1,340,467,340,531  c-11.8,-1.2,-23.3,-4.5,-34.9,-6.5c-10,-1.7,-19.9,-4.6,-30.1,-5.5c-0.7,-0.3,-1.4,-0.9,-2.2,-1c-19.8,-4,-39.5,-8,-59.3,-12c-12.2,-2.4,-24.3,-4.7,-36.5,-7  c-0.9,-0.3,-1.8,-0.8,-2.8,-1c-24.5,-4.9,-48.9,-9.9,-73.5,-14.6C89.3,481.3,78,477.1,66,478c-0.7,-1.6,-2.1,-1.8,-3.6,-2.1  c-11.1,-2.2,-22.2,-4.7,-33.3,-6.7c-9.7,-1.7,-19.1,-4.9,-29.1,-5.3c0,-64.3,0,-128.7,0,-193c0.8,-0.2,1.6,-0.4,2.4,-0.7c19,-7.8,37.9,-15.9,57.1,-23.4  c5.4,-2.1,6.7,-4.8,6.6,-10.2c-0.2,-55.1,-0.1,-110.2,-0.1,-165.4c0,-1.9,-1.4,-4.6,1.9,-5.4c0.3,0.3,0.7,0.7,1,1c-1.3,4.9,-1,9.9,-1,14.9  c0,51.1,0,102.3,0,153.4c0,1.2,0,2.3,0,3.5c0.1,3.5,1.2,5.9,5.3,6.5c7,1.1,14,2.6,21,3.9c22.1,4.3,44.1,8.6,66.2,12.8  c27.3,5.2,54.6,10.1,81.9,15.3c21.8,4.1,43.5,8.5,65.2,12.6c28.1,5.4,56.2,10.8,84.3,15.8C398.4,306.8,405.1,310.5,412,307z   M105,329c0,3.3,0,6.7,-0.1,10c-0.1,2.5,0.4,3.6,3.4,4.2c30,5.3,59.9,10.9,89.8,16.5c3.4,0.6,5.1,0.2,4.9,-3.7  c-0.2,-3.3,-0.1,-6.7,-0.1,-10c0.5,-3.6,-0.1,-6.3,-4.7,-6.1c-1.1,0.1,-2.2,-0.6,-3.4,-0.8c-28.3,-5,-56.6,-9.9,-84.9,-14.9  C105.6,323.4,104.5,325.2,105,329z M65.9,280.8c13.7,2.5,27.4,4.9,41.1,7.4c32.6,5.9,65.2,11.8,97.8,17.8  c41.4,7.6,82.8,15.2,124.2,22.8c6.8,1.2,13.3,1.4,20,-1.3c9.3,-3.6,18.9,-6.3,28.4,-9.4c6.4,-2.1,12.8,-4.2,19.2,-6.4  c-4.2,-2,-8.3,-3,-12.4,-3.8c-22.6,-4.2,-45.3,-8,-67.9,-12.6c-14.6,-3,-29.2,-5.6,-43.8,-8.5c-24,-4.6,-48,-9.2,-72,-13.7c-15.9,-3,-31.8,-6.2,-47.8,-9.2  c-19,-3.6,-38,-7.3,-57,-10.6c-9.9,-1.7,-19.6,-4.5,-29.7,-5.2C48,255.6,30.1,263,12.2,270.4c0,0.4,0,0.7,0,1.1  C30.1,274.6,48,277.7,65.9,280.8z"
+        android:fillColor="#EAEAEA"/>
+    <path
+        android:pathData="M672,782c-6,0.9,-11.1,4.3,-16.4,6.9c-30.8,15,-61.5,30.3,-92.3,45.4c-34.8,17.1,-69.5,34.3,-104.5,51.1  c-13,6.3,-26,12.8,-39,19.1c-1.5,0.7,-3.7,1,-3.9,3.4c-3.7,0,-7.3,0,-11,0c-0.4,-3,-3.1,-2.3,-4.7,-2.7c-19.3,-4.8,-38.6,-9.5,-57.9,-14.1  c-27.5,-6.5,-55.2,-12.7,-82.6,-19.4c-30.9,-7.5,-61.8,-15,-92.7,-22.1c-24.8,-5.8,-49.5,-12,-74.3,-18C70.8,826.5,48.9,821.3,27,816  c-1.1,-0.3,-2.3,-0.5,-3.3,-1c-3.2,-1.3,-3.5,-3.3,-0.7,-5.4c0.9,-0.7,2,-1.2,3.1,-1.7c12,-5.3,24,-10.7,36,-16c0.4,-0.2,0.9,-0.2,1.9,-0.3  c0,3.6,0,7,0,10.5c0,1.6,-0.5,3.5,2,3.9c0.7,2.8,3.2,2.5,5.2,3c39.3,9,78.7,18.1,118.1,27c43.9,10,87.7,20,131.6,29.9  c9.7,2.2,19.2,4.9,29.1,6c1.1,1.5,2.6,0.9,4,1l0,0c4.1,2,8.5,2.6,13,3l0,0c7.2,2.4,14.4,4.3,22,5l0,0c6.5,2.5,13.3,3.6,20.1,4.9  c2.1,0.4,4.2,-0.1,6.3,-1.1c23.5,-11,46.9,-21.9,70.4,-32.8c29.2,-13.5,58.5,-26.9,87.8,-40.3c18.3,-8.4,36.6,-16.6,54.8,-25  c1.6,-0.7,3.1,-1.9,4.6,-2.8c2,-2.9,1.1,-6.2,0.9,-9.2c-0.3,-4.7,1.9,-5.5,5.7,-4.7c10.8,2.2,21.6,4.6,32.5,6.9C672,778.7,672,780.3,672,782z  "
+        android:fillColor="#E6E4E4"/>
+    <path
+        android:pathData="M350,872c-9.9,-1.1,-19.4,-3.9,-29.1,-6C277,856,233.1,846,189.2,836c-39.4,-9,-78.7,-18,-118.1,-27  c-2,-0.4,-4.5,-0.2,-5.2,-3c0,-85.7,0,-171.4,0.1,-257.1c6.5,0.1,12.7,2.3,19,3.6c26.4,5.4,52.8,10.9,79.2,16.5c25.9,5.4,51.8,11,77.7,16.4  c26.2,5.5,52.5,11,78.7,16.5c30.1,6.3,60.2,12.6,90.3,19c0.3,68.9,0.7,137.8,1,206.7c0.1,17.1,0,34.3,0,51.4c0,5.9,0,6,-5.9,4.9  c-5.7,-1.1,-11.2,-3.2,-17.1,-2.9c0,0,0,0,0,0c-7,-3.1,-14.2,-5.4,-22,-5c0,0,0,0,0,0c-3.9,-2.9,-8.4,-2.9,-13,-3c0,0,0,0,0,0  C352.9,871.5,351.4,872.1,350,872z M177,687c0,3.2,0.1,6.3,0,9.5c-0.1,2.7,0.7,4,3.8,4.6c29.7,5.8,59.3,11.7,89,17.7  c2.4,0.5,4.7,0.1,4.9,-2.6c0.4,-3.7,1.2,-7.6,-0.6,-11.2c1,-3,1.2,-5.3,-3,-6c-29.6,-5.4,-59.2,-10.8,-88.7,-16.5C177.7,681.6,176.5,682.8,177,687  z"
+        android:fillColor="#E5E5E5"/>
+    <path
+        android:pathData="M411,621c-30.1,-6.3,-60.2,-12.6,-90.3,-19c-26.2,-5.5,-52.5,-11,-78.7,-16.5c-25.9,-5.5,-51.8,-11,-77.7,-16.4  c-26.4,-5.5,-52.8,-11.1,-79.2,-16.5c-6.3,-1.3,-12.5,-3.5,-19,-3.6c0,-23.6,0,-47.3,0,-70.9c12,-0.9,23.2,3.3,34.7,5.5c24.5,4.6,49,9.7,73.5,14.6  c1,0.2,1.9,0.6,2.8,1c0,3.3,0.7,6.7,0.7,9.9c0,5.6,2.4,7.5,7.5,8.4c15.3,2.7,30.5,5.8,45.8,8.7c12,2.3,24,4.4,36.1,6.6  c1.9,0.3,4.8,1.5,4.7,-1.4c-0.2,-4.6,1.7,-8.2,3.3,-12.1c10.2,0.9,20,3.8,30.1,5.5c11.7,2,23.1,5.3,34.9,6.5  c14.5,-3.2,28.1,-8.9,42.2,-13.2c8.1,-2.5,16.2,-5,24.1,-7.8c4.1,-1.5,4.8,-0.1,4.7,3.7C411,549.4,411,585.2,411,621z"
+        android:fillColor="#D9D9D9"/>
+    <path
+        android:pathData="M412,307c-6.9,3.5,-13.6,-0.2,-20.1,-1.3c-28.2,-5,-56.2,-10.4,-84.3,-15.8c-21.8,-4.1,-43.5,-8.5,-65.2,-12.6  c-27.3,-5.2,-54.6,-10.1,-81.9,-15.3c-22.1,-4.2,-44.1,-8.5,-66.2,-12.8c-7,-1.3,-13.9,-2.9,-21,-3.9c-4.1,-0.6,-5.2,-3,-5.3,-6.5c0,-1.2,0,-2.3,0,-3.5  c0,-51.1,0,-102.3,0,-153.4c0,-5,-0.3,-10,1,-14.9c24.1,4.3,48.1,8.6,72.2,12.8c24.9,4.4,49.8,8.7,74.6,13.1c31.9,5.6,63.7,11.3,95.6,16.9  c30.2,5.3,60.5,10.6,90.7,16c2.6,0.5,5.2,0.8,7.8,1.2c0,1.3,0.7,2,2,2C412,188.3,412,247.7,412,307z M409,217.4c0,-25.5,0,-51,0,-76.5  c0,-10.9,0.1,-11.2,-10.7,-13.2c-23.4,-4.4,-46.8,-8.5,-70.3,-12.6c-24.1,-4.3,-48.2,-8.4,-72.3,-12.6c-17.7,-3.1,-35.5,-6.3,-53.2,-9.4  c-22.1,-3.9,-44.3,-7.6,-66.4,-11.5c-20,-3.5,-40,-7.1,-60.1,-10.5c-6,-1,-6.1,-0.8,-6.1,5.6c0,53,0,105.9,0,158.9c0,1,0,2,0,3  c0.2,2.6,1,4.1,4,4.6c10.1,1.7,20.1,3.9,30.2,5.8c27.3,5.1,54.6,10.1,81.9,15.2c22.1,4.2,44.1,8.6,66.2,12.8  c27.3,5.2,54.6,10.2,81.9,15.3c22.7,4.3,45.5,8.6,68.2,12.8c6.5,1.2,6.5,1.1,6.5,-5.7C409,272,409,244.7,409,217.4z"
+        android:fillColor="#E8E8E8"/>
+    <path
+        android:pathData="M412,129c-1.3,0,-2,-0.7,-2,-2C411.3,127,412,127.7,412,129z"
+        android:fillColor="#EAEAEA"/>
+    <path
+        android:pathData="M65.8,248.3c10.1,0.7,19.8,3.5,29.7,5.2c19,3.3,38,7,57,10.6c15.9,3,31.8,6.2,47.8,9.2  c24,4.6,48,9.1,72,13.7c14.6,2.8,29.3,5.5,43.8,8.5c22.5,4.6,45.3,8.4,67.9,12.6c4.1,0.8,8.2,1.8,12.4,3.8  c-6.4,2.1,-12.8,4.3,-19.2,6.4c-9.5,3.1,-19.1,5.8,-28.4,9.4c-6.7,2.6,-13.3,2.5,-20,1.3c-41.4,-7.6,-82.8,-15.2,-124.2,-22.8  c-32.6,-6,-65.2,-11.9,-97.8,-17.8c-13.7,-2.5,-27.4,-4.9,-41.1,-7.4C65.9,270,65.9,259.1,65.8,248.3z"
+        android:fillColor="#E6A3A3"/>
+    <path
+        android:pathData="M275,519c-1.5,3.9,-3.5,7.5,-3.3,12.1c0.1,2.9,-2.8,1.7,-4.7,1.4c-12,-2.2,-24.1,-4.3,-36.1,-6.6  c-15.3,-2.9,-30.5,-6,-45.8,-8.7c-5.1,-0.9,-7.5,-2.8,-7.5,-8.4c0,-3.2,-0.8,-6.5,-0.7,-9.9c12.2,2.3,24.4,4.6,36.5,7c19.8,3.9,39.5,8,59.3,12  C273.6,518.2,274.3,518.7,275,519z"
+        android:fillColor="#CBCBCA"/>
+    <path
+        android:pathData="M202.9,345.9c0,3.3,-0.1,6.7,0.1,10c0.2,3.9,-1.4,4.3,-4.9,3.7c-29.9,-5.6,-59.8,-11.2,-89.8,-16.5  c-3,-0.5,-3.5,-1.7,-3.4,-4.2c0.1,-3.3,0.1,-6.7,0.1,-10c21.7,3.9,43.4,7.9,65.2,11.6C181.1,342.4,191.8,345.3,202.9,345.9z"
+        android:fillColor="#CFCFCE"/>
+    <path
+        android:pathData="M65.8,248.3c0,10.9,0,21.7,0,32.6c-17.9,-3.1,-35.8,-6.2,-53.7,-9.3c0,-0.4,0,-0.7,0,-1.1  C30.1,263,48,255.6,65.8,248.3z"
+        android:fillColor="#E57474"/>
+    <path
+        android:pathData="M202.9,345.9c-11.1,-0.6,-21.8,-3.5,-32.6,-5.4c-21.8,-3.7,-43.5,-7.7,-65.2,-11.6c-0.6,-3.8,0.6,-5.6,4.8,-4.8  c28.3,5,56.6,9.9,84.9,14.9c1.1,0.2,2.3,0.8,3.4,0.8C202.8,339.6,203.4,342.3,202.9,345.9z"
+        android:fillColor="#BDBDBD"/>
+    <path
+        android:pathData="M367,876c7.8,-0.4,15,1.9,22,5C381.4,880.3,374.2,878.4,367,876z"
+        android:fillColor="#EFEFEE"/>
+    <path
+        android:pathData="M354,873c4.5,0.1,9.1,0.1,13,3C362.5,875.6,358.1,875,354,873z"
+        android:fillColor="#EFEFEE"/>
+    <path
+        android:pathData="M350,872c1.4,0.1,3,-0.5,4,1C352.6,872.9,351,873.5,350,872z"
+        android:fillColor="#EFEFEE"/>
+    <path
+        android:pathData="M274.1,705c1.9,3.6,1,7.5,0.6,11.2c-0.3,2.8,-2.5,3.1,-4.9,2.6c-29.7,-5.9,-59.3,-11.9,-89,-17.7  c-3.1,-0.6,-3.9,-1.9,-3.8,-4.6c0.1,-3.2,0,-6.3,0,-9.5c1.2,0,2.4,-0.1,3.5,0.1c19.2,3.8,38.4,7.7,57.6,11.4  C250.1,700.8,261.9,703.8,274.1,705z"
+        android:fillColor="#D6D6D5"/>
+    <path
+        android:pathData="M274.1,705c-12.1,-1.2,-24,-4.2,-35.9,-6.5c-19.2,-3.7,-38.4,-7.6,-57.6,-11.4c-1.1,-0.2,-2.3,-0.1,-3.5,-0.1  c-0.5,-4.2,0.7,-5.4,5.3,-4.5c29.5,5.7,59.1,11.1,88.7,16.5C275.3,699.7,275.1,702,274.1,705z"
+        android:fillColor="#C9C9C8"/>
+    <path
+        android:pathData="M409,217.4c0,27.3,0,54.6,0,82c0,6.8,0,6.9,-6.5,5.7c-22.7,-4.2,-45.5,-8.6,-68.2,-12.8  c-27.3,-5.1,-54.6,-10.1,-81.9,-15.3c-22.1,-4.2,-44.1,-8.6,-66.2,-12.8c-27.3,-5.2,-54.6,-10.1,-81.9,-15.2c-10.1,-1.9,-20.1,-4.1,-30.2,-5.8  c-3,-0.5,-3.9,-2.1,-4,-4.6c-0.1,-1,0,-2,0,-3c0,-53,0,-105.9,0,-158.9c0,-6.4,0,-6.6,6.1,-5.6c20,3.4,40,7,60.1,10.5c22.1,3.9,44.3,7.6,66.4,11.5  c17.7,3.1,35.5,6.3,53.2,9.4c24.1,4.2,48.2,8.4,72.3,12.6c23.4,4.1,46.9,8.2,70.3,12.6c10.8,2,10.7,2.3,10.7,13.2  C409,166.4,409,191.9,409,217.4z M283.9,146.9c0.4,-3.2,-0.2,-5.3,-4,-6c-29.7,-5,-59.4,-9.9,-89,-15.3c-4.8,-0.9,-5.2,0.7,-4.8,4.4  c0,3.5,-0.1,7,0,10.5c0,1.3,-0.4,3.2,1.4,3.3c2.9,0.1,5.3,1.8,8.1,2.3c13.8,2.4,27.6,4.9,41.4,7.4c13.3,2.4,26.5,5.1,39.8,7.4  c6.6,1.2,7.3,0.4,7.3,-6.5C284,151.9,283.9,149.4,283.9,146.9z"
+        android:fillColor="#E8E7E7"/>
+    <path
+        android:pathData="M283.9,146.9c0,2.5,0.1,5,0.1,7.5c0,6.9,-0.7,7.7,-7.3,6.5c-13.3,-2.4,-26.5,-5,-39.8,-7.4  c-13.8,-2.5,-27.6,-5.1,-41.4,-7.4c-2.8,-0.5,-5.2,-2.2,-8.1,-2.3c-1.8,-0.1,-1.4,-2,-1.4,-3.3c0,-3.5,0,-7,0,-10.5c1.9,0.3,3.9,0.7,5.8,1  c21.6,4,43.1,8.1,64.7,11.8C265.6,144.4,274.5,147.1,283.9,146.9z"
+        android:fillColor="#CFCFCE"/>
+    <path
+        android:pathData="M283.9,146.9c-9.3,0.2,-18.3,-2.5,-27.3,-4.1c-21.6,-3.7,-43.1,-7.8,-64.7,-11.8c-1.9,-0.4,-3.9,-0.7,-5.8,-1  c-0.4,-3.6,-0.1,-5.2,4.8,-4.4c29.6,5.4,59.3,10.4,89,15.3C283.7,141.6,284.3,143.7,283.9,146.9z"
+        android:fillColor="#BDBDBD"/>
+</vector>
diff --git a/packages/DocumentsUI/res/drawable/hourglass.xml b/packages/DocumentsUI/res/drawable/hourglass.xml
new file mode 100644
index 0000000..9b8d0e2
--- /dev/null
+++ b/packages/DocumentsUI/res/drawable/hourglass.xml
@@ -0,0 +1,168 @@
+<!--
+Copyright (C) 2016 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:width="421dp"
+        android:height="909dp"
+        android:viewportWidth="421.0"
+        android:viewportHeight="909.0">
+    <path
+        android:pathData="M36,122.9c-2.8,-2.6,-5.7,-5.1,-8.3,-7.8c-5.6,-6,-9.2,-12.9,-8.8,-21.5c0.3,-7.5,0.6,-15,-0.1,-22.5   c-1.2,-14.1,5.5,-23.9,16,-31.9c16.7,-12.8,36.1,-19.6,56.1,-25.1c23.8,-6.5,48,-10.2,72.5,-12.3C168.6,1.3,174,2.2,179,0   c19.3,0,38.7,0,58,0c6,2.1,12.4,1.3,18.6,1.8c30.2,2.7,59.9,7.6,88.5,17.5c16.5,5.7,32.6,12.6,45.2,25.4c6.5,6.6,10.3,14,9.8,23.6   c-0.4,7.8,-0.5,15.7,0,23.4c0.6,10.3,-3.4,18.4,-10.6,25.2c-2.2,2,-4.4,4,-6.6,6c-3,2,-6.1,4,-9.1,5.9c-9.2,4.5,-18.5,9,-28.2,12.3   c-42.4,14.5,-86.3,18.8,-130.8,19.6c-10.9,0.2,-21.9,-0.4,-32.9,-0.7c-4.6,-0.4,-9.2,-0.8,-13.9,-1.1c-18.9,-1.1,-37.5,-3.9,-56,-7.6   c-15.3,-3.1,-30.2,-7.6,-44.9,-12.6c-7.6,-3.7,-15.3,-7.4,-22.9,-11.1C41.1,125.8,38.6,124.3,36,122.9z M41,72c2.9,6.9,7.1,12.6,13.1,17.2   c13,10,27.9,15.8,43.4,20.6c28.2,8.8,57.2,12.8,86.5,14c31.8,1.2,63.7,0.8,95.3,-4.6c25.3,-4.4,50.1,-10,72.9,-22.2   c10.8,-5.8,20,-13.1,24.7,-24.9c2.3,-11,-2.3,-19.5,-10.2,-26.4c-10.5,-9.2,-23.1,-14.9,-36.2,-19.6C295.2,13.4,258.4,9.7,221.2,8.1   c-11.1,-0.5,-22.2,0,-33.4,0.6c-21.4,1,-42.6,3.1,-63.6,7.3c-22.2,4.5,-44.1,10.3,-63.4,22.6C48.9,46.3,38.4,55.4,41,72z"
+        android:fillColor="#9F9F9F"/>
+    <path
+        android:pathData="M0,829c3.7,-2.8,4.7,-7.6,7.8,-10.9c2.6,-2.8,4.9,-5.7,9.2,-7.6c0,3.4,-0.1,6.5,0,9.5c0,1.5,-0.7,3.5,1.7,4   c0.4,3.3,1.4,6.4,2.9,9.4c3.8,7.7,10,13,16.8,17.9c9.2,6.7,19.7,10.8,29.8,15.5c-0.7,2.4,1.3,0.7,1.8,1.1l0,0c1.5,2.1,3.7,2.2,6,2   l0,0c0.8,0.6,1.5,1.4,2.4,1.7c9.5,2.7,18.9,5.8,28.7,7.4c3.6,0.6,7,3.5,10.9,1.1c2.4,0.4,4.8,0.8,7.1,1.2c0.2,1.5,1.3,1.6,2.5,1.8   c6.6,0.9,13.3,2.4,19.9,2.8c5.1,0.3,10.3,2.9,15.4,0.3c0.4,0,0.8,0.1,1.1,0.1c0.3,2.2,2.1,1.8,3.5,1.8c3.8,0,7.6,0,11.5,0   c1.1,1.4,2.7,1,4.1,1c17.2,0,34.5,0,51.7,0c1.4,0,3,0.4,4.1,-1c3.8,0,7.6,0,11.5,0c1.4,0,3.2,0.4,3.5,-1.8c9.4,-1,18.7,-2.1,28.1,-3.1   c6,1.3,11.6,0.4,16.9,-2.8c21.2,-4.2,42.1,-9.3,61.8,-18.4c15.8,-7.3,30.8,-15.8,38,-33.1c2.4,-2,1.9,-4.8,2.2,-7.4c0.3,-3,0,-6,0.1,-9   c0,-1,-0.3,-2.1,0.7,-2.7c1.1,-0.7,1.7,0.5,2.5,1c7.2,5,12.1,11.7,14.8,20c0.4,1.2,0.6,2.2,2.1,2.3c0,3.3,0,6.7,0,10   c-1.5,0,-1.8,1.1,-2.2,2.2c-3.8,10.2,-11.2,17.5,-20.1,23.3c-20.8,13.6,-44.1,21.2,-68.1,26.7c-29.2,6.7,-58.7,11,-88.7,11.7   c-1.6,0,-3.5,-0.5,-3.9,2c-18.7,0,-37.3,0,-56,0c-0.3,-2.5,-2.3,-1.9,-3.9,-2c-5.6,-0.1,-11.2,-0.5,-16.9,-0.8c-18.5,-1.2,-36.8,-3.8,-55,-7.3   C79.9,893.9,54,887,30.2,874C19,867.9,8.5,860.9,2.5,849C2,848,1.4,847,0,847C0,841,0,835,0,829z"
+        android:fillColor="#E6E4E4"/>
+    <path
+        android:pathData="M372.9,128.9c3,-2,6.1,-4,9.1,-5.9c-0.2,2.7,0.2,5.4,1,8c-1.5,1.6,-0.3,1.8,1,2c0.3,1,0.7,2,1,3   c-1.5,1.6,-0.3,1.8,1,2c0.7,2,1.3,4,2,6c-1.5,1.6,-0.3,1.8,1,2c0.3,1.7,0.7,3.3,1,5c-1,2.3,-0.6,4.1,2,5c4.9,23.8,9,47.6,8,72   c-3.5,1.5,-2.1,3.8,-1,6c-1,6,-2,12,-3,18c-1.3,1,-1.3,2,0,3c0,0.7,0,1.3,0,2c-2.1,0.4,-2.6,1.3,-1,3c0,0.3,0,0.7,0,1   c-1.3,0.2,-2.5,0.4,-1,2c0.4,2.1,-0.7,4,-1,6c-1.3,0.2,-2.5,0.4,-1,2c-3.7,9.3,-7.3,18.7,-11,28c-2.7,1.2,-4.2,2.9,-3,6   c-3.4,6.9,-7.8,13.3,-12.2,19.5c-6.1,8.6,-12.4,17.3,-19.4,25.2c-7.3,8.3,-15.5,15.8,-23.9,23c-11.9,10.3,-24.9,19.3,-38.1,27.7   c-12.2,7.8,-25.4,14.1,-38.4,20.5c-12.1,6,-18.5,15.8,-21,28.6c-1.5,7.8,-0.5,15.4,2,22.8c1.2,3.5,3.7,6.1,5.6,9.2   c5.4,8.6,14.8,10.5,22.6,15c15.3,9,30.8,17.7,45.3,28.1c14.4,10.4,28.2,21.5,40.5,34.1c10.1,10.4,18.5,22.2,26.8,34.3   c6.5,9.5,11.3,19.6,16.1,29.8c-1.5,1.6,-0.3,1.8,1,2c0.7,2,1.3,4,2,6c-1,2.3,-0.6,4.1,2,5c0.7,1.2,1.2,2.5,1,4c-1,2.3,-0.6,4.1,2,5   c1.2,12.3,4.6,24.1,5.7,36.5c0.8,8.4,1.4,16.8,1,25.1c-0.3,5.9,-1.1,12,-1.9,18c-1.2,8.7,-2.3,17.4,-4.2,25.9   c-1.5,6.6,-3.7,13.1,-5.6,19.6c-1.8,3.1,-2.9,6.5,-3.9,9.9c-3.4,6.4,-5.6,13.6,-11.9,18.2c-0.1,-3.8,1.6,-7.1,3,-10.4   c8.7,-20.9,13,-42.8,14.7,-65.1c1,-12.9,0.2,-25.8,-1.7,-38.7c-2.7,-18.5,-7.8,-36.2,-15.8,-53.1c-7.3,-15.4,-16.8,-29.3,-27.7,-42.4   c-2.7,-3.2,-6.3,-5.7,-9.6,-8.6c0.4,0.8,0.7,1.4,1,1.9c0.7,1.1,1.5,2.2,2.3,3.3c16.5,21.5,28.5,45.2,34.2,71.7c0.7,3.3,3.1,6.9,0.3,10.4   c-1,-1.9,-2.1,-3.7,-3.1,-5.6c-3.3,-6.3,-6.1,-12.9,-11.7,-17.6c-0.4,-0.9,-0.8,-1.8,-1.3,-2.6c-4,-6.3,-10.4,-10.4,-14.8,-16.2c0,-5.4,-2.7,-9.9,-4.8,-14.5   c-8.4,-18.6,-20.4,-34.9,-32.9,-50.8c-8.4,-10.8,-15.5,-22.8,-28.7,-28.8c-5.3,-2.4,-10,-6,-15.1,-8.6c-5.1,-2.6,-9.9,-6.4,-16.3,-5.2   c-5.2,1,-10.4,2.1,-15.3,4.1c-29.3,11.9,-48.4,34.1,-61.8,61.9c-0.3,0.3,-0.7,0.6,-1,1c-7.1,0.8,-13.9,2.9,-20.7,5.1   c-32.6,10.6,-61,27.4,-82.3,54.9c-9.2,11.6,-15.4,24.7,-18.9,39c-1.5,-1.1,-1.1,-2.7,-1.1,-4.1c-0.1,-9.6,0.3,-19.2,1.8,-28.7   c3.7,-22.6,11.8,-43.5,24,-62.8c12.6,-20,28.6,-36.9,47,-51.7c21.3,-17.3,44.6,-31.3,69.3,-42.9c14.5,-6.8,20,-18.8,21.8,-33.1   c1.8,-13.3,-4.9,-24.1,-12.2,-34.4c-3.6,-5,-7.4,-9.8,-13.2,-12.5c-5.8,-2.8,-11.6,-5.7,-17.4,-8.7c-22,-11.6,-42.6,-25.1,-61.1,-41.7   c-20.7,-18.6,-37.9,-40,-48.8,-65.9c-6.7,-15.7,-10.9,-32.1,-12,-49c-1.8,-27.1,2.1,-53.6,11.7,-79.1c3.8,-10.1,7.1,-20.4,13.3,-29.4   c14.7,5,29.6,9.5,44.9,12.6c18.5,3.7,37.2,6.5,56,7.6c4.6,0.3,9.3,0.7,13.9,1.1c0.2,3.6,-1.5,6.8,-2.6,10   c-11.9,33.6,-17.8,68.2,-17.2,103.8c0.2,9.7,1.3,19.4,2.7,29.1c3.8,24.6,11.4,47.7,26,68.1c12.2,17.1,28.4,28.6,49,33.4   c4.7,1.1,9.5,2.2,14.5,-0.5c18.7,-10.2,36.8,-21.2,53.7,-34.2c15.4,-11.9,29.4,-25.3,41.5,-40.5c12.9,-16.2,23.4,-33.8,30.4,-53.4   c6.4,-17.6,10.3,-35.6,10.9,-54.3c0.5,-15.9,0.2,-31.9,-3,-47.6C383.8,158.7,380.1,143.3,372.9,128.9z"
+        android:fillColor="#EDECEC"/>
+    <path
+        android:pathData="M383,780c1.1,-3.4,2.1,-6.8,3.9,-9.9c7.8,7.1,12.8,15.2,12.2,26.4c-0.6,10.7,-0.3,21.5,-0.5,32.3   c-7.2,17.3,-22.2,25.8,-38,33.1c-19.7,9.1,-40.6,14.1,-61.8,18.4c-5.6,0.9,-11.3,1.9,-16.9,2.8c-9.4,1,-18.7,2.1,-28.1,3.1   c-5,0.3,-9.9,0.7,-14.9,1c-20,1.2,-40,0.9,-60,0c-5,-0.3,-9.9,-0.7,-14.9,-1c-0.4,0,-0.8,-0.1,-1.1,-0.1c-12.6,-1.6,-25.2,-3.2,-37.9,-4.8   c-2.4,-0.4,-4.8,-0.8,-7.1,-1.2c-2.6,-0.6,-5.1,-1.3,-7.7,-1.8c-11.6,-2,-22.6,-6.3,-34.2,-8.4c0,0,0,0,0,0c-1.7,-1.6,-3.7,-2.2,-6,-2c0,0,0,0,0,0   c-0.2,-1,-1.1,-0.9,-1.8,-1.1c-10.2,-4.7,-20.6,-8.8,-29.8,-15.5c-6.8,-4.9,-13,-10.2,-16.8,-17.9c-1.5,-3,-2.4,-6.1,-2.9,-9.4   c0.1,-10.3,0,-20.6,0.2,-30.9c0.1,-8.3,3.5,-15,10,-20.2c2,3.5,3.4,7.1,4.1,11c-1.1,0.8,-1,2,-1.1,3.1c-0.6,8.2,2.9,14.9,8.3,20.6   c8.9,9.6,20.4,15.4,32.3,20.2c17.9,7.2,36.5,11.9,55.4,15.4c20.1,3.7,40.3,5.7,60.6,6.5c21.4,0.8,42.8,0.4,64.2,-1.6   c19.8,-1.9,39.4,-4.6,58.7,-9.3c19.9,-4.8,39.3,-11.2,56.4,-22.9c7.8,-5.3,15.2,-11.3,17.4,-21C386.4,790.1,388.3,784.3,383,780z"
+        android:fillColor="#9F9F9F"/>
+    <path
+        android:pathData="M378,305c-1.2,-3.1,0.3,-4.8,3,-6C380.6,301.3,379.7,303.3,378,305z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M399,234c-1.1,-2.2,-2.5,-4.5,1,-6C399.7,230,400.8,232.2,399,234z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M392,156c-2.6,-0.9,-3,-2.7,-2,-5C391.4,152.4,391.6,154.2,392,156z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M389,636c-2.6,-0.9,-3,-2.7,-2,-5C388.4,632.4,388.6,634.2,389,636z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M392,645c-2.6,-0.9,-3,-2.7,-2,-5C391.4,641.4,391.6,643.2,392,645z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M396,255c-1.3,-1,-1.3,-2,0,-3C397.3,253,397.3,254,396,255z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M395,260c-1.6,-1.7,-1.1,-2.6,1,-3C395.7,258,395.3,259,395,260z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M384,133c-1.3,-0.2,-2.5,-0.4,-1,-2C383.8,131.4,384,132.2,384,133z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M385,625c-1.3,-0.2,-2.5,-0.4,-1,-2C384.8,623.4,385,624.2,385,625z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M392,271c-1.5,-1.6,-0.3,-1.8,1,-2C393,269.8,392.8,270.6,392,271z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M394,263c-1.5,-1.6,-0.3,-1.8,1,-2C395,261.8,394.8,262.6,394,263z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M386,138c-1.3,-0.2,-2.5,-0.4,-1,-2C385.8,136.4,386,137.2,386,138z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M389,146c-1.3,-0.2,-2.5,-0.4,-1,-2C388.8,144.4,389,145.2,389,146z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M33.1,783.9c-0.8,-3.9,-2.2,-7.6,-4.1,-11c-3.7,-11.7,-7.2,-23.5,-9.2,-35.5c-1.3,-7.6,-1.9,-15.4,-2.7,-23.2   c-0.6,-6.2,-0.9,-12.4,-1,-18.5c-0.2,-7.9,1.9,-15.7,2.3,-23.4c0.5,-10.1,2.9,-19.5,5.5,-29c6.3,-23.1,17.3,-43.9,31.5,-62.8   c23.4,-31.1,53.3,-54.7,86.9,-74.1c10.1,-5.8,20.3,-11.6,30.9,-16.2c11.7,-5.2,16.3,-14.9,18.8,-26.3c3.2,-14.9,-2.8,-26.6,-12.7,-37.1   c-1.8,-1.9,-3.9,-3.1,-6.2,-4.2c-23.7,-11.4,-46.4,-24.4,-67.2,-40.7c-16.2,-12.6,-31.3,-26.5,-44.2,-42.6c-16.2,-20.3,-28.8,-42.5,-36.2,-67.5   c-3.1,-10.6,-5.4,-21.3,-6.2,-32.4c-0.7,-9.6,-3.2,-19.3,-2,-28.8c0.8,-6.6,1.5,-13.4,1.9,-20c1,-15.2,4.9,-29.6,9.2,-44c1.7,-5.7,4,-11.3,6.4,-16.8   c0.9,-2.2,1.3,-4.4,1.3,-6.8c2.6,1.4,5.1,2.9,7.2,4.9c-0.6,0.8,-1.4,1.4,-1.8,2.3c-11.2,26.2,-16.2,53.8,-17.4,82.2   c-0.4,8.8,1,17.5,1.9,26.2c2,19.7,7.4,38.4,15.6,56.3c17.9,39.2,46.6,69.1,81.3,93.6c18.5,13.1,38.1,24.3,58.5,34.1   c3.3,1.6,6,3.7,8.1,6.5c8,10.6,12.6,22.1,9.6,35.7c-2.1,9.4,-6.5,17.9,-14.8,22.7c-8.2,4.7,-16.9,8.5,-25.3,12.9   c-22.5,12,-43.8,25.8,-62.6,43c-21.9,19.9,-40.8,42.1,-53.7,69.2C26.3,646.5,20.6,682,24.1,719c1.8,18.7,7,36.7,12.7,54.6   c5.9,18.7,18.2,30.9,35.3,38.9c15.4,7.2,31.5,12.2,48.1,15.8c1.6,0.4,4.3,-0.3,4.8,2.6c-18,-2.8,-35.4,-7.5,-52.3,-14.5   c-12.2,-5.1,-23.4,-11.4,-32.4,-21.4C37.2,791.7,36.5,787,33.1,783.9z"
+        android:fillColor="#EDECEC"/>
+    <path
+        android:pathData="M372.9,128.9c7.2,14.3,10.9,29.8,14.1,45.4c3.2,15.7,3.5,31.6,3,47.6c-0.6,18.7,-4.6,36.7,-10.9,54.3   c-7.1,19.6,-17.6,37.2,-30.4,53.4c-12.1,15.2,-26.1,28.6,-41.5,40.5c-16.9,13,-35,24,-53.7,34.2c-5,2.7,-9.8,1.6,-14.5,0.5   c-20.6,-4.8,-36.8,-16.3,-49,-33.4c-14.6,-20.4,-22.3,-43.5,-26,-68.1c-1.5,-9.7,-2.6,-19.4,-2.7,-29.1c-0.6,-35.6,5.4,-70.2,17.2,-103.8   c1.2,-3.3,2.8,-6.4,2.6,-10c11,0.2,21.9,0.9,32.9,0.7c44.4,-0.8,88.4,-5.2,130.8,-19.6C354.4,137.9,363.7,133.5,372.9,128.9z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M377,72c-4.6,11.9,-13.9,19.1,-24.7,24.9c-22.9,12.2,-47.6,17.9,-72.9,22.2c-31.6,5.4,-63.5,5.9,-95.3,4.6   c-29.3,-1.1,-58.3,-5.1,-86.5,-14C82.1,105,67.2,99.1,54.2,89.2C48.2,84.6,43.9,78.8,41,72c3.9,-1.8,4.6,-6.2,7.3,-9   c10.3,-10.5,22.9,-16.9,36.5,-21.8c19.7,-7.1,40,-11.5,60.7,-14.5c19.4,-2.8,38.9,-3.9,58.4,-4.5c17.9,-0.6,35.8,0.8,53.6,2.8   c15,1.6,29.9,3.5,44.5,7.1c20,4.9,39.7,10.7,57.2,22.1C366.5,58.8,371.5,65.5,377,72z"
+        android:fillColor="#8D8E8E"/>
+    <path
+        android:pathData="M125,831c-0.4,-2.9,-3.2,-2.3,-4.8,-2.6c-16.6,-3.7,-32.7,-8.7,-48.1,-15.8c-17.1,-8,-29.4,-20.2,-35.3,-38.9   c-5.7,-17.9,-10.9,-35.9,-12.7,-54.6c-3.6,-37.1,2.1,-72.5,18.4,-106.4c13,-27.1,31.9,-49.3,53.7,-69.2c18.8,-17.1,40.2,-30.9,62.6,-43   c8.4,-4.5,17.1,-8.2,25.3,-12.9c8.4,-4.8,12.7,-13.3,14.8,-22.7c3.1,-13.6,-1.6,-25.1,-9.6,-35.7c-2.1,-2.8,-4.8,-4.9,-8.1,-6.5   c-20.4,-9.9,-40,-21.1,-58.5,-34.1c-34.7,-24.6,-63.4,-54.5,-81.3,-93.6c-8.2,-17.9,-13.6,-36.6,-15.6,-56.3c-0.9,-8.7,-2.3,-17.5,-1.9,-26.2   c1.2,-28.3,6.2,-55.9,17.4,-82.2c0.4,-0.9,1.2,-1.5,1.8,-2.3c7.6,3.7,15.3,7.4,22.9,11.1c-6.2,9,-9.5,19.3,-13.3,29.4   c-9.6,25.5,-13.5,52,-11.7,79.1c1.1,16.9,5.4,33.3,12,49c11,25.9,28.1,47.4,48.8,65.9c18.5,16.6,39.1,30.2,61.1,41.7   c5.7,3,11.5,5.9,17.4,8.7c5.8,2.8,9.7,7.6,13.2,12.5c7.3,10.3,14,21.1,12.2,34.4c-1.9,14.3,-7.4,26.3,-21.8,33.1   c-24.7,11.6,-48,25.7,-69.3,42.9c-18.3,14.9,-34.3,31.7,-47,51.7c-12.2,19.3,-20.3,40.2,-24,62.8c-1.6,9.6,-2,19.1,-1.8,28.7   c0,1.4,-0.4,3.1,1.1,4.1c-0.6,14.9,0.1,29.8,3,44.5c4.2,21.4,9.1,42.6,26,58.4c-0.2,2.3,0.9,4.1,2.2,5.9c8.8,12.3,22,18.6,35.3,24.1   c26.4,10.9,54.2,16.1,82.4,19.1c1.7,0.2,3,0.4,3,2.4c-4.5,0.7,-9,0.9,-13.4,0.5c-13.2,-1,-26.5,-1.9,-39.6,-4.1c-0.6,-2.4,-1.7,-2.3,-3.1,-0.6   c-1.3,-0.1,-2.6,-0.3,-3.9,-0.4c-0.6,-2.3,-1.6,-2.4,-3.1,-0.7c-0.6,-0.1,-1.3,-0.2,-1.9,-0.3c-0.6,-2.4,-1.7,-2.4,-3.1,-0.6   C126.2,831.2,125.6,831.1,125,831z"
+        android:fillColor="#E8E8E7"/>
+    <path
+        android:pathData="M377,72c-5.4,-6.4,-10.5,-13.2,-17.7,-17.9C341.7,42.7,322.1,36.9,302,32c-14.6,-3.6,-29.5,-5.5,-44.5,-7.1   c-17.8,-1.9,-35.7,-3.3,-53.6,-2.8c-19.5,0.6,-39,1.7,-58.4,4.5c-20.7,3,-41,7.4,-60.7,14.5C71.3,46.1,58.7,52.4,48.4,63   c-2.7,2.8,-3.5,7.2,-7.3,9c-2.6,-16.6,7.9,-25.6,19.8,-33.3c19.3,-12.3,41.2,-18.2,63.4,-22.6c21,-4.2,42.2,-6.3,63.6,-7.3   c11.1,-0.5,22.3,-1,33.4,-0.6c37.2,1.5,74,5.3,109.4,17.9c13.1,4.6,25.6,10.4,36.2,19.6C374.7,52.5,379.3,61,377,72z"
+        android:fillColor="#808080"/>
+    <path
+        android:pathData="M179,887.2c20,0.9,40,1.2,60,0c0,0.3,0,0.5,0,0.8c-1.1,1.4,-2.7,1,-4.1,1c-17.2,0,-34.5,0,-51.7,0   c-1.4,0,-3,0.4,-4.1,-1C179,887.7,179,887.5,179,887.2z"
+        android:fillColor="#F4F3F2"/>
+    <path
+        android:pathData="M76,869.9c11.6,2.2,22.6,6.5,34.2,8.4c2.6,0.4,5.2,1.2,7.7,1.8c-3.9,2.3,-7.3,-0.6,-10.9,-1.1   c-9.8,-1.6,-19.2,-4.7,-28.7,-7.4C77.5,871.3,76.8,870.5,76,869.9z"
+        android:fillColor="#F4F3F2"/>
+    <path
+        android:pathData="M125.1,881.3c12.6,1.6,25.2,3.2,37.9,4.8c-5.2,2.6,-10.3,0,-15.4,-0.3c-6.7,-0.4,-13.3,-1.9,-19.9,-2.8   C126.3,882.9,125.2,882.8,125.1,881.3z"
+        android:fillColor="#F4F3F2"/>
+    <path
+        android:pathData="M282,883.1c5.6,-0.9,11.3,-1.9,16.9,-2.8C293.7,883.4,288.1,884.4,282,883.1z"
+        android:fillColor="#F4F3F2"/>
+    <path
+        android:pathData="M179,887.2c0,0.3,0,0.5,0,0.8c-3.8,0,-7.6,0,-11.5,0c-1.4,0,-3.2,0.4,-3.5,-1.8C169,886.5,174,886.9,179,887.2z"
+        android:fillColor="#FFFFFF"/>
+    <path
+        android:pathData="M239,888c0,-0.3,0,-0.5,0,-0.8c5,-0.3,9.9,-0.7,14.9,-1c-0.3,2.2,-2.1,1.8,-3.5,1.8C246.6,888,242.8,888,239,888z"
+        android:fillColor="#FFFFFF"/>
+    <path
+        android:pathData="M70,867.9c2.3,-0.2,4.3,0.4,6,2C73.8,870.1,71.6,870,70,867.9z"
+        android:fillColor="#F4F3F2"/>
+    <path
+        android:pathData="M68.2,866.8c0.7,0.2,1.6,0.2,1.8,1.1C69.5,867.4,67.6,869.2,68.2,866.8z"
+        android:fillColor="#F4F3F2"/>
+    <path
+        android:pathData="M165,584c0.3,-0.3,0.7,-0.6,1,-1c10.5,-1.3,21,-3.3,31.5,-3.8c20.8,-1.1,41.4,0.7,61.7,5.4   c30.5,7,57.8,20.3,81.8,40.5c4.4,5.9,10.8,10,14.8,16.2c0.5,0.8,0.9,1.7,1.3,2.6c-2.4,4.1,-4.7,8.1,-7.1,12.2c-4,3.7,-6.3,8.9,-11,12   c-1.6,-0.1,-2.8,0.5,-4.1,1.5c-12.6,9.3,-26.7,15.6,-41.5,20.1c-31,9.4,-62.6,13.3,-95.1,11.6c-12.3,-0.6,-24.5,-1.5,-36.5,-3.4   c-4.5,-0.7,-5.3,0.5,-4.8,4.2c-0.5,0,-1,0.1,-1.5,0c-17.8,-3.9,-34.7,-10.3,-50.9,-18.7c-1,-0.5,-1.6,-1.1,-1.6,-2.3c2.3,-0.3,4.1,1.1,6.1,2   c14.2,6.2,28.9,10.6,44.1,13.3c2.5,0.4,3,0.2,2.4,-2.3c-2.5,-9.3,-3.7,-18.7,-4.8,-28.3c-1.5,-13.7,-0.3,-27.1,1.7,-40.5   C154.6,610.8,159.7,597.4,165,584z"
+        android:fillColor="#E57474"/>
+    <path
+        android:pathData="M103,681c0.1,1.1,0.7,1.8,1.6,2.3c16.2,8.4,33.1,14.8,50.9,18.7c0.5,0.1,1,0,1.5,0c0.6,0.4,1.3,0.7,1.9,1.1   c-0.1,2.7,1,5.2,1.9,7.6c6.5,17.8,16.5,33.6,27.5,48.8c12.5,17.1,27.1,32.1,45.1,43.6c6.6,4.2,13.7,7.3,20.5,11   c-15.6,2.8,-31.4,2.5,-47.1,2.4c-9.9,0,-19.9,-0.2,-29.8,-1.1c-18.2,-1.6,-36.2,-3.9,-54,-8.3c-18.1,-4.4,-35.9,-9.5,-51,-21.1   c-16.9,-15.8,-21.8,-37,-26,-58.4c-2.9,-14.7,-3.6,-29.6,-3,-44.5c3.5,-14.4,9.7,-27.4,18.9,-39c3.4,4.8,6.2,10.1,10.3,14.2   c6,6.1,11.6,13,19.7,16.8c0.5,0.8,1.2,1,2.1,1c0,0,0,0,0,0c0.3,0.3,0.7,0.7,1,1c0,0,0,0,0,0c0.3,0.3,0.7,0.7,1,1l0,0   c1,1.3,2.4,1.8,4,2l0,0C100.7,681.2,101.9,681,103,681L103,681z"
+        android:fillColor="#E6A3A3"/>
+    <path
+        android:pathData="M341,625c-24,-20.1,-51.3,-33.5,-81.8,-40.5c-20.3,-4.7,-41,-6.5,-61.7,-5.4c-10.5,0.6,-21,2.5,-31.5,3.8   c13.4,-27.8,32.5,-49.9,61.8,-61.9c4.9,-2,10.1,-3.1,15.3,-4.1c6.3,-1.2,11.2,2.6,16.3,5.2c5.2,2.6,9.9,6.2,15.1,8.6   c13.3,6,20.3,18,28.7,28.8c12.5,16,24.6,32.2,32.9,50.8C338.3,615.2,341,619.7,341,625z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M91.9,675c-8,-3.8,-13.6,-10.7,-19.7,-16.8c-4.1,-4.1,-6.9,-9.4,-10.3,-14.2c21.3,-27.5,49.8,-44.3,82.3,-54.9   c6.8,-2.2,13.6,-4.3,20.7,-5.1c-5.3,13.4,-10.4,26.9,-12.5,41.2c-2,13.4,-3.2,26.8,-1.7,40.5c1.1,9.6,2.3,19,4.8,28.3   c0.7,2.5,0.1,2.7,-2.4,2.3c-15.2,-2.6,-29.9,-7.1,-44.1,-13.3c-2,-0.9,-3.7,-2.3,-6.1,-2c0,0,0,0,0,0c-0.7,-1.2,-1.9,-1,-3,-1c0,0,0,0,0,0   c-0.3,-2.7,-2.4,-1.8,-4,-2c0,0,0,0,0,0c-0.3,-0.3,-0.7,-0.7,-1,-1c0,0,0,0,0,0c-0.3,-0.3,-0.7,-0.7,-1,-1c0,0,0,0,0,0   C93.6,675.2,92.8,675,91.9,675z"
+        android:fillColor="#D86868"/>
+    <path
+        android:pathData="M135,832.8c1.3,0.1,2.6,0.3,3.9,0.4c0.9,0.8,2,0.7,3.1,0.6c13.1,2.2,26.4,3,39.6,4.1   c4.5,0.3,9,0.2,13.4,-0.5c3.1,0.3,6.3,0.7,9.4,0.8c22.6,0.4,45.2,-0.9,67.6,-4.1c23.9,-3.3,47.4,-8.2,69.8,-17.6   c10.7,-4.5,21.4,-9.3,29.3,-18.3l0,0.1c6.2,-4.6,8.5,-11.8,11.9,-18.2c5.2,4.3,3.3,10.1,2.2,15c-2.2,9.7,-9.6,15.7,-17.4,21   c-17.1,11.7,-36.5,18.1,-56.4,22.9c-19.3,4.7,-38.9,7.4,-58.7,9.3c-21.4,2,-42.8,2.5,-64.2,1.6c-20.3,-0.8,-40.5,-2.8,-60.6,-6.5   c-19,-3.5,-37.6,-8.2,-55.4,-15.4c-11.9,-4.8,-23.4,-10.6,-32.3,-20.2c-5.3,-5.8,-8.9,-12.4,-8.3,-20.6c0.1,-1.2,0,-2.4,1.1,-3.1   c3.3,3.1,4.1,7.8,7.2,11.1c9,9.9,20.2,16.3,32.4,21.4c16.8,7,34.3,11.7,52.3,14.5c0.6,0.1,1.2,0.2,1.9,0.3c0.9,0.8,2,0.7,3.1,0.6   c0.6,0.1,1.3,0.2,1.9,0.3C132.8,833,133.9,833,135,832.8z"
+        android:fillColor="#999899"/>
+    <path
+        android:pathData="M371.9,667c2.8,-3.5,0.4,-7.1,-0.3,-10.4c-5.7,-26.6,-17.7,-50.3,-34.2,-71.7c-0.8,-1,-1.5,-2.2,-2.3,-3.3   c-0.3,-0.5,-0.6,-1.1,-1,-1.9c3.4,3,6.9,5.4,9.6,8.6c10.8,13.1,20.4,27,27.7,42.4c8,16.9,13.1,34.6,15.8,53.1   c1.9,12.9,2.6,25.8,1.7,38.7c-1.7,22.4,-6,44.3,-14.7,65.1c-1.4,3.3,-3.1,6.6,-3,10.4c0,0,0,-0.1,0,-0.1c-1.8,-0.3,-3.3,0.4,-4.7,1.2   c-6.3,3.7,-12.6,7.3,-19.4,10.2c-24,10.3,-48.8,14.5,-74.7,9.2c-4.2,-0.9,-9.4,-0.2,-12.4,-4.8c13.8,-2,27.6,-4.4,41.1,-8   c10,-2.7,20,-5.4,29,-10.8c1.5,-0.5,3.2,-0.9,4.6,-1.6c10.6,-5.1,19.5,-12.1,25.3,-22.6c4.7,-3.1,8.2,-10.7,6.9,-15c0.3,-1.4,0.6,-2.9,1,-4.3   c5.4,-16.2,7.5,-33.1,9,-50C378,689.7,375.5,678.3,371.9,667z"
+        android:fillColor="#F1F0F0"/>
+    <path
+        android:pathData="M371.9,667c3.6,11.3,6.1,22.7,5.1,34.6c-1.5,16.9,-3.6,33.8,-9,50c-0.5,1.4,-0.7,2.9,-1,4.3   c-2.3,5,-4.6,10,-6.9,15c-5.8,10.5,-14.7,17.5,-25.3,22.6c-1.5,0.7,-3.1,1.1,-4.6,1.6c-0.3,-2.1,0.3,-3.9,1.1,-5.7   c3.4,-7.4,6.4,-14.9,8.9,-22.6c6,-18.1,10,-36.5,12,-55.6c1.2,-11.6,0.9,-23.2,0.6,-34.8c-0.2,-6.8,-0.7,-13.8,-2.8,-20.5   c2.4,-4.1,4.7,-8.1,7.1,-12.2c5.6,4.7,8.4,11.3,11.7,17.6C369.8,663.3,370.8,665.2,371.9,667z"
+        android:fillColor="#E6A3A3"/>
+    <path
+        android:pathData="M260,814c2.9,4.6,8.1,3.9,12.4,4.8c25.9,5.3,50.7,1.1,74.7,-9.2c6.7,-2.9,13.1,-6.4,19.4,-10.2   c1.4,-0.9,2.9,-1.6,4.7,-1.2c-7.9,9.1,-18.6,13.8,-29.3,18.3c-22.3,9.4,-45.9,14.3,-69.8,17.6c-22.4,3.1,-45,4.4,-67.6,4.1   c-3.1,-0.1,-6.3,-0.5,-9.4,-0.8c-0.1,-2,-1.4,-2.2,-3,-2.4c-28.3,-3,-56,-8.2,-82.4,-19.1c-13.4,-5.5,-26.5,-11.8,-35.3,-24.1c-1.3,-1.8,-2.4,-3.6,-2.2,-5.9   c15.1,11.6,32.9,16.7,51,21.1c17.7,4.4,35.8,6.6,54,8.3c10,0.9,20,1.1,29.8,1.1c15.7,0.1,31.5,0.4,47.1,-2.4   C256,814,258,814,260,814z"
+        android:fillColor="#EDECEC"/>
+    <path
+        android:pathData="M130,831.9c-1.1,0.1,-2.2,0.2,-3.1,-0.6C128.3,829.5,129.4,829.5,130,831.9z"
+        android:fillColor="#EDECEC"/>
+    <path
+        android:pathData="M135,832.8c-1.1,0.1,-2.2,0.2,-3.1,-0.7C133.4,830.5,134.4,830.6,135,832.8z"
+        android:fillColor="#EDECEC"/>
+    <path
+        android:pathData="M142,833.8c-1.1,0.1,-2.2,0.2,-3.1,-0.6C140.3,831.5,141.4,831.5,142,833.8z"
+        android:fillColor="#EDECEC"/>
+    <path
+        android:pathData="M260,814c-2,0,-4,0,-6,0c-6.8,-3.7,-13.9,-6.8,-20.5,-11c-18,-11.5,-32.7,-26.4,-45.1,-43.6c-11,-15.2,-21,-31,-27.5,-48.8   c-0.9,-2.5,-2,-4.9,-1.9,-7.7c0.7,0,1.4,-0.1,2,0.1c16.2,4.6,33.1,5.3,49.6,5.3c10,0,20.1,-0.2,30.2,-1.3c19.5,-2,38.7,-5.3,57,-12.4   c15.6,-6,30.1,-13.9,41.3,-26.9c4.7,-3.1,7,-8.3,11,-12c2.1,6.7,2.6,13.7,2.8,20.5c0.3,11.6,0.6,23.1,-0.6,34.8c-2,19,-6,37.5,-12,55.6   c-2.6,7.7,-5.5,15.3,-8.9,22.6c-0.9,1.9,-1.5,3.7,-1.1,5.7c-9,5.4,-19,8.1,-29,10.8C287.6,809.6,273.8,811.9,260,814z"
+        android:fillColor="#E6A3A3"/>
+    <path
+        android:pathData="M339,668c-11.1,13,-25.7,20.8,-41.3,26.9c-18.3,7.1,-37.5,10.4,-57,12.4c-10.1,1,-20.3,1.3,-30.2,1.3   c-16.6,0,-33.4,-0.7,-49.6,-5.3c-0.6,-0.2,-1.3,-0.1,-2,-0.1c-0.6,-0.4,-1.3,-0.7,-1.9,-1.1c-0.4,-3.8,0.3,-5,4.8,-4.2c12.1,2,24.3,2.8,36.5,3.4   c32.4,1.7,64.1,-2.3,95.1,-11.6c14.8,-4.5,29,-10.8,41.5,-20.1C336.3,668.5,337.5,667.9,339,668z"
+        android:fillColor="#FFFFFF"/>
+    <path
+        android:pathData="M96,678c1.6,0.2,3.7,-0.7,4,2C98.4,679.8,97,679.3,96,678z"
+        android:fillColor="#FFFFFF"/>
+    <path
+        android:pathData="M100,680c1.1,0,2.3,-0.2,3,1C101.9,681,100.7,681.2,100,680z"
+        android:fillColor="#FFFFFF"/>
+    <path
+        android:pathData="M91.9,675c0.8,0,1.6,0.2,2.1,1C93.2,676,92.4,675.8,91.9,675z"
+        android:fillColor="#FFFFFF"/>
+    <path
+        android:pathData="M94,676c0.3,0.3,0.7,0.7,1,1C94.7,676.7,94.3,676.3,94,676z"
+        android:fillColor="#FFFFFF"/>
+    <path
+        android:pathData="M95,677c0.3,0.3,0.7,0.7,1,1C95.7,677.7,95.3,677.3,95,677z"
+        android:fillColor="#C5C5C5"/>
+    <path
+        android:pathData="M360,771c2.3,-5,4.6,-10,6.9,-15C368.2,760.3,364.7,767.8,360,771z"
+        android:fillColor="#EDECEC"/>
+</vector>
diff --git a/packages/DocumentsUI/res/layout/directory_cluster.xml b/packages/DocumentsUI/res/layout/directory_cluster.xml
index 8245e53..2fa09d3 100644
--- a/packages/DocumentsUI/res/layout/directory_cluster.xml
+++ b/packages/DocumentsUI/res/layout/directory_cluster.xml
@@ -25,7 +25,7 @@
         android:elevation="8dp"
         android:background="@color/material_grey_50"/>
 
-    <com.android.documentsui.DirectoryContainerView
+    <FrameLayout
         android:id="@+id/container_directory"
         android:layout_width="match_parent"
         android:layout_height="0dp"
diff --git a/packages/DocumentsUI/res/menu/activity.xml b/packages/DocumentsUI/res/menu/activity.xml
index b791ef1..73571af 100644
--- a/packages/DocumentsUI/res/menu/activity.xml
+++ b/packages/DocumentsUI/res/menu/activity.xml
@@ -32,23 +32,6 @@
         android:imeOptions="actionSearch"
         android:visible="false" />
     <item
-        android:id="@+id/menu_sort"
-        android:title="@string/menu_sort"
-        android:icon="@drawable/ic_menu_sortby"
-        android:showAsAction="always">
-        <menu>
-            <item
-                android:id="@+id/menu_sort_name"
-                android:title="@string/sort_name" />
-            <item
-                android:id="@+id/menu_sort_date"
-                android:title="@string/sort_date" />
-            <item
-                android:id="@+id/menu_sort_size"
-                android:title="@string/sort_size" />
-        </menu>
-    </item>
-    <item
         android:id="@+id/menu_grid"
         android:title="@string/menu_grid"
         android:icon="@drawable/ic_menu_view_grid"
@@ -70,7 +53,7 @@
         android:title="@string/menu_create_dir"
         android:icon="@drawable/ic_menu_new_folder"
         android:alphabeticShortcut="e"
-        android:showAsAction="always"
+        android:showAsAction="never"
         android:visible="false" />
     <item
         android:id="@+id/menu_paste_from_clipboard"
@@ -80,6 +63,23 @@
         android:visible="false" />
     <!-- Copy action is defined in mode_directory.xml -->
     <item
+        android:id="@+id/menu_sort"
+        android:title="@string/menu_sort"
+        android:icon="@drawable/ic_menu_sortby"
+        android:showAsAction="never">
+        <menu>
+            <item
+                android:id="@+id/menu_sort_name"
+                android:title="@string/sort_name" />
+            <item
+                android:id="@+id/menu_sort_date"
+                android:title="@string/sort_date" />
+            <item
+                android:id="@+id/menu_sort_size"
+                android:title="@string/sort_size" />
+        </menu>
+    </item>
+    <item
         android:id="@+id/menu_file_size"
         android:showAsAction="never"
         android:visible="false" />
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 3c49f16..b97918e 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -101,7 +101,7 @@
     <!-- Toast shown when creating a folder failed with an error [CHAR LIMIT=48] -->
     <string name="create_error">Failed to create folder</string>
     <!-- Error message shown when querying for a list of documents failed [CHAR LIMIT=48] -->
-    <string name="query_error">Failed to query documents</string>
+    <string name="query_error">Can\u2019t load content at the moment</string>
 
     <!-- Title of storage root location that contains recently modified or used documents [CHAR LIMIT=24] -->
     <string name="root_recent">Recent</string>
@@ -123,7 +123,7 @@
     <string name="no_results">No matches in %1$s</string>
 
     <!-- Toast shown when no app can be found to open the selected document [CHAR LIMIT=48] -->
-    <string name="toast_no_application">Can\'t open file</string>
+    <string name="toast_no_application">Can\u2019t open file</string>
     <!-- Toast shown when some of the selected documents failed to be deleted [CHAR LIMIT=48] -->
     <string name="toast_failed_delete">Unable to delete some documents</string>
 
@@ -160,27 +160,27 @@
     <string name="delete_preparing">Preparing for delete\u2026</string>
     <!-- Title of the copy error notification [CHAR LIMIT=48] -->
     <plurals name="copy_error_notification_title">
-        <item quantity="one">Couldn\'t copy <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
-        <item quantity="other">Couldn\'t copy <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
+        <item quantity="one">Couldn\u2019t copy <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
+        <item quantity="other">Couldn\u2019t copy <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
     </plurals>
     <!-- Title of the move error notification [CHAR LIMIT=48] -->
     <plurals name="move_error_notification_title">
-        <item quantity="one">Couldn\'t move <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
-        <item quantity="other">Couldn\'t move <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
+        <item quantity="one">Couldn\u2019t move <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
+        <item quantity="other">Couldn\u2019t move <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
     </plurals>
     <!-- Title of the delete error notification [CHAR LIMIT=48] -->
     <plurals name="delete_error_notification_title">
-        <item quantity="one">Couldn\'t delete <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
-        <item quantity="other">Couldn\'t delete <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
+        <item quantity="one">Couldn\u2019t delete <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
+        <item quantity="other">Couldn\u2019t delete <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
     </plurals>
     <!-- Second line for notifications saying that more information will be shown after touching [CHAR LIMIT=48] -->
     <string name="notification_touch_for_details">Tap to view details</string>
     <!-- Label of the close dialog button.[CHAR LIMIT=24] -->
     <string name="close">Close</string>
     <!-- Contents of the copying failure alert dialog. [CHAR LIMIT=48] -->
-    <string name="copy_failure_alert_content">These files weren\'t copied: <xliff:g id="list">%1$s</xliff:g></string>
+    <string name="copy_failure_alert_content">These files weren\u2019t copied: <xliff:g id="list">%1$s</xliff:g></string>
     <!-- Contents of the moving failure alert dialog. [CHAR LIMIT=48] -->
-    <string name="move_failure_alert_content">These files weren\'t moved: <xliff:g id="list">%1$s</xliff:g></string>
+    <string name="move_failure_alert_content">These files weren\u2019t moved: <xliff:g id="list">%1$s</xliff:g></string>
     <!-- Contents of the copying warning dialog due to converted files. [CHAR LIMIT=64] -->
     <string name="copy_converted_warning_content">These files were converted to another format: <xliff:g id="list" example="Document.pdf, Photo.jpg, Song.ogg">%1$s</xliff:g></string>
     <!-- Toast shown when a user copies files to clipboard. -->
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index e72343e..3c21a21 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -45,22 +45,16 @@
 import android.view.Menu;
 import android.view.MenuItem;
 import android.widget.Spinner;
-import android.widget.Toolbar;
 
-import com.android.documentsui.RecentsProvider.ResumeColumns;
 import com.android.documentsui.SearchManager.SearchManagerListener;
 import com.android.documentsui.State.ViewMode;
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
-import com.android.documentsui.model.DurableUtils;
 import com.android.documentsui.model.RootInfo;
 import com.android.internal.util.Preconditions;
 
-import libcore.io.IoUtils;
-
 import java.io.FileNotFoundException;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -71,6 +65,9 @@
 
     static final String EXTRA_STATE = "state";
 
+    // See comments where this const is referenced for details.
+    private static final int DRAWER_NO_FIDDLE_DELAY = 1500;
+
     State mState;
     RootsCache mRoots;
     SearchManager mSearchManager;
@@ -78,17 +75,21 @@
     NavigationView mNavigator;
 
     private final String mTag;
+
     @LayoutRes
     private int mLayoutId;
-    private DirectoryContainerView mDirectoryContainer;
+
+    // Track the time we opened the drawer in response to back being pressed.
+    // We use the time gap to figure out whether to close app or reopen the drawer.
+    private long mDrawerLastFiddled;
 
     public abstract void onDocumentPicked(DocumentInfo doc, @Nullable SiblingProvider siblings);
     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
 
     abstract void onTaskFinished(Uri... uris);
     abstract void refreshDirectory(int anim);
-    abstract void saveStackBlocking();
-    abstract State buildState();
+    /** Allows sub-classes to include information in a newly created State instance. */
+    abstract void includeState(State initialState);
 
     public BaseActivity(@LayoutRes int layoutId, String tag) {
         mLayoutId = layoutId;
@@ -103,9 +104,7 @@
         setContentView(mLayoutId);
 
         mDrawer = DrawerController.create(this);
-        mState = (icicle != null)
-                ? icicle.<State>getParcelable(EXTRA_STATE)
-                        : buildState();
+        mState = getState(icicle);
         Metrics.logActivityLaunch(this, mState, getIntent());
 
         mRoots = DocumentsApplication.getRootsCache(this);
@@ -114,11 +113,11 @@
                 new RootsCache.OnCacheUpdateListener() {
                     @Override
                     public void onCacheUpdate() {
-                        new HandleRootsChangedTask().execute(getCurrentRoot());
+                        new HandleRootsChangedTask(BaseActivity.this)
+                                .execute(getCurrentRoot());
                     }
                 });
 
-        mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory);
         mSearchManager = new SearchManager(this);
 
         DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
@@ -184,7 +183,20 @@
         super.onDestroy();
     }
 
-    State buildDefaultState() {
+    private State getState(@Nullable Bundle icicle) {
+        if (icicle != null) {
+            State state = icicle.<State>getParcelable(EXTRA_STATE);
+            if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state);
+            return state;
+        }
+
+        State state = createSharedState();
+        includeState(state);
+        if (DEBUG) Log.d(mTag, "Created new state object: " + state);
+        return state;
+    }
+
+    private State createSharedState() {
         State state = new State();
 
         final Intent intent = getIntent();
@@ -224,22 +236,7 @@
         if (mRoots.isRecentsRoot(root)) {
             refreshCurrentRootAndDirectory(ANIM_NONE);
         } else {
-            new PickRootTask(root).executeOnExecutor(getExecutorForCurrentDirectory());
-        }
-    }
-
-    void expandMenus(Menu menu) {
-        for (int i = 0; i < menu.size(); i++) {
-            final MenuItem item = menu.getItem(i);
-            switch (item.getItemId()) {
-                case R.id.menu_advanced:
-                case R.id.menu_file_size:
-                case R.id.menu_new_window:
-                case R.id.menu_search:
-                    break;
-                default:
-                    item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-            }
+            new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
         }
     }
 
@@ -351,7 +348,6 @@
     public final void refreshCurrentRootAndDirectory(int anim) {
         mSearchManager.cancelSearch();
 
-        mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_ENTER);
         refreshDirectory(anim);
 
         final RootsFragment roots = RootsFragment.get(getFragmentManager());
@@ -370,7 +366,6 @@
      */
     @Override
     public void onSearchChanged() {
-        mDirectoryContainer.setDrawDisappearingFirst(false);
         refreshDirectory(ANIM_NONE);
     }
 
@@ -539,16 +534,35 @@
             return;
         }
 
-        final int size = mState.stack.size();
+        int size = mState.stack.size();
 
-        if (mDrawer.isOpen()) {
-            mDrawer.setOpen(false);
-        } else if (size > 1) {
+        // Do some "do what a I want" drawer fiddling, but don't
+        // do it if user already hit back recently and we recently
+        // did some fiddling.
+        if ((System.currentTimeMillis() - mDrawerLastFiddled) > DRAWER_NO_FIDDLE_DELAY) {
+            // Close drawer if it is open.
+            if (mDrawer.isOpen()) {
+                mDrawer.setOpen(false);
+                mDrawerLastFiddled = System.currentTimeMillis();
+                return;
+            }
+
+            // Open the Close drawer if it is closed and we're at the top of a root.
+            if (size == 1) {
+                mDrawer.setOpen(true);
+                // Remember so we don't just close it again if back is pressed again.
+                mDrawerLastFiddled = System.currentTimeMillis();
+                return;
+            }
+        }
+
+        if (size > 1) {
             mState.stack.pop();
             refreshCurrentRootAndDirectory(ANIM_LEAVE);
-        } else {
-            super.onBackPressed();
+            return;
         }
+
+        super.onBackPressed();
     }
 
     public void onStackPicked(DocumentStack stack) {
@@ -574,115 +588,40 @@
         }
     }
 
-    final class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
+    private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> {
         private RootInfo mRoot;
 
-        public PickRootTask(RootInfo root) {
+        public PickRootTask(BaseActivity activity, RootInfo root) {
+            super(activity);
             mRoot = root;
         }
 
         @Override
-        protected DocumentInfo doInBackground(Void... params) {
-            return getRootDocumentBlocking(mRoot);
+        protected DocumentInfo run(Void... params) {
+            return mOwner.getRootDocumentBlocking(mRoot);
         }
 
         @Override
-        protected void onPostExecute(DocumentInfo result) {
-            if (result != null && !isDestroyed()) {
-                openContainerDocument(result);
+        protected void finish(DocumentInfo result) {
+            if (result != null) {
+                mOwner.openContainerDocument(result);
             }
         }
     }
 
-    final class RestoreStackTask extends AsyncTask<Void, Void, Void> {
-        private volatile boolean mRestoredStack;
-        private volatile boolean mExternal;
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            if (DEBUG && !mState.stack.isEmpty()) {
-                Log.w(mTag, "Overwriting existing stack.");
-            }
-            RootsCache roots = DocumentsApplication.getRootsCache(BaseActivity.this);
-
-            // Restore last stack for calling package
-            final String packageName = getCallingPackageMaybeExtra();
-            final Cursor cursor = getContentResolver()
-                    .query(RecentsProvider.buildResume(packageName), null, null, null, null);
-            try {
-                if (cursor.moveToFirst()) {
-                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
-                    final byte[] rawStack = cursor.getBlob(
-                            cursor.getColumnIndex(ResumeColumns.STACK));
-                    DurableUtils.readFromArray(rawStack, mState.stack);
-                    mRestoredStack = true;
-                }
-            } catch (IOException e) {
-                Log.w(mTag, "Failed to resume: " + e);
-            } finally {
-                IoUtils.closeQuietly(cursor);
-            }
-
-            if (mRestoredStack) {
-                // Update the restored stack to ensure we have freshest data
-                final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState);
-                try {
-                    mState.stack.updateRoot(matchingRoots);
-                    mState.stack.updateDocuments(getContentResolver());
-                } catch (FileNotFoundException e) {
-                    Log.w(mTag, "Failed to restore stack: " + e);
-                    mState.stack.reset();
-                    mRestoredStack = false;
-                }
-            }
-
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            if (isDestroyed()) return;
-            mState.restored = true;
-            refreshCurrentRootAndDirectory(ANIM_NONE);
-            onStackRestored(mRestoredStack, mExternal);
-        }
-    }
-
-    final class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
-        private Uri mRootUri;
-
-        public RestoreRootTask(Uri rootUri) {
-            mRootUri = rootUri;
-        }
-
-        @Override
-        protected RootInfo doInBackground(Void... params) {
-            final String rootId = DocumentsContract.getRootId(mRootUri);
-            return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
-        }
-
-        @Override
-        protected void onPostExecute(RootInfo root) {
-            if (isDestroyed()) return;
-            mState.restored = true;
-
-            if (root != null) {
-                onRootPicked(root);
-            } else {
-                Log.w(mTag, "Failed to find root: " + mRootUri);
-                finish();
-            }
-        }
-    }
-
-    final class HandleRootsChangedTask extends AsyncTask<RootInfo, Void, RootInfo> {
+    private static final class HandleRootsChangedTask
+            extends PairedTask<BaseActivity, RootInfo, RootInfo> {
         DocumentInfo mHome;
 
+        public HandleRootsChangedTask(BaseActivity activity) {
+            super(activity);
+        }
+
         @Override
-        protected RootInfo doInBackground(RootInfo... roots) {
+        protected RootInfo run(RootInfo... roots) {
             checkArgument(roots.length == 1);
             final RootInfo currentRoot = roots[0];
-            final Collection<RootInfo> cachedRoots = mRoots.getRootsBlocking();
+            final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
             RootInfo homeRoot = null;
             for (final RootInfo root : cachedRoots) {
                 if (root.isHome()) {
@@ -694,17 +633,17 @@
                 }
             }
             Preconditions.checkNotNull(homeRoot);
-            mHome = getRootDocumentBlocking(homeRoot);
+            mHome = mOwner.getRootDocumentBlocking(homeRoot);
             return homeRoot;
         }
 
         @Override
-        protected void onPostExecute(RootInfo homeRoot) {
-            if (homeRoot != null && mHome != null && !isDestroyed()) {
+        protected void finish(RootInfo homeRoot) {
+            if (homeRoot != null && mHome != null) {
                 // Clear entire backstack and start in new root
-                mState.onRootChanged(homeRoot);
-                mSearchManager.update(homeRoot);
-                openContainerDocument(mHome);
+                mOwner.mState.onRootChanged(homeRoot);
+                mOwner.mSearchManager.update(homeRoot);
+                mOwner.openContainerDocument(mHome);
             }
         }
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryContainerView.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryContainerView.java
deleted file mode 100644
index 71ea8a9..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryContainerView.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2013 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.documentsui;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import java.util.ArrayList;
-
-public class DirectoryContainerView extends FrameLayout {
-    private boolean mDisappearingFirst = false;
-
-    public DirectoryContainerView(Context context) {
-        super(context);
-    }
-
-    public DirectoryContainerView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void dispatchDraw(Canvas canvas) {
-        final ArrayList<View> disappearing = mDisappearingChildren;
-        if (mDisappearingFirst && disappearing != null) {
-            for (int i = 0; i < disappearing.size(); i++) {
-                super.drawChild(canvas, disappearing.get(i), getDrawingTime());
-            }
-        }
-        super.dispatchDraw(canvas);
-    }
-
-    @Override
-    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
-        if (mDisappearingFirst && mDisappearingChildren != null
-                && mDisappearingChildren.contains(child)) {
-            return false;
-        }
-        return super.drawChild(canvas, child, drawingTime);
-    }
-
-    public void setDrawDisappearingFirst(boolean disappearingFirst) {
-        mDisappearingFirst = disappearingFirst;
-    }
-}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 815ff3d..3485fe4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.Shared.DEBUG;
 import static com.android.documentsui.State.ACTION_CREATE;
 import static com.android.documentsui.State.ACTION_GET_CONTENT;
 import static com.android.documentsui.State.ACTION_OPEN;
@@ -31,11 +32,12 @@
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
+import android.database.Cursor;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
@@ -52,7 +54,12 @@
 import com.android.documentsui.model.RootInfo;
 import com.android.documentsui.services.FileOperationService;
 
+import libcore.io.IoUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 
 public class DocumentsActivity extends BaseActivity {
@@ -94,16 +101,14 @@
             // In this case, we set the activity title in AsyncTask.onPostExecute().  To prevent
             // talkback from reading aloud the default title, we clear it here.
             setTitle("");
-            new RestoreStackTask().execute();
+            new RestoreStackTask(this).execute();
         } else {
             refreshCurrentRootAndDirectory(ANIM_NONE);
         }
     }
 
     @Override
-    State buildState() {
-        State state = buildDefaultState();
-
+    void includeState(State state) {
         final Intent intent = getIntent();
         final String action = intent.getAction();
         if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
@@ -134,8 +139,6 @@
             state.transferMode = intent.getIntExtra(FileOperationService.EXTRA_OPERATION,
                     FileOperationService.OPERATION_COPY);
         }
-
-        return state;
     }
 
     @Override
@@ -218,14 +221,6 @@
     }
 
     @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        boolean showMenu = super.onCreateOptionsMenu(menu);
-
-        expandMenus(menu);
-        return showMenu;
-    }
-
-    @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         super.onPrepareOptionsMenu(menu);
 
@@ -319,11 +314,13 @@
     }
 
     void onSaveRequested(DocumentInfo replaceTarget) {
-        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory());
+        new ExistingFinishTask(this, replaceTarget.derivedUri)
+                .executeOnExecutor(getExecutorForCurrentDirectory());
     }
 
     void onSaveRequested(String mimeType, String displayName) {
-        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getExecutorForCurrentDirectory());
+        new CreateFinishTask(this, mimeType, displayName)
+                .executeOnExecutor(getExecutorForCurrentDirectory());
     }
 
     @Override
@@ -343,7 +340,8 @@
             openContainerDocument(doc);
         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
             // Explicit file picked, return
-            new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory());
+            new ExistingFinishTask(this, doc.derivedUri)
+                    .executeOnExecutor(getExecutorForCurrentDirectory());
         } else if (mState.action == ACTION_CREATE) {
             // Replace selected file
             SaveFragment.get(fm).setReplaceTarget(doc);
@@ -358,7 +356,8 @@
             for (int i = 0; i < size; i++) {
                 uris[i] = docs.get(i).derivedUri;
             }
-            new ExistingFinishTask(uris).executeOnExecutor(getExecutorForCurrentDirectory());
+            new ExistingFinishTask(this, uris)
+                    .executeOnExecutor(getExecutorForCurrentDirectory());
         }
     }
 
@@ -373,11 +372,10 @@
             // Should not be reached.
             throw new IllegalStateException("Invalid mState.action.");
         }
-        new PickFinishTask(result).executeOnExecutor(getExecutorForCurrentDirectory());
+        new PickFinishTask(this, result).executeOnExecutor(getExecutorForCurrentDirectory());
     }
 
-    @Override
-    void saveStackBlocking() {
+    void writeStackToRecentsBlocking() {
         final ContentResolver resolver = getContentResolver();
         final ContentValues values = new ContentValues();
 
@@ -438,69 +436,137 @@
         finish();
     }
 
+
     public static DocumentsActivity get(Fragment fragment) {
         return (DocumentsActivity) fragment.getActivity();
     }
 
-    private final class PickFinishTask extends AsyncTask<Void, Void, Void> {
+    /**
+     * Restores the stack from Recents for the specified package.
+     */
+    private static final class RestoreStackTask
+            extends PairedTask<DocumentsActivity, Void, Void> {
+
+        private volatile boolean mRestoredStack;
+        private volatile boolean mExternal;
+        private State mState;
+
+        public RestoreStackTask(DocumentsActivity activity) {
+            super(activity);
+            mState = activity.mState;
+        }
+
+        @Override
+        protected Void run(Void... params) {
+            if (DEBUG && !mState.stack.isEmpty()) {
+                Log.w(TAG, "Overwriting existing stack.");
+            }
+            RootsCache roots = DocumentsApplication.getRootsCache(mOwner);
+
+            String packageName = mOwner.getCallingPackageMaybeExtra();
+            Uri resumeUri = RecentsProvider.buildResume(packageName);
+            Cursor cursor = mOwner.getContentResolver().query(resumeUri, null, null, null, null);
+            try {
+                if (cursor.moveToFirst()) {
+                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
+                    final byte[] rawStack = cursor.getBlob(
+                            cursor.getColumnIndex(ResumeColumns.STACK));
+                    DurableUtils.readFromArray(rawStack, mState.stack);
+                    mRestoredStack = true;
+                }
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to resume: " + e);
+            } finally {
+                IoUtils.closeQuietly(cursor);
+            }
+
+            if (mRestoredStack) {
+                // Update the restored stack to ensure we have freshest data
+                final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState);
+                try {
+                    mState.stack.updateRoot(matchingRoots);
+                    mState.stack.updateDocuments(mOwner.getContentResolver());
+                } catch (FileNotFoundException e) {
+                    Log.w(TAG, "Failed to restore stack for package: " + packageName
+                            + " because of error: "+ e);
+                    mState.stack.reset();
+                    mRestoredStack = false;
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        protected void finish(Void result) {
+            mState.restored = true;
+            mOwner.refreshCurrentRootAndDirectory(ANIM_NONE);
+            mOwner.onStackRestored(mRestoredStack, mExternal);
+        }
+    }
+
+    private static final class PickFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
         private final Uri mUri;
 
-        public PickFinishTask(Uri uri) {
+        public PickFinishTask(DocumentsActivity activity, Uri uri) {
+            super(activity);
             mUri = uri;
         }
 
         @Override
-        protected Void doInBackground(Void... params) {
-            saveStackBlocking();
+        protected Void run(Void... params) {
+            mOwner.writeStackToRecentsBlocking();
             return null;
         }
 
         @Override
-        protected void onPostExecute(Void result) {
-            onTaskFinished(mUri);
+        protected void finish(Void result) {
+            mOwner.onTaskFinished(mUri);
         }
     }
 
-    final class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
+    private static final class ExistingFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
         private final Uri[] mUris;
 
-        public ExistingFinishTask(Uri... uris) {
+        public ExistingFinishTask(DocumentsActivity activity, Uri... uris) {
+            super(activity);
             mUris = uris;
         }
 
         @Override
-        protected Void doInBackground(Void... params) {
-            saveStackBlocking();
+        protected Void run(Void... params) {
+            mOwner.writeStackToRecentsBlocking();
             return null;
         }
 
         @Override
-        protected void onPostExecute(Void result) {
-            onTaskFinished(mUris);
+        protected void finish(Void result) {
+            mOwner.onTaskFinished(mUris);
         }
     }
 
     /**
      * Task that creates a new document in the background.
      */
-    final class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
+    private static final class CreateFinishTask extends PairedTask<DocumentsActivity, Void, Uri> {
         private final String mMimeType;
         private final String mDisplayName;
 
-        public CreateFinishTask(String mimeType, String displayName) {
+        public CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName) {
+            super(activity);
             mMimeType = mimeType;
             mDisplayName = displayName;
         }
 
         @Override
-        protected void onPreExecute() {
-            setPending(true);
+        protected void prepare() {
+            mOwner.setPending(true);
         }
 
         @Override
-        protected Uri doInBackground(Void... params) {
-            final ContentResolver resolver = getContentResolver();
-            final DocumentInfo cwd = getCurrentDirectory();
+        protected Uri run(Void... params) {
+            final ContentResolver resolver = mOwner.getContentResolver();
+            final DocumentInfo cwd = mOwner.getCurrentDirectory();
 
             ContentProviderClient client = null;
             Uri childUri = null;
@@ -516,22 +582,22 @@
             }
 
             if (childUri != null) {
-                saveStackBlocking();
+                mOwner.writeStackToRecentsBlocking();
             }
 
             return childUri;
         }
 
         @Override
-        protected void onPostExecute(Uri result) {
+        protected void finish(Uri result) {
             if (result != null) {
-                onTaskFinished(result);
+                mOwner.onTaskFinished(result);
             } else {
                 Snackbars.makeSnackbar(
-                    DocumentsActivity.this, R.string.save_error, Snackbar.LENGTH_SHORT).show();
+                        mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show();
             }
 
-            setPending(false);
+            mOwner.setPending(false);
         }
     }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java
index 89be910..d589d5e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java
@@ -24,8 +24,6 @@
 import android.app.FragmentManager;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
-import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
@@ -37,10 +35,8 @@
 import android.view.MenuItem;
 import android.widget.Toolbar;
 
-import com.android.documentsui.RecentsProvider.ResumeColumns;
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.model.DocumentInfo;
-import com.android.documentsui.model.DurableUtils;
 import com.android.documentsui.model.RootInfo;
 import com.android.internal.util.Preconditions;
 
@@ -72,23 +68,19 @@
             // talkback from reading aloud the default title, we clear it here.
             setTitle("");
             final Uri rootUri = getIntent().getData();
-            new RestoreRootTask(rootUri).executeOnExecutor(getExecutorForCurrentDirectory());
+            new RestoreRootTask(this, rootUri).executeOnExecutor(getExecutorForCurrentDirectory());
         } else {
             refreshCurrentRootAndDirectory(ANIM_NONE);
         }
     }
 
     @Override
-    State buildState() {
-        State state = buildDefaultState();
-
+    void includeState(State state) {
         state.action = ACTION_MANAGE;
         state.acceptMimes = new String[] { "*/*" };
         state.allowMultiple = true;
         state.showSize = true;
         state.excludedAuthorities = getExcludedAuthorities();
-
-        return state;
     }
 
     @Override
@@ -108,14 +100,12 @@
 
         final MenuItem advanced = menu.findItem(R.id.menu_advanced);
         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
-        final MenuItem newWindow = menu.findItem(R.id.menu_new_window);
         final MenuItem pasteFromCb = menu.findItem(R.id.menu_paste_from_clipboard);
         final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
 
         advanced.setVisible(false);
         createDir.setVisible(false);
         pasteFromCb.setEnabled(false);
-        newWindow.setEnabled(false);
         fileSize.setVisible(false);
 
         Menus.disableHiddenItems(menu);
@@ -170,21 +160,6 @@
     public void onDocumentsPicked(List<DocumentInfo> docs) {}
 
     @Override
-    void saveStackBlocking() {
-        final ContentResolver resolver = getContentResolver();
-        final ContentValues values = new ContentValues();
-
-        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
-
-        // Remember location for next app launch
-        final String packageName = getCallingPackageMaybeExtra();
-        values.clear();
-        values.put(ResumeColumns.STACK, rawStack);
-        values.put(ResumeColumns.EXTERNAL, 0);
-        resolver.insert(RecentsProvider.buildResume(packageName), values);
-    }
-
-    @Override
     void onTaskFinished(Uri... uris) {
         Log.d(TAG, "onFinished() " + Arrays.toString(uris));
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index 3aba356..c81f342 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -30,7 +30,6 @@
 import android.content.ContentValues;
 import android.content.Intent;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
@@ -98,19 +97,19 @@
             refreshCurrentRootAndDirectory(ANIM_NONE);
         } else if (intent.getAction() == Intent.ACTION_VIEW) {
             checkArgument(uri != null);
-            new OpenUriForViewTask().executeOnExecutor(
+            new OpenUriForViewTask(this).executeOnExecutor(
                     ProviderExecutor.forAuthority(uri.getAuthority()), uri);
         } else if (DocumentsContract.isRootUri(this, uri)) {
             if (DEBUG) Log.d(TAG, "Launching with root URI.");
             // If we've got a specific root to display, restore that root using a dedicated
             // authority. That way a misbehaving provider won't result in an ANR.
-            new RestoreRootTask(uri).executeOnExecutor(
+            new RestoreRootTask(this, uri).executeOnExecutor(
                     ProviderExecutor.forAuthority(uri.getAuthority()));
         } else {
             if (DEBUG) Log.d(TAG, "Launching into Home directory.");
             // If all else fails, try to load "Home" directory.
             final Uri homeUri = DocumentsContract.buildHomeUri();
-            new RestoreRootTask(homeUri).executeOnExecutor(
+            new RestoreRootTask(this, homeUri).executeOnExecutor(
                     ProviderExecutor.forAuthority(homeUri.getAuthority()));
         }
 
@@ -134,9 +133,7 @@
     }
 
     @Override
-    State buildState() {
-        State state = buildDefaultState();
-
+    void includeState(State state) {
         final Intent intent = getIntent();
 
         state.action = State.ACTION_BROWSE;
@@ -149,8 +146,6 @@
         if (stack != null) {
             state.stack = stack;
         }
-
-        return state;
     }
 
     @Override
@@ -190,14 +185,6 @@
     }
 
     @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        boolean showMenu = super.onCreateOptionsMenu(menu);
-
-        expandMenus(menu);
-        return showMenu;
-    }
-
-    @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         super.onPrepareOptionsMenu(menu);
 
@@ -206,15 +193,13 @@
         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
         final MenuItem pasteFromCb = menu.findItem(R.id.menu_paste_from_clipboard);
         final MenuItem settings = menu.findItem(R.id.menu_settings);
+        final MenuItem newWindow = menu.findItem(R.id.menu_new_window);
 
         createDir.setVisible(true);
         createDir.setEnabled(canCreateDirectory());
         pasteFromCb.setEnabled(mClipper.hasItemsToPaste());
         settings.setVisible(root.hasSettings());
-
-        // TODO: For some reason settings menu item is not
-        // honoring the "showAsAction=never" setting in activity.xml.
-        settings.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+        newWindow.setVisible(true);
 
         Menus.disableHiddenItems(menu, pasteFromCb);
         return true;
@@ -363,13 +348,15 @@
         }
     }
 
-    @Override
-    void saveStackBlocking() {
+    // Turns out only DocumentsActivity was ever calling saveStackBlocking.
+    // There may be a  case where we want to contribute entries from
+    // Behavior here in FilesActivity, but it isn't yet obvious.
+    // TODO: Contribute to recents, or remove this.
+    void writeStackToRecentsBlocking() {
         final ContentResolver resolver = getContentResolver();
         final ContentValues values = new ContentValues();
 
-        final byte[] rawStack = DurableUtils.writeToArrayOrNull(
-                getDisplayState().stack);
+        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
 
         // Remember location for next app launch
         final String packageName = getCallingPackageMaybeExtra();
@@ -408,12 +395,19 @@
      * to know which root to select. Also, the stack doesn't contain intermediate directories.
      * It's primarly used for opening ZIP archives from Downloads app.
      */
-    final class OpenUriForViewTask extends AsyncTask<Uri, Void, Void> {
+    private static final class OpenUriForViewTask extends PairedTask<FilesActivity, Uri, Void> {
+
+        private final State mState;
+        public OpenUriForViewTask(FilesActivity activity) {
+            super(activity);
+            mState = activity.mState;
+        }
+
         @Override
-        protected Void doInBackground(Uri... params) {
+        protected Void run(Uri... params) {
             final Uri uri = params[0];
 
-            final RootsCache rootsCache = DocumentsApplication.getRootsCache(FilesActivity.this);
+            final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner);
             final String authority = uri.getAuthority();
 
             final Collection<RootInfo> roots =
@@ -426,20 +420,17 @@
             final RootInfo root = roots.iterator().next();
             mState.stack.root = root;
             try {
-                mState.stack.add(DocumentInfo.fromUri(getContentResolver(), uri));
+                mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri));
             } catch (FileNotFoundException e) {
                 Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + uri);
             }
-            mState.stack.add(getRootDocumentBlocking(root));
+            mState.stack.add(mOwner.getRootDocumentBlocking(root));
             return null;
         }
 
         @Override
-        protected void onPostExecute(Void result) {
-            if (isDestroyed()) {
-                return;
-            }
-            refreshCurrentRootAndDirectory(ANIM_NONE);
+        protected void finish(Void result) {
+            mOwner.refreshCurrentRootAndDirectory(ANIM_NONE);
         }
     }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PairedTask.java b/packages/DocumentsUI/src/com/android/documentsui/PairedTask.java
new file mode 100644
index 0000000..b74acb8
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/PairedTask.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 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.documentsui;
+
+import android.app.Activity;
+import android.os.AsyncTask;
+
+/**
+ * An {@link AsyncTask} that guards work with checks that a paired {@link Activity}
+ * is still alive. Instances of this class make no progress.
+ *
+ * <p>Use this type of task for greater safety when executing tasks that might complete
+ * after an Activity is destroyed.
+ *
+ * <p>Also useful as tasks can be static, limiting scope, but still have access to
+ * the owning class (by way the A template and the mActivity field).
+ *
+ * @template Owner Activity type.
+ * @template Input input type
+ * @template Output output type
+ */
+abstract class PairedTask<Owner extends Activity, Input, Output>
+        extends AsyncTask<Input, Void, Output> {
+
+    protected final Owner mOwner;
+
+    public PairedTask(Owner owner) {
+        mOwner = owner;
+    }
+
+    /** Called prior to run being executed. Analogous to {@link AsyncTask#onPreExecute} */
+    void prepare() {}
+
+    /** Analogous to {@link AsyncTask#doInBackground} */
+    abstract Output run(Input... input);
+
+    /** Analogous to {@link AsyncTask#onPostExecute} */
+    abstract void finish(Output output);
+
+    @Override
+    final protected void onPreExecute() {
+        if (mOwner.isDestroyed()) {
+            return;
+        }
+        prepare();
+    }
+
+    @Override
+    final protected Output doInBackground(Input... input) {
+        if (mOwner.isDestroyed()) {
+            return null;
+        }
+        return run(input);
+    }
+
+    @Override
+    final protected void onPostExecute(Output result) {
+        if (mOwner.isDestroyed()) {
+            return;
+        }
+        finish(result);
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RestoreRootTask.java b/packages/DocumentsUI/src/com/android/documentsui/RestoreRootTask.java
new file mode 100644
index 0000000..9048b9d
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RestoreRootTask.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 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.documentsui;
+
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.util.Log;
+
+import com.android.documentsui.model.RootInfo;
+
+final class RestoreRootTask extends PairedTask<BaseActivity, Void, RootInfo> {
+    private static final String TAG = "RestoreRootTask";
+
+    private final Uri mRootUri;
+
+    public RestoreRootTask(BaseActivity activity, Uri rootUri) {
+        super(activity);
+        mRootUri = rootUri;
+    }
+
+    @Override
+    protected RootInfo run(Void... params) {
+        String rootId = DocumentsContract.getRootId(mRootUri);
+        return mOwner.mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
+    }
+
+    @Override
+    protected void finish(RootInfo root) {
+        mOwner.mState.restored = true;
+
+        if (root != null) {
+            mOwner.onRootPicked(root);
+        } else {
+            Log.w(TAG, "Failed to find root: " + mRootUri);
+            mOwner.finish();
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/State.java b/packages/DocumentsUI/src/com/android/documentsui/State.java
index 28f7432..d141de6 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/State.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/State.java
@@ -158,9 +158,14 @@
         out.writeInt(mStackTouched ? 1 : 0);
     }
 
-    public static final Creator<State> CREATOR = new Creator<State>() {
+    public static final ClassLoaderCreator<State> CREATOR = new ClassLoaderCreator<State>() {
         @Override
         public State createFromParcel(Parcel in) {
+            return createFromParcel(in, null);
+        }
+
+        @Override
+        public State createFromParcel(Parcel in, ClassLoader loader) {
             final State state = new State();
             state.action = in.readInt();
             state.acceptMimes = in.readStringArray();
@@ -174,9 +179,9 @@
             state.restored = in.readInt() != 0;
             DurableUtils.readFromParcel(in, state.stack);
             state.currentSearch = in.readString();
-            in.readMap(state.dirState, null);
-            in.readList(state.selectedDocumentsForCopy, null);
-            in.readList(state.excludedAuthorities, null);
+            in.readMap(state.dirState, loader);
+            in.readList(state.selectedDocumentsForCopy, loader);
+            in.readList(state.excludedAuthorities, loader);
             state.openableOnly = in.readInt() != 0;
             state.mStackTouched = in.readInt() != 0;
             return state;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 1e3da6b..70bee3c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -100,7 +100,6 @@
 import com.android.documentsui.model.RootInfo;
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.services.FileOperations;
-
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -854,27 +853,28 @@
     }
 
     private void showEmptyDirectory() {
-        showEmptyView(R.string.empty);
+        showEmptyView(R.string.empty, R.drawable.cabinet);
     }
 
     private void showNoResults(RootInfo root) {
         CharSequence msg = getContext().getResources().getText(R.string.no_results);
-        showEmptyView(String.format(String.valueOf(msg), root.title));
+        showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
     }
 
-    // Shows an error indicating documents couldn't be queried.
     private void showQueryError() {
-        showEmptyView(R.string.query_error);
+        showEmptyView(R.string.query_error, R.drawable.hourglass);
     }
 
-    private void showEmptyView(@StringRes int id) {
-        showEmptyView(getContext().getResources().getText(id));
+    private void showEmptyView(@StringRes int id, int drawable) {
+        showEmptyView(getContext().getResources().getText(id), drawable);
     }
 
-    private void showEmptyView(CharSequence msg) {
+    private void showEmptyView(CharSequence msg, int drawable) {
         View content = mEmptyView.findViewById(R.id.content);
         TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
+        ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
         msgView.setText(msg);
+        imageView.setImageResource(drawable);
 
         content.animate().cancel();  // cancel any ongoing animations
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java
index 9855427..2527650 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java
@@ -16,10 +16,6 @@
 
 package com.android.documentsui;
 
-import static com.android.documentsui.Shared.TAG;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.pm.ProviderInfo;
@@ -471,6 +467,14 @@
 
     @Override
     public Bundle call(String method, String arg, Bundle extras) {
+        // We're not supposed to override any of the default DocumentsProvider
+        // methods that are supported by "call", so javadoc asks that we
+        // always call super.call first and return if response is not null.
+        Bundle result = super.call(method, arg, extras);
+        if (result != null) {
+            return result;
+        }
+
         switch (method) {
             case "clear":
                 clearCacheAndBuildRoots();
@@ -484,11 +488,10 @@
                 simulateReadErrorsForFile(arg);
                 return null;
             case "createDocumentWithFlags":
-                Bundle bundle = dispatchCreateDocumentWithFlags(extras);
-                return bundle;
-            default:
-                return super.call(method, arg, extras);
+                return dispatchCreateDocumentWithFlags(extras);
         }
+
+        return null;
     }
 
     private Bundle createVirtualFileFromBundle(Bundle extras) {
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java b/packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java
index 1829746..ef1e8e2 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java
@@ -24,9 +24,11 @@
 import android.os.Bundle;
 import android.os.Process;
 import android.provider.DocumentsContract;
+import android.util.Log;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.LinkedList;
@@ -56,11 +58,17 @@
 
     private static MtpObjectInfo[] loadDocuments(MtpManager manager, int deviceId, int[] handles)
             throws IOException {
-        final MtpObjectInfo[] objectInfos = new MtpObjectInfo[handles.length];
+        final ArrayList<MtpObjectInfo> objects = new ArrayList<>();
         for (int i = 0; i < handles.length; i++) {
-            objectInfos[i] = manager.getObjectInfo(deviceId, handles[i]);
+            final MtpObjectInfo info = manager.getObjectInfo(deviceId, handles[i]);
+            if (info == null) {
+                Log.e(MtpDocumentsProvider.TAG,
+                        "Failed to obtain object info handle=" + handles[i]);
+                continue;
+            }
+            objects.add(info);
         }
-        return objectInfos;
+        return objects.toArray(new MtpObjectInfo[objects.size()]);
     }
 
     synchronized Cursor queryChildDocuments(String[] columnNames, Identifier parent)
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
index b44f9af..8a3ebef 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
@@ -577,9 +577,7 @@
     static void getObjectDocumentValues(
             ContentValues values, int deviceId, String parentId, MtpObjectInfo info) {
         values.clear();
-        final String mimeType = info.getFormat() == MtpConstants.FORMAT_ASSOCIATION ?
-                DocumentsContract.Document.MIME_TYPE_DIR :
-                MediaFile.getMimeTypeForFormatCode(info.getFormat());
+        final String mimeType = getMimeType(info);
         int flag = 0;
         if (info.getProtectionStatus() == 0) {
             flag |= Document.FLAG_SUPPORTS_DELETE |
@@ -608,6 +606,17 @@
         values.put(Document.COLUMN_SIZE, info.getCompressedSize());
     }
 
+    private static String getMimeType(MtpObjectInfo info) {
+        if (info.getFormat() == MtpConstants.FORMAT_ASSOCIATION) {
+            return DocumentsContract.Document.MIME_TYPE_DIR;
+        }
+        final String formatCodeMimeType = MediaFile.getMimeTypeForFormatCode(info.getFormat());
+        if (formatCodeMimeType != null) {
+            return formatCodeMimeType;
+        }
+        return MediaFile.getMimeTypeForFile(info.getName());
+    }
+
     static String[] strings(Object... args) {
         final String[] results = new String[args.length];
         for (int i = 0; i < args.length; i++) {
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index 70a1aae..0338454 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -184,6 +184,9 @@
     public ParcelFileDescriptor openDocument(
             String documentId, String mode, CancellationSignal signal)
                     throws FileNotFoundException {
+        if (DEBUG) {
+            Log.d(TAG, "openDocument: " + documentId);
+        }
         final Identifier identifier = mDatabase.createIdentifier(documentId);
         try {
             openDevice(identifier.mDeviceId);
@@ -270,6 +273,9 @@
     @Override
     public String createDocument(String parentDocumentId, String mimeType, String displayName)
             throws FileNotFoundException {
+        if (DEBUG) {
+            Log.d(TAG, "createDocument: " + displayName);
+        }
         try {
             final Identifier parentId = mDatabase.createIdentifier(parentDocumentId);
             openDevice(parentId.mDeviceId);
diff --git a/packages/Osu/Android.mk b/packages/Osu/Android.mk
new file mode 100644
index 0000000..15744c5
--- /dev/null
+++ b/packages/Osu/Android.mk
@@ -0,0 +1,19 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_LIBRARIES := telephony-common ims-common bouncycastle conscrypt
+
+LOCAL_PACKAGE_NAME := Osu
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+include $(BUILD_PACKAGE)
+
+########################
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/packages/Osu/AndroidManifest.xml b/packages/Osu/AndroidManifest.xml
new file mode 100644
index 0000000..288f1a4
--- /dev/null
+++ b/packages/Osu/AndroidManifest.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android">
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application
+	android:enabled="false"
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+	android:persistent="true"
+        android:supportsRtl="true">
+        <activity android:name=".MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="com.android.hotspot2.OSU_NOTIFICATION" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+	<receiver android:name="com.android.MainActivity$WifiReceiver" >
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" android:enabled="true"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.net.wifi.SCAN_RESULTS" android:enabled="true"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.net.wifi.PASSPOINT_WNM_FRAME_RECEIVED" android:enabled="true"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.net.wifi.PASSPOINT_ICON_RECEIVED" android:enabled="true"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.net.wifi.CONFIGURED_NETWORKS_CHANGE" android:enabled="true"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.net.wifi.WIFI_STATE_CHANGED" android:enabled="true"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.net.wifi.STATE_CHANGE" android:enabled="true"/>
+            </intent-filter>
+	</receiver>
+	<service android:name="com.android.MainActivity$OSUService" />
+    </application>
+
+</manifest>
diff --git a/packages/Osu/res/layout/activity_main.xml b/packages/Osu/res/layout/activity_main.xml
new file mode 100644
index 0000000..7e33537
--- /dev/null
+++ b/packages/Osu/res/layout/activity_main.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/no_osu"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/no_osu"/>
+    <ListView
+        android:id="@+id/profile_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:divider="#1F000000"
+        android:dividerHeight="1dp"
+        android:padding="16dp" />
+
+</LinearLayout>
diff --git a/packages/Osu/res/layout/list_item.xml b/packages/Osu/res/layout/list_item.xml
new file mode 100644
index 0000000..eb431d3
--- /dev/null
+++ b/packages/Osu/res/layout/list_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:gravity="top"
+    android:layout_marginTop="10sp"
+    android:layout_marginBottom="10sp"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <ImageView
+        android:id="@+id/profile_logo"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_weight="1"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content">
+
+        <TextView
+            android:id="@+id/profile_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"/>
+
+        <TextView
+            android:id="@+id/profile_detail"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="10sp"
+            android:textSize="12sp"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/packages/Osu/res/mipmap-hdpi/ic_launcher.png b/packages/Osu/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/packages/Osu/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-mdpi/ic_launcher.png b/packages/Osu/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/packages/Osu/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-xhdpi/ic_launcher.png b/packages/Osu/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/packages/Osu/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-xxhdpi/ic_launcher.png b/packages/Osu/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/packages/Osu/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-xxxhdpi/ic_launcher.png b/packages/Osu/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
--- /dev/null
+++ b/packages/Osu/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/values-w820dp/dimens.xml b/packages/Osu/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/packages/Osu/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/packages/Osu/res/values/colors.xml b/packages/Osu/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/packages/Osu/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/packages/Osu/res/values/dimens.xml b/packages/Osu/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/packages/Osu/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/packages/Osu/res/values/strings.xml b/packages/Osu/res/values/strings.xml
new file mode 100644
index 0000000..93593dc
--- /dev/null
+++ b/packages/Osu/res/values/strings.xml
@@ -0,0 +1,4 @@
+<resources>
+    <string name="app_name">OSU</string>
+    <string name="no_osu">No OSU available</string>
+</resources>
diff --git a/packages/Osu/src/com/android/MainActivity.java b/packages/Osu/src/com/android/MainActivity.java
new file mode 100644
index 0000000..7e7d49a
--- /dev/null
+++ b/packages/Osu/src/com/android/MainActivity.java
@@ -0,0 +1,419 @@
+package com.android;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.TaskStackBuilder;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.anqp.OSUProvider;
+import com.android.hotspot2.AppBridge;
+import com.android.hotspot2.PasspointMatch;
+import com.android.hotspot2.osu.OSUInfo;
+import com.android.hotspot2.osu.OSUManager;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+//import com.android.Osu.R;
+
+/**
+ * Main activity.
+ */
+public class MainActivity extends Activity {
+    private static final int NOTIFICATION_ID = 0; // Used for OSU count
+    private static final int NOTIFICATION_MESSAGE_ID = 1; // Used for other messages
+    private static final Locale LOCALE = java.util.Locale.getDefault();
+
+    private static volatile OSUService sOsuService;
+
+    private ListView osuListView;
+    private OsuListAdapter2 osuListAdapter;
+    private String message;
+
+    public MainActivity() {
+
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (message != null) {
+            showDialog(message);
+            message = null;
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent intent = getIntent();
+        Bundle bundle = intent.getExtras();
+
+        if (bundle == null) {   // User interaction
+            if (sOsuService == null) {
+                Intent serviceIntent = new Intent(this, OSUService.class);
+                serviceIntent.putExtra(ACTION_KEY, "dummy-key");
+                startService(serviceIntent);
+                return;
+            }
+
+            List<OSUInfo> osuInfos = sOsuService.getOsuInfos();
+
+            setContentView(R.layout.activity_main);
+            Log.d("osu", "osu count:" + osuInfos.size());
+            View noOsuView = findViewById(R.id.no_osu);
+            if (osuInfos.size() > 0) {
+                noOsuView.setVisibility(View.GONE);
+                osuListAdapter = new OsuListAdapter2(this, osuInfos);
+                osuListView = (ListView) findViewById(R.id.profile_list);
+                osuListView.setAdapter(osuListAdapter);
+                osuListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+                    @Override
+                    public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+                        OSUInfo osuData = (OSUInfo) adapterView.getAdapter().getItem(position);
+                        Log.d("osu", "launch osu:" + osuData.getName(LOCALE)
+                                + " id:" + osuData.getOsuID());
+                        sOsuService.selectOsu(osuData.getOsuID());
+                        finish();
+                    }
+                });
+            } else {
+                noOsuView.setVisibility(View.VISIBLE);
+            }
+        } else if (intent.getAction().equals(AppBridge.ACTION_OSU_NOTIFICATION)) {
+            if (bundle.containsKey(AppBridge.OSU_COUNT)) {
+                showOsuCount(bundle.getInt("osu-count", 0), Collections.<OSUInfo>emptyList());
+            } else if (bundle.containsKey(AppBridge.PROV_SUCCESS)) {
+                showStatus(bundle.getBoolean(AppBridge.PROV_SUCCESS),
+                        bundle.getString(AppBridge.SP_NAME),
+                        bundle.getString(AppBridge.PROV_MESSAGE),
+                        null);
+            } else if (bundle.containsKey(AppBridge.DEAUTH)) {
+                showDeauth(bundle.getString(AppBridge.SP_NAME),
+                        bundle.getBoolean(AppBridge.DEAUTH),
+                        bundle.getInt(AppBridge.DEAUTH_DELAY),
+                        bundle.getString(AppBridge.DEAUTH_URL));
+            }
+            /*
+            else if (bundle.containsKey(AppBridge.OSU_INFO)) {
+                List<OsuData> osus = printOsuDataList(bundle.getParcelableArray(AppBridge.OSU_INFO));
+                showOsuList(osus);
+            }
+            */
+        }
+    }
+
+    private void showOsuCount(int osuCount, List<OSUInfo> osus) {
+        if (osuCount > 0) {
+            printOsuDataList(osus);
+            sendNotification(osuCount);
+        } else {
+            cancelNotification();
+        }
+        finish();
+    }
+
+    private void showStatus(boolean provSuccess, String spName, String provMessage,
+                            String remoteStatus) {
+        if (provSuccess) {
+            sendDialogMessage(
+                    String.format("Credentials for %s was successfully installed", spName));
+        } else {
+            if (spName != null) {
+                if (remoteStatus != null) {
+                    sendDialogMessage(
+                            String.format("Failed to install credentials for %s: %s: %s",
+                                    spName, provMessage, remoteStatus));
+                } else {
+                    sendDialogMessage(
+                            String.format("Failed to install credentials for %s: %s",
+                                    spName, provMessage));
+                }
+            } else {
+                sendDialogMessage(
+                        String.format("Failed to contact OSU: %s", provMessage));
+            }
+        }
+    }
+
+    private void showDeauth(String spName, boolean ess, int delay, String url) {
+        String delayReadable = getReadableTimeInSeconds(delay);
+        if (ess) {
+            if (delay > 60) {
+                sendDialogMessage(
+                        String.format("There is an issue connecting to %s [for the next %s]. " +
+                                "Please visit %s for details", spName, delayReadable, url));
+            } else {
+                sendDialogMessage(
+                        String.format("There is an issue connecting to %s. " +
+                                "Please visit %s for details", spName, url));
+            }
+        } else {
+            sendDialogMessage(
+                    String.format("There is an issue with the closest Access Point for %s. " +
+                                    "You may wait %s or move to another Access Point to " +
+                                    "regain access. Please visit %s for details.",
+                            spName, delayReadable, url));
+        }
+    }
+
+    private static final String ACTION_KEY = "action";
+
+    public static class WifiReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context c, Intent intent) {
+            Log.d(OSUManager.TAG, "OSU App got intent: " + intent.getAction());
+            Intent serviceIntent;
+            serviceIntent = new Intent(c, OSUService.class);
+            serviceIntent.putExtra(ACTION_KEY, intent.getAction());
+            serviceIntent.putExtras(intent);
+            c.startService(serviceIntent);
+        }
+    }
+
+    public static class OSUService extends IntentService {
+        private OSUManager mOsuManager;
+        private final IBinder mBinder = new Binder();
+
+        public OSUService() {
+            super("OSUService");
+        }
+
+        @Override
+        public int onStartCommand(Intent intent, int flags, int startId) {
+            onHandleIntent(intent);
+            return START_STICKY;
+        }
+
+        @Override
+        public void onCreate() {
+            super.onCreate();
+            Log.d("YYY", String.format("Service %x running, OSU %x",
+                    System.identityHashCode(this), System.identityHashCode(mOsuManager)));
+            if (mOsuManager == null) {
+                mOsuManager = new OSUManager(this);
+            }
+            sOsuService = this;
+        }
+
+        @Override
+        public void onDestroy() {
+            super.onDestroy();
+            Log.d("YYY", String.format("Service %x killed", System.identityHashCode(this)));
+        }
+
+        @Override
+        public IBinder onBind(Intent intent) {
+            return mBinder;
+        }
+
+        @Override
+        protected void onHandleIntent(Intent intent) {
+            Bundle bundle = intent.getExtras();
+            WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
+            Log.d(OSUManager.TAG, "OSU Service got intent: " + intent.getStringExtra(ACTION_KEY));
+            switch (intent.getStringExtra(ACTION_KEY)) {
+                case WifiManager.SCAN_RESULTS_AVAILABLE_ACTION:
+                    mOsuManager.pushScanResults(wifiManager.getScanResults());
+                    break;
+                case WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION:
+                    long bssid = bundle.getLong(WifiManager.EXTRA_PASSPOINT_WNM_BSSID);
+                    String url = bundle.getString(WifiManager.EXTRA_PASSPOINT_WNM_URL);
+
+                    try {
+                        if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_METHOD)) {
+                            int method = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_METHOD);
+                            if (method != OSUProvider.OSUMethod.SoapXml.ordinal()) {
+                                Log.w(OSUManager.TAG, "Unsupported remediation method: " + method);
+                            }
+                            PasspointMatch match = null;
+                            if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH)) {
+                                int ordinal =
+                                        bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH);
+                                if (ordinal >= 0 && ordinal < PasspointMatch.values().length) {
+                                    match = PasspointMatch.values()[ordinal];
+                                }
+                            }
+                            mOsuManager.wnmRemediate(bssid, url, match);
+                        } else if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_ESS)) {
+                            boolean ess = bundle.getBoolean(WifiManager.EXTRA_PASSPOINT_WNM_ESS);
+                            int delay = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_DELAY);
+                            mOsuManager.deauth(bssid, ess, delay, url);
+                        } else {
+                            Log.w(OSUManager.TAG, "Unknown WNM event");
+                        }
+                    } catch (IOException | SAXException e) {
+                        Log.w(OSUManager.TAG, "Remediation event failed to parse: " + e);
+                    }
+                    break;
+                case WifiManager.PASSPOINT_ICON_RECEIVED_ACTION:
+                    mOsuManager.notifyIconReceived(
+                            bundle.getLong(WifiManager.EXTRA_PASSPOINT_ICON_BSSID),
+                            bundle.getString(WifiManager.EXTRA_PASSPOINT_ICON_FILE),
+                            bundle.getByteArray(WifiManager.EXTRA_PASSPOINT_ICON_DATA));
+                    break;
+                case WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION:
+                    mOsuManager.networkConfigChange((WifiConfiguration)
+                            intent.getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION));
+                    break;
+                case WifiManager.WIFI_STATE_CHANGED_ACTION:
+                    int state = bundle.getInt(WifiManager.EXTRA_WIFI_STATE);
+                    if (state == WifiManager.WIFI_STATE_DISABLED) {
+                        mOsuManager.wifiStateChange(false);
+                    } else if (state == WifiManager.WIFI_STATE_ENABLED) {
+                        mOsuManager.wifiStateChange(true);
+                    }
+                    break;
+                case WifiManager.NETWORK_STATE_CHANGED_ACTION:
+                    mOsuManager.networkConnectEvent((WifiInfo)
+                            intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO));
+                    break;
+            }
+        }
+
+        public List<OSUInfo> getOsuInfos() {
+            return mOsuManager.getAvailableOSUs();
+        }
+
+        public void selectOsu(int id) {
+            mOsuManager.setOSUSelection(id);
+        }
+    }
+
+    private String getReadableTimeInSeconds(int timeSeconds) {
+        long hours = TimeUnit.SECONDS.toHours(timeSeconds);
+        long minutes = TimeUnit.SECONDS.toMinutes(timeSeconds) - TimeUnit.HOURS.toMinutes(hours);
+        long seconds =
+                timeSeconds - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes);
+        if (hours > 0) {
+            return String.format("%02d:%02d:%02d", hours, minutes, seconds);
+        } else {
+            return String.format("%ds", seconds);
+        }
+    }
+
+    private void sendNotification(int count) {
+        Notification.Builder builder =
+                new Notification.Builder(this)
+                        .setContentTitle(String.format("%s OSU available", count))
+                        .setContentText("Choose one to connect")
+                        .setSmallIcon(android.R.drawable.ic_dialog_info)
+                        .setAutoCancel(false);
+        Intent resultIntent = new Intent(this, MainActivity.class);
+
+        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
+        stackBuilder.addParentStack(MainActivity.class);
+        stackBuilder.addNextIntent(resultIntent);
+        PendingIntent resultPendingIntent =
+                stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
+        builder.setContentIntent(resultPendingIntent);
+        NotificationManager notificationManager =
+                (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+        notificationManager.notify(NOTIFICATION_ID, builder.build());
+    }
+
+    private void cancelNotification() {
+        NotificationManager notificationManager =
+                (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+        notificationManager.cancel(NOTIFICATION_ID);
+    }
+
+    private void sendDialogMessage(String message) {
+//        sendNotificationMessage(message);
+        this.message = message;
+    }
+
+    private void showDialog(String message) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setMessage(message)
+                .setTitle("OSU");
+        builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+            @Override
+            public void onCancel(DialogInterface dialogInterface) {
+                dialogInterface.cancel();
+                finish();
+            }
+        });
+        AlertDialog dialog = builder.create();
+        dialog.show();
+    }
+
+    private void sendNotificationMessage(String title) {
+        Notification.Builder builder =
+                new Notification.Builder(this)
+                        .setContentTitle(title)
+                        .setContentText("Click to dismiss.")
+                        .setSmallIcon(android.R.drawable.ic_dialog_info)
+                        .setAutoCancel(true);
+        NotificationManager notificationManager =
+                (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+        notificationManager.notify(NOTIFICATION_MESSAGE_ID, builder.build());
+    }
+
+    private static class OsuListAdapter2 extends ArrayAdapter<OSUInfo> {
+        private Activity activity;
+
+        public OsuListAdapter2(Activity activity, List<OSUInfo> osuDataList) {
+            super(activity, R.layout.list_item, osuDataList);
+            this.activity = activity;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View view = convertView;
+            if (view == null) {
+                view = LayoutInflater.from(getContext()).inflate(R.layout.list_item, parent, false);
+            }
+            OSUInfo osuData = getItem(position);
+            TextView osuName = (TextView) view.findViewById(R.id.profile_name);
+            osuName.setText(osuData.getName(LOCALE));
+            TextView osuDetail = (TextView) view.findViewById(R.id.profile_detail);
+            osuDetail.setText(osuData.getServiceDescription(LOCALE));
+            ImageView osuIcon = (ImageView) view.findViewById(R.id.profile_logo);
+            byte[] iconData = osuData.getIconFileElement().getIconData();
+            osuIcon.setImageDrawable(
+                    new BitmapDrawable(activity.getResources(),
+                            BitmapFactory.decodeByteArray(iconData, 0, iconData.length)));
+            return view;
+        }
+    }
+
+    private void printOsuDataList(List<OSUInfo> osuDataList) {
+        for (OSUInfo osuData : osuDataList) {
+            Log.d("osu", String.format("OSUData:[%s][%s][%d]",
+                    osuData.getName(LOCALE), osuData.getServiceDescription(LOCALE),
+                    osuData.getOsuID()));
+        }
+    }
+
+}
diff --git a/packages/Osu/src/com/android/anqp/ANQPElement.java b/packages/Osu/src/com/android/anqp/ANQPElement.java
new file mode 100644
index 0000000..58aee29
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/ANQPElement.java
@@ -0,0 +1,16 @@
+package com.android.anqp;
+
+/**
+ * Base class for an IEEE802.11u ANQP element.
+ */
+public abstract class ANQPElement {
+    private final Constants.ANQPElementType mID;
+
+    protected ANQPElement(Constants.ANQPElementType id) {
+        mID = id;
+    }
+
+    public Constants.ANQPElementType getID() {
+        return mID;
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/Constants.java b/packages/Osu/src/com/android/anqp/Constants.java
new file mode 100644
index 0000000..214bb93
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/Constants.java
@@ -0,0 +1,233 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ANQP related constants (802.11-2012)
+ */
+public class Constants {
+
+    public static final int NIBBLE_MASK = 0x0f;
+    public static final int BYTE_MASK = 0xff;
+    public static final int SHORT_MASK = 0xffff;
+    public static final long INT_MASK = 0xffffffffL;
+    public static final int BYTES_IN_SHORT = 2;
+    public static final int BYTES_IN_INT = 4;
+    public static final int BYTES_IN_EUI48 = 6;
+    public static final long MILLIS_IN_A_SEC = 1000L;
+
+    public static final int HS20_PREFIX = 0x119a6f50;   // Note that this is represented as a LE int
+    public static final int HS20_FRAME_PREFIX = 0x109a6f50;
+    public static final int UTF8_INDICATOR = 1;
+
+    public static final int LANG_CODE_LENGTH = 3;
+
+    public static final int ANQP_QUERY_LIST = 256;
+    public static final int ANQP_CAPABILITY_LIST = 257;
+    public static final int ANQP_VENUE_NAME = 258;
+    public static final int ANQP_EMERGENCY_NUMBER = 259;
+    public static final int ANQP_NWK_AUTH_TYPE = 260;
+    public static final int ANQP_ROAMING_CONSORTIUM = 261;
+    public static final int ANQP_IP_ADDR_AVAILABILITY = 262;
+    public static final int ANQP_NAI_REALM = 263;
+    public static final int ANQP_3GPP_NETWORK = 264;
+    public static final int ANQP_GEO_LOC = 265;
+    public static final int ANQP_CIVIC_LOC = 266;
+    public static final int ANQP_LOC_URI = 267;
+    public static final int ANQP_DOM_NAME = 268;
+    public static final int ANQP_EMERGENCY_ALERT = 269;
+    public static final int ANQP_TDLS_CAP = 270;
+    public static final int ANQP_EMERGENCY_NAI = 271;
+    public static final int ANQP_NEIGHBOR_REPORT = 272;
+    public static final int ANQP_VENDOR_SPEC = 56797;
+
+    public static final int HS_QUERY_LIST = 1;
+    public static final int HS_CAPABILITY_LIST = 2;
+    public static final int HS_FRIENDLY_NAME = 3;
+    public static final int HS_WAN_METRICS = 4;
+    public static final int HS_CONN_CAPABILITY = 5;
+    public static final int HS_NAI_HOME_REALM_QUERY = 6;
+    public static final int HS_OPERATING_CLASS = 7;
+    public static final int HS_OSU_PROVIDERS = 8;
+    public static final int HS_ICON_REQUEST = 10;
+    public static final int HS_ICON_FILE = 11;
+
+    public enum ANQPElementType {
+        ANQPQueryList,
+        ANQPCapabilityList,
+        ANQPVenueName,
+        ANQPEmergencyNumber,
+        ANQPNwkAuthType,
+        ANQPRoamingConsortium,
+        ANQPIPAddrAvailability,
+        ANQPNAIRealm,
+        ANQP3GPPNetwork,
+        ANQPGeoLoc,
+        ANQPCivicLoc,
+        ANQPLocURI,
+        ANQPDomName,
+        ANQPEmergencyAlert,
+        ANQPTDLSCap,
+        ANQPEmergencyNAI,
+        ANQPNeighborReport,
+        ANQPVendorSpec,
+        HSQueryList,
+        HSCapabilityList,
+        HSFriendlyName,
+        HSWANMetrics,
+        HSConnCapability,
+        HSNAIHomeRealmQuery,
+        HSOperatingclass,
+        HSOSUProviders,
+        HSIconRequest,
+        HSIconFile
+    }
+
+    private static final Map<Integer, ANQPElementType> sAnqpMap = new HashMap<>();
+    private static final Map<Integer, ANQPElementType> sHs20Map = new HashMap<>();
+    private static final Map<ANQPElementType, Integer> sRevAnqpmap =
+            new EnumMap<>(ANQPElementType.class);
+    private static final Map<ANQPElementType, Integer> sRevHs20map =
+            new EnumMap<>(ANQPElementType.class);
+
+    static {
+        sAnqpMap.put(ANQP_QUERY_LIST, ANQPElementType.ANQPQueryList);
+        sAnqpMap.put(ANQP_CAPABILITY_LIST, ANQPElementType.ANQPCapabilityList);
+        sAnqpMap.put(ANQP_VENUE_NAME, ANQPElementType.ANQPVenueName);
+        sAnqpMap.put(ANQP_EMERGENCY_NUMBER, ANQPElementType.ANQPEmergencyNumber);
+        sAnqpMap.put(ANQP_NWK_AUTH_TYPE, ANQPElementType.ANQPNwkAuthType);
+        sAnqpMap.put(ANQP_ROAMING_CONSORTIUM, ANQPElementType.ANQPRoamingConsortium);
+        sAnqpMap.put(ANQP_IP_ADDR_AVAILABILITY, ANQPElementType.ANQPIPAddrAvailability);
+        sAnqpMap.put(ANQP_NAI_REALM, ANQPElementType.ANQPNAIRealm);
+        sAnqpMap.put(ANQP_3GPP_NETWORK, ANQPElementType.ANQP3GPPNetwork);
+        sAnqpMap.put(ANQP_GEO_LOC, ANQPElementType.ANQPGeoLoc);
+        sAnqpMap.put(ANQP_CIVIC_LOC, ANQPElementType.ANQPCivicLoc);
+        sAnqpMap.put(ANQP_LOC_URI, ANQPElementType.ANQPLocURI);
+        sAnqpMap.put(ANQP_DOM_NAME, ANQPElementType.ANQPDomName);
+        sAnqpMap.put(ANQP_EMERGENCY_ALERT, ANQPElementType.ANQPEmergencyAlert);
+        sAnqpMap.put(ANQP_TDLS_CAP, ANQPElementType.ANQPTDLSCap);
+        sAnqpMap.put(ANQP_EMERGENCY_NAI, ANQPElementType.ANQPEmergencyNAI);
+        sAnqpMap.put(ANQP_NEIGHBOR_REPORT, ANQPElementType.ANQPNeighborReport);
+        sAnqpMap.put(ANQP_VENDOR_SPEC, ANQPElementType.ANQPVendorSpec);
+
+        sHs20Map.put(HS_QUERY_LIST, ANQPElementType.HSQueryList);
+        sHs20Map.put(HS_CAPABILITY_LIST, ANQPElementType.HSCapabilityList);
+        sHs20Map.put(HS_FRIENDLY_NAME, ANQPElementType.HSFriendlyName);
+        sHs20Map.put(HS_WAN_METRICS, ANQPElementType.HSWANMetrics);
+        sHs20Map.put(HS_CONN_CAPABILITY, ANQPElementType.HSConnCapability);
+        sHs20Map.put(HS_NAI_HOME_REALM_QUERY, ANQPElementType.HSNAIHomeRealmQuery);
+        sHs20Map.put(HS_OPERATING_CLASS, ANQPElementType.HSOperatingclass);
+        sHs20Map.put(HS_OSU_PROVIDERS, ANQPElementType.HSOSUProviders);
+        sHs20Map.put(HS_ICON_REQUEST, ANQPElementType.HSIconRequest);
+        sHs20Map.put(HS_ICON_FILE, ANQPElementType.HSIconFile);
+
+        for (Map.Entry<Integer, ANQPElementType> entry : sAnqpMap.entrySet()) {
+            sRevAnqpmap.put(entry.getValue(), entry.getKey());
+        }
+        for (Map.Entry<Integer, ANQPElementType> entry : sHs20Map.entrySet()) {
+            sRevHs20map.put(entry.getValue(), entry.getKey());
+        }
+    }
+
+    public static ANQPElementType mapANQPElement(int id) {
+        return sAnqpMap.get(id);
+    }
+
+    public static ANQPElementType mapHS20Element(int id) {
+        return sHs20Map.get(id);
+    }
+
+    public static Integer getANQPElementID(ANQPElementType elementType) {
+        return sRevAnqpmap.get(elementType);
+    }
+
+    public static Integer getHS20ElementID(ANQPElementType elementType) {
+        return sRevHs20map.get(elementType);
+    }
+
+    public static boolean hasBaseANQPElements(Collection<ANQPElementType> elements) {
+        if (elements == null) {
+            return false;
+        }
+        for (ANQPElementType element : elements) {
+            if (sRevAnqpmap.containsKey(element)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean hasR2Elements(List<ANQPElementType> elements) {
+        return elements.contains(ANQPElementType.HSOSUProviders);
+    }
+
+    public static long getInteger(ByteBuffer payload, ByteOrder bo, int size) {
+        byte[] octets = new byte[size];
+        payload.get(octets);
+        long value = 0;
+        if (bo == ByteOrder.LITTLE_ENDIAN) {
+            for (int n = octets.length - 1; n >= 0; n--) {
+                value = (value << Byte.SIZE) | (octets[n] & BYTE_MASK);
+            }
+        }
+        else {
+            for (byte octet : octets) {
+                value = (value << Byte.SIZE) | (octet & BYTE_MASK);
+            }
+        }
+        return value;
+    }
+
+    public static String getPrefixedString(ByteBuffer payload, int lengthLength, Charset charset)
+            throws ProtocolException {
+        return getPrefixedString(payload, lengthLength, charset, false);
+    }
+
+    public static String getPrefixedString(ByteBuffer payload, int lengthLength, Charset charset,
+                                           boolean useNull) throws ProtocolException {
+        if (payload.remaining() < lengthLength) {
+            throw new ProtocolException("Runt string: " + payload.remaining());
+        }
+        return getString(payload, (int) getInteger(payload, ByteOrder.LITTLE_ENDIAN,
+                lengthLength), charset, useNull);
+    }
+
+    public static String getTrimmedString(ByteBuffer payload, int length, Charset charset)
+            throws ProtocolException {
+        String s = getString(payload, length, charset, false);
+        int zero = length - 1;
+        while (zero >= 0) {
+            if (s.charAt(zero) != 0) {
+                break;
+            }
+            zero--;
+        }
+        return zero < length - 1 ? s.substring(0, zero + 1) : s;
+    }
+
+    public static String getString(ByteBuffer payload, int length, Charset charset)
+            throws ProtocolException {
+        return getString(payload, length, charset, false);
+    }
+
+    public static String getString(ByteBuffer payload, int length, Charset charset, boolean useNull)
+            throws ProtocolException {
+        if (length > payload.remaining()) {
+            throw new ProtocolException("Bad string length: " + length);
+        }
+        if (useNull && length == 0) {
+            return null;
+        }
+        byte[] octets = new byte[length];
+        payload.get(octets);
+        return new String(octets, charset);
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/HSIconFileElement.java b/packages/Osu/src/com/android/anqp/HSIconFileElement.java
new file mode 100644
index 0000000..bd14e3f
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/HSIconFileElement.java
@@ -0,0 +1,59 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * The Icon Binary File vendor specific ANQP Element,
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.11
+ */
+public class HSIconFileElement extends ANQPElement {
+
+    public enum StatusCode {Success, FileNotFound, Unspecified}
+
+    private final StatusCode mStatusCode;
+    private final String mType;
+    private final byte[] mIconData;
+
+    public HSIconFileElement(Constants.ANQPElementType infoID, ByteBuffer payload)
+            throws ProtocolException {
+        super(infoID);
+
+        if (payload.remaining() < 4) {
+            throw new ProtocolException("Truncated icon file: " + payload.remaining());
+        }
+
+        int statusID = payload.get() & BYTE_MASK;
+        mStatusCode = statusID < StatusCode.values().length ? StatusCode.values()[statusID] : null;
+        mType = Constants.getPrefixedString(payload, 1, StandardCharsets.US_ASCII);
+
+        int dataLength = payload.getShort() & SHORT_MASK;
+        mIconData = new byte[dataLength];
+        payload.get(mIconData);
+    }
+
+    public StatusCode getStatusCode() {
+        return mStatusCode;
+    }
+
+    public String getType() {
+        return mType;
+    }
+
+    public byte[] getIconData() {
+        return mIconData;
+    }
+
+    @Override
+    public String toString() {
+        return "HSIconFile{" +
+                "statusCode=" + mStatusCode +
+                ", type='" + mType + '\'' +
+                ", iconData=" + mIconData.length + " bytes }";
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/HSOsuProvidersElement.java b/packages/Osu/src/com/android/anqp/HSOsuProvidersElement.java
new file mode 100644
index 0000000..646e003
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/HSOsuProvidersElement.java
@@ -0,0 +1,49 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The OSU Providers List vendor specific ANQP Element,
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.8
+ */
+public class HSOsuProvidersElement extends ANQPElement {
+    private final String mSSID;
+    private final List<OSUProvider> mProviders;
+
+    public HSOsuProvidersElement(Constants.ANQPElementType infoID, ByteBuffer payload)
+            throws ProtocolException {
+        super(infoID);
+
+        mSSID = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8);
+        int providerCount = payload.get() & Constants.BYTE_MASK;
+
+        mProviders = new ArrayList<>(providerCount);
+
+        while (providerCount > 0) {
+            mProviders.add(new OSUProvider(mSSID, payload));
+            providerCount--;
+        }
+    }
+
+    public String getSSID() {
+        return mSSID;
+    }
+
+    public List<OSUProvider> getProviders() {
+        return Collections.unmodifiableList(mProviders);
+    }
+
+    @Override
+    public String toString() {
+        return "HSOsuProviders{" +
+                "SSID='" + mSSID + '\'' +
+                ", providers=" + mProviders +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/I18Name.java b/packages/Osu/src/com/android/anqp/I18Name.java
new file mode 100644
index 0000000..0a16db7
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/I18Name.java
@@ -0,0 +1,80 @@
+package com.android.anqp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * A generic Internationalized name used in ANQP elements as specified in 802.11-2012 and
+ * "Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00"
+ */
+public class I18Name {
+    private final String mLanguage;
+    private final Locale mLocale;
+    private final String mText;
+
+    public I18Name(ByteBuffer payload) throws ProtocolException {
+        if (payload.remaining() < Constants.LANG_CODE_LENGTH + 1) {
+            throw new ProtocolException("Truncated I18Name: " + payload.remaining());
+        }
+        int nameLength = payload.get() & BYTE_MASK;
+        if (nameLength < Constants.LANG_CODE_LENGTH) {
+            throw new ProtocolException("Runt I18Name: " + nameLength);
+        }
+        mLanguage = Constants.getTrimmedString(payload,
+                Constants.LANG_CODE_LENGTH, StandardCharsets.US_ASCII);
+        mLocale = Locale.forLanguageTag(mLanguage);
+        mText = Constants.getString(payload, nameLength -
+                Constants.LANG_CODE_LENGTH, StandardCharsets.UTF_8);
+    }
+
+    public I18Name(String compoundString) throws IOException {
+        if (compoundString.length() < Constants.LANG_CODE_LENGTH) {
+            throw new IOException("I18String too short: '" + compoundString + "'");
+        }
+        mLanguage = compoundString.substring(0, Constants.LANG_CODE_LENGTH);
+        mText = compoundString.substring(Constants.LANG_CODE_LENGTH);
+        mLocale = Locale.forLanguageTag(mLanguage);
+    }
+
+    public String getLanguage() {
+        return mLanguage;
+    }
+
+    public Locale getLocale() {
+        return mLocale;
+    }
+
+    public String getText() {
+        return mText;
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (thatObject == null || getClass() != thatObject.getClass()) {
+            return false;
+        }
+
+        I18Name that = (I18Name) thatObject;
+        return mLanguage.equals(that.mLanguage) && mText.equals(that.mText);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mLanguage.hashCode();
+        result = 31 * result + mText.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return mText + ':' + mLocale.getLanguage();
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/IconInfo.java b/packages/Osu/src/com/android/anqp/IconInfo.java
new file mode 100644
index 0000000..9e9f1ee
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/IconInfo.java
@@ -0,0 +1,91 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * The Icons available OSU Providers sub field, as specified in
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.8.1.4
+ */
+public class IconInfo {
+    private final int mWidth;
+    private final int mHeight;
+    private final String mLanguage;
+    private final String mIconType;
+    private final String mFileName;
+
+    public IconInfo(ByteBuffer payload) throws ProtocolException {
+        if (payload.remaining() < 9) {
+            throw new ProtocolException("Truncated icon meta data");
+        }
+
+        mWidth = payload.getShort() & SHORT_MASK;
+        mHeight = payload.getShort() & SHORT_MASK;
+        mLanguage = Constants.getTrimmedString(payload,
+                Constants.LANG_CODE_LENGTH, StandardCharsets.US_ASCII);
+        mIconType = Constants.getPrefixedString(payload, 1, StandardCharsets.US_ASCII);
+        mFileName = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8);
+    }
+
+    public int getWidth() {
+        return mWidth;
+    }
+
+    public int getHeight() {
+        return mHeight;
+    }
+
+    public String getLanguage() {
+        return mLanguage;
+    }
+
+    public String getIconType() {
+        return mIconType;
+    }
+
+    public String getFileName() {
+        return mFileName;
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (thatObject == null || getClass() != thatObject.getClass()) {
+            return false;
+        }
+
+        IconInfo that = (IconInfo) thatObject;
+        return mHeight == that.mHeight &&
+                mWidth == that.mWidth &&
+                mFileName.equals(that.mFileName) &&
+                mIconType.equals(that.mIconType) &&
+                mLanguage.equals(that.mLanguage);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mWidth;
+        result = 31 * result + mHeight;
+        result = 31 * result + mLanguage.hashCode();
+        result = 31 * result + mIconType.hashCode();
+        result = 31 * result + mFileName.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "IconInfo{" +
+                "Width=" + mWidth +
+                ", Height=" + mHeight +
+                ", Language=" + mLanguage +
+                ", IconType='" + mIconType + '\'' +
+                ", FileName='" + mFileName + '\'' +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/OSUProvider.java b/packages/Osu/src/com/android/anqp/OSUProvider.java
new file mode 100644
index 0000000..e2669d4
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/OSUProvider.java
@@ -0,0 +1,158 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * An OSU Provider, as specified in
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.8.1
+ */
+public class OSUProvider {
+
+    public enum OSUMethod {OmaDm, SoapXml}
+
+    private final String mSSID;
+    private final List<I18Name> mNames;
+    private final String mOSUServer;
+    private final List<OSUMethod> mOSUMethods;
+    private final List<IconInfo> mIcons;
+    private final String mOsuNai;
+    private final List<I18Name> mServiceDescriptions;
+    private final int mHashCode;
+
+    public OSUProvider(String ssid, ByteBuffer payload) throws ProtocolException {
+        if (payload.remaining() < 11) {
+            throw new ProtocolException("Truncated OSU provider: " + payload.remaining());
+        }
+
+        mSSID = ssid;
+
+        int length = payload.getShort() & SHORT_MASK;
+        int namesLength = payload.getShort() & SHORT_MASK;
+
+        ByteBuffer namesBuffer = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+        namesBuffer.limit(namesBuffer.position() + namesLength);
+        payload.position(payload.position() + namesLength);
+
+        mNames = new ArrayList<>();
+
+        while (namesBuffer.hasRemaining()) {
+            mNames.add(new I18Name(namesBuffer));
+        }
+
+        mOSUServer = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8);
+        int methodLength = payload.get() & BYTE_MASK;
+        mOSUMethods = new ArrayList<>(methodLength);
+        while (methodLength > 0) {
+            int methodID = payload.get() & BYTE_MASK;
+            mOSUMethods.add(methodID < OSUMethod.values().length ?
+                    OSUMethod.values()[methodID] :
+                    null);
+            methodLength--;
+        }
+
+        int iconsLength = payload.getShort() & SHORT_MASK;
+        ByteBuffer iconsBuffer = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+        iconsBuffer.limit(iconsBuffer.position() + iconsLength);
+        payload.position(payload.position() + iconsLength);
+
+        mIcons = new ArrayList<>();
+
+        while (iconsBuffer.hasRemaining()) {
+            mIcons.add(new IconInfo(iconsBuffer));
+        }
+
+        mOsuNai = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8, true);
+
+        int descriptionsLength = payload.getShort() & SHORT_MASK;
+        ByteBuffer descriptionsBuffer = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+        descriptionsBuffer.limit(descriptionsBuffer.position() + descriptionsLength);
+        payload.position(payload.position() + descriptionsLength);
+
+        mServiceDescriptions = new ArrayList<>();
+
+        while (descriptionsBuffer.hasRemaining()) {
+            mServiceDescriptions.add(new I18Name(descriptionsBuffer));
+        }
+
+        int result = mNames.hashCode();
+        result = 31 * result + mSSID.hashCode();
+        result = 31 * result + mOSUServer.hashCode();
+        result = 31 * result + mOSUMethods.hashCode();
+        result = 31 * result + mIcons.hashCode();
+        result = 31 * result + (mOsuNai != null ? mOsuNai.hashCode() : 0);
+        result = 31 * result + mServiceDescriptions.hashCode();
+        mHashCode = result;
+    }
+
+    public String getSSID() {
+        return mSSID;
+    }
+
+    public List<I18Name> getNames() {
+        return mNames;
+    }
+
+    public String getOSUServer() {
+        return mOSUServer;
+    }
+
+    public List<OSUMethod> getOSUMethods() {
+        return mOSUMethods;
+    }
+
+    public List<IconInfo> getIcons() {
+        return mIcons;
+    }
+
+    public String getOsuNai() {
+        return mOsuNai;
+    }
+
+    public List<I18Name> getServiceDescriptions() {
+        return mServiceDescriptions;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        OSUProvider that = (OSUProvider) o;
+
+        if (!mSSID.equals(that.mSSID)) return false;
+        if (!mOSUServer.equals(that.mOSUServer)) return false;
+        if (!mNames.equals(that.mNames)) return false;
+        if (!mServiceDescriptions.equals(that.mServiceDescriptions)) return false;
+        if (!mIcons.equals(that.mIcons)) return false;
+        if (!mOSUMethods.equals(that.mOSUMethods)) return false;
+        if (mOsuNai != null ? !mOsuNai.equals(that.mOsuNai) : that.mOsuNai != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return mHashCode;
+    }
+
+    @Override
+    public String toString() {
+        return "OSUProvider{" +
+                "names=" + mNames +
+                ", OSUServer='" + mOSUServer + '\'' +
+                ", OSUMethods=" + mOSUMethods +
+                ", icons=" + mIcons +
+                ", NAI='" + mOsuNai + '\'' +
+                ", serviceDescriptions=" + mServiceDescriptions +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/AuthParam.java b/packages/Osu/src/com/android/anqp/eap/AuthParam.java
new file mode 100644
index 0000000..4243954
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/AuthParam.java
@@ -0,0 +1,9 @@
+package com.android.anqp.eap;
+
+/**
+ * An Authentication parameter, part of the NAI Realm ANQP element, specified in
+ * IEEE802.11-2012 section 8.4.4.10, table 8-188
+ */
+public interface AuthParam {
+    public EAP.AuthInfoID getAuthInfoID();
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/Credential.java b/packages/Osu/src/com/android/anqp/eap/Credential.java
new file mode 100644
index 0000000..0a89f4f
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/Credential.java
@@ -0,0 +1,72 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class Credential implements AuthParam {
+
+    public enum CredType {
+        Reserved,
+        SIM,
+        USIM,
+        NFC,
+        HWToken,
+        Softoken,
+        Certificate,
+        Username,
+        None,
+        Anonymous,
+        VendorSpecific}
+
+    private final EAP.AuthInfoID mAuthInfoID;
+    private final CredType mCredType;
+
+    public Credential(EAP.AuthInfoID infoID, int length, ByteBuffer payload)
+            throws ProtocolException {
+        if (length != 1) {
+            throw new ProtocolException("Bad length: " + length);
+        }
+
+        mAuthInfoID = infoID;
+        int typeID = payload.get() & BYTE_MASK;
+
+        mCredType = typeID < CredType.values().length ?
+                CredType.values()[typeID] :
+                CredType.Reserved;
+    }
+
+    @Override
+    public EAP.AuthInfoID getAuthInfoID() {
+        return mAuthInfoID;
+    }
+
+    @Override
+    public int hashCode() {
+        return mAuthInfoID.hashCode() * 31 + mCredType.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (thatObject == this) {
+            return true;
+        } else if (thatObject == null || thatObject.getClass() != Credential.class) {
+            return false;
+        } else {
+            return ((Credential) thatObject).getCredType() == getCredType();
+        }
+    }
+
+    public CredType getCredType() {
+        return mCredType;
+    }
+
+    @Override
+    public String toString() {
+        return "Auth method " + mAuthInfoID + " = " + mCredType + "\n";
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/EAP.java b/packages/Osu/src/com/android/anqp/eap/EAP.java
new file mode 100644
index 0000000..4b968b6
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/EAP.java
@@ -0,0 +1,155 @@
+package com.android.anqp.eap;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * EAP Related constants for the ANQP NAIRealm element, IEEE802.11-2012 section 8.4.4.10
+ */
+public abstract class EAP {
+
+    private static final Map<Integer, EAPMethodID> sEapIds = new HashMap<>();
+    private static final Map<EAPMethodID, Integer> sRevEapIds = new HashMap<>();
+    private static final Map<Integer, AuthInfoID> sAuthIds = new HashMap<>();
+
+    public static final int EAP_MD5 = 4;
+    public static final int EAP_OTP = 5;
+    public static final int EAP_RSA = 9;
+    public static final int EAP_KEA = 11;
+    public static final int EAP_KEA_VALIDATE = 12;
+    public static final int EAP_TLS = 13;
+    public static final int EAP_LEAP = 17;
+    public static final int EAP_SIM = 18;
+    public static final int EAP_TTLS = 21;
+    public static final int EAP_AKA = 23;
+    public static final int EAP_3Com = 24;
+    public static final int EAP_MSCHAPv2 = 26;
+    public static final int EAP_PEAP = 29;
+    public static final int EAP_POTP = 32;
+    public static final int EAP_ActiontecWireless = 35;
+    public static final int EAP_HTTPDigest = 38;
+    public static final int EAP_SPEKE = 41;
+    public static final int EAP_MOBAC = 42;
+    public static final int EAP_FAST = 43;
+    public static final int EAP_ZLXEAP = 44;
+    public static final int EAP_Link = 45;
+    public static final int EAP_PAX = 46;
+    public static final int EAP_PSK = 47;
+    public static final int EAP_SAKE = 48;
+    public static final int EAP_IKEv2 = 49;
+    public static final int EAP_AKAPrim = 50;
+    public static final int EAP_GPSK = 51;
+    public static final int EAP_PWD = 52;
+    public static final int EAP_EKE = 53;
+    public static final int EAP_TEAP = 55;
+
+    public enum EAPMethodID {
+        EAP_MD5,
+        EAP_OTP,
+        EAP_RSA,
+        EAP_KEA,
+        EAP_KEA_VALIDATE,
+        EAP_TLS,
+        EAP_LEAP,
+        EAP_SIM,
+        EAP_TTLS,
+        EAP_AKA,
+        EAP_3Com,
+        EAP_MSCHAPv2,
+        EAP_PEAP,
+        EAP_POTP,
+        EAP_ActiontecWireless,
+        EAP_HTTPDigest,
+        EAP_SPEKE,
+        EAP_MOBAC,
+        EAP_FAST,
+        EAP_ZLXEAP,
+        EAP_Link,
+        EAP_PAX,
+        EAP_PSK,
+        EAP_SAKE,
+        EAP_IKEv2,
+        EAP_AKAPrim,
+        EAP_GPSK,
+        EAP_PWD,
+        EAP_EKE,
+        EAP_TEAP
+    }
+
+    public static final int ExpandedEAPMethod = 1;
+    public static final int NonEAPInnerAuthType = 2;
+    public static final int InnerAuthEAPMethodType = 3;
+    public static final int ExpandedInnerEAPMethod = 4;
+    public static final int CredentialType = 5;
+    public static final int TunneledEAPMethodCredType = 6;
+    public static final int VendorSpecific = 221;
+
+    public enum AuthInfoID {
+        Undefined,
+        ExpandedEAPMethod,
+        NonEAPInnerAuthType,
+        InnerAuthEAPMethodType,
+        ExpandedInnerEAPMethod,
+        CredentialType,
+        TunneledEAPMethodCredType,
+        VendorSpecific
+    }
+
+    static {
+        sEapIds.put(EAP_MD5, EAPMethodID.EAP_MD5);
+        sEapIds.put(EAP_OTP, EAPMethodID.EAP_OTP);
+        sEapIds.put(EAP_RSA, EAPMethodID.EAP_RSA);
+        sEapIds.put(EAP_KEA, EAPMethodID.EAP_KEA);
+        sEapIds.put(EAP_KEA_VALIDATE, EAPMethodID.EAP_KEA_VALIDATE);
+        sEapIds.put(EAP_TLS, EAPMethodID.EAP_TLS);
+        sEapIds.put(EAP_LEAP, EAPMethodID.EAP_LEAP);
+        sEapIds.put(EAP_SIM, EAPMethodID.EAP_SIM);
+        sEapIds.put(EAP_TTLS, EAPMethodID.EAP_TTLS);
+        sEapIds.put(EAP_AKA, EAPMethodID.EAP_AKA);
+        sEapIds.put(EAP_3Com, EAPMethodID.EAP_3Com);
+        sEapIds.put(EAP_MSCHAPv2, EAPMethodID.EAP_MSCHAPv2);
+        sEapIds.put(EAP_PEAP, EAPMethodID.EAP_PEAP);
+        sEapIds.put(EAP_POTP, EAPMethodID.EAP_POTP);
+        sEapIds.put(EAP_ActiontecWireless, EAPMethodID.EAP_ActiontecWireless);
+        sEapIds.put(EAP_HTTPDigest, EAPMethodID.EAP_HTTPDigest);
+        sEapIds.put(EAP_SPEKE, EAPMethodID.EAP_SPEKE);
+        sEapIds.put(EAP_MOBAC, EAPMethodID.EAP_MOBAC);
+        sEapIds.put(EAP_FAST, EAPMethodID.EAP_FAST);
+        sEapIds.put(EAP_ZLXEAP, EAPMethodID.EAP_ZLXEAP);
+        sEapIds.put(EAP_Link, EAPMethodID.EAP_Link);
+        sEapIds.put(EAP_PAX, EAPMethodID.EAP_PAX);
+        sEapIds.put(EAP_PSK, EAPMethodID.EAP_PSK);
+        sEapIds.put(EAP_SAKE, EAPMethodID.EAP_SAKE);
+        sEapIds.put(EAP_IKEv2, EAPMethodID.EAP_IKEv2);
+        sEapIds.put(EAP_AKAPrim, EAPMethodID.EAP_AKAPrim);
+        sEapIds.put(EAP_GPSK, EAPMethodID.EAP_GPSK);
+        sEapIds.put(EAP_PWD, EAPMethodID.EAP_PWD);
+        sEapIds.put(EAP_EKE, EAPMethodID.EAP_EKE);
+        sEapIds.put(EAP_TEAP, EAPMethodID.EAP_TEAP);
+
+        for (Map.Entry<Integer, EAPMethodID> entry : sEapIds.entrySet()) {
+            sRevEapIds.put(entry.getValue(), entry.getKey());
+        }
+
+        sAuthIds.put(ExpandedEAPMethod, AuthInfoID.ExpandedEAPMethod);
+        sAuthIds.put(NonEAPInnerAuthType, AuthInfoID.NonEAPInnerAuthType);
+        sAuthIds.put(InnerAuthEAPMethodType, AuthInfoID.InnerAuthEAPMethodType);
+        sAuthIds.put(ExpandedInnerEAPMethod, AuthInfoID.ExpandedInnerEAPMethod);
+        sAuthIds.put(CredentialType, AuthInfoID.CredentialType);
+        sAuthIds.put(TunneledEAPMethodCredType, AuthInfoID.TunneledEAPMethodCredType);
+        sAuthIds.put(VendorSpecific, AuthInfoID.VendorSpecific);
+    }
+
+    public static EAPMethodID mapEAPMethod(int methodID) {
+        return sEapIds.get(methodID);
+    }
+
+    public static Integer mapEAPMethod(EAPMethodID methodID) {
+        return sRevEapIds.get(methodID);
+    }
+
+    public static AuthInfoID mapAuthMethod(int methodID) {
+        return sAuthIds.get(methodID);
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/EAPMethod.java b/packages/Osu/src/com/android/anqp/eap/EAPMethod.java
new file mode 100644
index 0000000..fa6c2f9
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/EAPMethod.java
@@ -0,0 +1,191 @@
+package com.android.anqp.eap;
+
+import com.android.anqp.Constants;
+import com.android.hotspot2.AuthMatch;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An EAP Method, part of the NAI Realm ANQP element, specified in
+ * IEEE802.11-2012 section 8.4.4.10, figure 8-420
+ */
+public class EAPMethod {
+    private final EAP.EAPMethodID mEAPMethodID;
+    private final Map<EAP.AuthInfoID, Set<AuthParam>> mAuthParams;
+
+    public EAPMethod(ByteBuffer payload) throws ProtocolException {
+        if (payload.remaining() < 3) {
+            throw new ProtocolException("Runt EAP Method: " + payload.remaining());
+        }
+
+        int length = payload.get() & Constants.BYTE_MASK;
+        int methodID = payload.get() & Constants.BYTE_MASK;
+        int count = payload.get() & Constants.BYTE_MASK;
+
+        mEAPMethodID = EAP.mapEAPMethod(methodID);
+        mAuthParams = new EnumMap<>(EAP.AuthInfoID.class);
+
+        int realCount = 0;
+
+        ByteBuffer paramPayload = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+        paramPayload.limit(paramPayload.position() + length - 2);
+        payload.position(payload.position() + length - 2);
+        while (paramPayload.hasRemaining()) {
+            int id = paramPayload.get() & Constants.BYTE_MASK;
+
+            EAP.AuthInfoID authInfoID = EAP.mapAuthMethod(id);
+            if (authInfoID == null) {
+                throw new ProtocolException("Unknown auth parameter ID: " + id);
+            }
+
+            int len = paramPayload.get() & Constants.BYTE_MASK;
+            if (len == 0 || len > paramPayload.remaining()) {
+                throw new ProtocolException("Bad auth method length: " + len);
+            }
+
+            switch (authInfoID) {
+                case ExpandedEAPMethod:
+                    addAuthParam(new ExpandedEAPMethod(authInfoID, len, paramPayload));
+                    break;
+                case NonEAPInnerAuthType:
+                    addAuthParam(new NonEAPInnerAuth(len, paramPayload));
+                    break;
+                case InnerAuthEAPMethodType:
+                    addAuthParam(new InnerAuthEAP(len, paramPayload));
+                    break;
+                case ExpandedInnerEAPMethod:
+                    addAuthParam(new ExpandedEAPMethod(authInfoID, len, paramPayload));
+                    break;
+                case CredentialType:
+                    addAuthParam(new Credential(authInfoID, len, paramPayload));
+                    break;
+                case TunneledEAPMethodCredType:
+                    addAuthParam(new Credential(authInfoID, len, paramPayload));
+                    break;
+                case VendorSpecific:
+                    addAuthParam(new VendorSpecificAuth(len, paramPayload));
+                    break;
+            }
+
+            realCount++;
+        }
+        if (realCount != count)
+            throw new ProtocolException("Invalid parameter count: " + realCount +
+                    ", expected " + count);
+    }
+
+    public EAPMethod(EAP.EAPMethodID eapMethodID, AuthParam authParam) {
+        mEAPMethodID = eapMethodID;
+        mAuthParams = new HashMap<>(1);
+        if (authParam != null) {
+            Set<AuthParam> authParams = new HashSet<>();
+            authParams.add(authParam);
+            mAuthParams.put(authParam.getAuthInfoID(), authParams);
+        }
+    }
+
+    private void addAuthParam(AuthParam param) {
+        Set<AuthParam> authParams = mAuthParams.get(param.getAuthInfoID());
+        if (authParams == null) {
+            authParams = new HashSet<>();
+            mAuthParams.put(param.getAuthInfoID(), authParams);
+        }
+        authParams.add(param);
+    }
+
+    public Map<EAP.AuthInfoID, Set<AuthParam>> getAuthParams() {
+        return Collections.unmodifiableMap(mAuthParams);
+    }
+
+    public EAP.EAPMethodID getEAPMethodID() {
+        return mEAPMethodID;
+    }
+
+    public int match(com.android.hotspot2.pps.Credential credential) {
+
+        EAPMethod credMethod = credential.getEAPMethod();
+        if (mEAPMethodID != credMethod.getEAPMethodID()) {
+            return AuthMatch.None;
+        }
+
+        switch (mEAPMethodID) {
+            case EAP_TTLS:
+                if (mAuthParams.isEmpty()) {
+                    return AuthMatch.Method;
+                }
+                int paramCount = 0;
+                for (Map.Entry<EAP.AuthInfoID, Set<AuthParam>> entry :
+                        credMethod.getAuthParams().entrySet()) {
+                    Set<AuthParam> params = mAuthParams.get(entry.getKey());
+                    if (params == null) {
+                        continue;
+                    }
+
+                    if (!Collections.disjoint(params, entry.getValue())) {
+                        return AuthMatch.MethodParam;
+                    }
+                    paramCount += params.size();
+                }
+                return paramCount > 0 ? AuthMatch.None : AuthMatch.Method;
+            case EAP_TLS:
+                return AuthMatch.MethodParam;
+            case EAP_SIM:
+            case EAP_AKA:
+            case EAP_AKAPrim:
+                return AuthMatch.Method;
+            default:
+                return AuthMatch.Method;
+        }
+    }
+
+    public AuthParam getAuthParam() {
+        if (mAuthParams.isEmpty()) {
+            return null;
+        }
+        Set<AuthParam> params = mAuthParams.values().iterator().next();
+        if (params.isEmpty()) {
+            return null;
+        }
+        return params.iterator().next();
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        else if (thatObject == null || getClass() != thatObject.getClass()) {
+            return false;
+        }
+
+        EAPMethod that = (EAPMethod) thatObject;
+        return mEAPMethodID == that.mEAPMethodID && mAuthParams.equals(that.mAuthParams);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mEAPMethodID.hashCode();
+        result = 31 * result + mAuthParams.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("EAP Method ").append(mEAPMethodID).append('\n');
+        for (Set<AuthParam> paramSet : mAuthParams.values()) {
+            for (AuthParam param : paramSet) {
+                sb.append("      ").append(param.toString());
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/ExpandedEAPMethod.java b/packages/Osu/src/com/android/anqp/eap/ExpandedEAPMethod.java
new file mode 100644
index 0000000..1358c09
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/ExpandedEAPMethod.java
@@ -0,0 +1,78 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.INT_MASK;
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class ExpandedEAPMethod implements AuthParam {
+
+    private final EAP.AuthInfoID mAuthInfoID;
+    private final int mVendorID;
+    private final long mVendorType;
+
+    public ExpandedEAPMethod(EAP.AuthInfoID authInfoID, int length, ByteBuffer payload)
+            throws ProtocolException {
+        if (length != 7) {
+            throw new ProtocolException("Bad length: " + payload.remaining());
+        }
+
+        mAuthInfoID = authInfoID;
+
+        ByteBuffer vndBuffer = payload.duplicate().order(ByteOrder.BIG_ENDIAN);
+
+        int id = vndBuffer.getShort() & SHORT_MASK;
+        id = (id << Byte.SIZE) | (vndBuffer.get() & BYTE_MASK);
+        mVendorID = id;
+        mVendorType = vndBuffer.getInt() & INT_MASK;
+
+        payload.position(payload.position()+7);
+    }
+
+    public ExpandedEAPMethod(EAP.AuthInfoID authInfoID, int vendorID, long vendorType) {
+        mAuthInfoID = authInfoID;
+        mVendorID = vendorID;
+        mVendorType = vendorType;
+    }
+
+    @Override
+    public EAP.AuthInfoID getAuthInfoID() {
+        return mAuthInfoID;
+    }
+
+    @Override
+    public int hashCode() {
+        return (mAuthInfoID.hashCode() * 31 + mVendorID) * 31 + (int) mVendorType;
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (thatObject == this) {
+            return true;
+        } else if (thatObject == null || thatObject.getClass() != ExpandedEAPMethod.class) {
+            return false;
+        } else {
+            ExpandedEAPMethod that = (ExpandedEAPMethod) thatObject;
+            return that.getVendorID() == getVendorID() && that.getVendorType() == getVendorType();
+        }
+    }
+
+    public int getVendorID() {
+        return mVendorID;
+    }
+
+    public long getVendorType() {
+        return mVendorType;
+    }
+
+    @Override
+    public String toString() {
+        return "Auth method " + mAuthInfoID + ", id " + mVendorID + ", type " + mVendorType + "\n";
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/InnerAuthEAP.java b/packages/Osu/src/com/android/anqp/eap/InnerAuthEAP.java
new file mode 100644
index 0000000..571cf26
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/InnerAuthEAP.java
@@ -0,0 +1,56 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class InnerAuthEAP implements AuthParam {
+
+    private final EAP.EAPMethodID mEapMethodID;
+
+    public InnerAuthEAP(int length, ByteBuffer payload) throws ProtocolException {
+        if (length != 1) {
+            throw new ProtocolException("Bad length: " + length);
+        }
+        int typeID = payload.get() & BYTE_MASK;
+        mEapMethodID = EAP.mapEAPMethod(typeID);
+    }
+
+    public InnerAuthEAP(EAP.EAPMethodID eapMethodID) {
+        mEapMethodID = eapMethodID;
+    }
+
+    @Override
+    public EAP.AuthInfoID getAuthInfoID() {
+        return EAP.AuthInfoID.InnerAuthEAPMethodType;
+    }
+
+    public EAP.EAPMethodID getEAPMethodID() {
+        return mEapMethodID;
+    }
+
+    @Override
+    public int hashCode() {
+        return mEapMethodID != null ? mEapMethodID.hashCode() : 0;
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (thatObject == this) {
+            return true;
+        } else if (thatObject == null || thatObject.getClass() != InnerAuthEAP.class) {
+            return false;
+        } else {
+            return ((InnerAuthEAP) thatObject).getEAPMethodID() == getEAPMethodID();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Auth method InnerAuthEAP, inner = " + mEapMethodID + '\n';
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/NonEAPInnerAuth.java b/packages/Osu/src/com/android/anqp/eap/NonEAPInnerAuth.java
new file mode 100644
index 0000000..9d37b4d
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/NonEAPInnerAuth.java
@@ -0,0 +1,93 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class NonEAPInnerAuth implements AuthParam {
+
+    public enum NonEAPType {Reserved, PAP, CHAP, MSCHAP, MSCHAPv2}
+    private static final Map<NonEAPType, String> sOmaMap = new EnumMap<>(NonEAPType.class);
+    private static final Map<String, NonEAPType> sRevOmaMap = new HashMap<>();
+
+    private final NonEAPType mType;
+
+    static {
+        sOmaMap.put(NonEAPType.PAP, "PAP");
+        sOmaMap.put(NonEAPType.CHAP, "CHAP");
+        sOmaMap.put(NonEAPType.MSCHAP, "MS-CHAP");
+        sOmaMap.put(NonEAPType.MSCHAPv2, "MS-CHAP-V2");
+
+        for (Map.Entry<NonEAPType, String> entry : sOmaMap.entrySet()) {
+            sRevOmaMap.put(entry.getValue(), entry.getKey());
+        }
+    }
+
+    public NonEAPInnerAuth(int length, ByteBuffer payload) throws ProtocolException {
+        if (length != 1) {
+            throw new ProtocolException("Bad length: " + payload.remaining());
+        }
+
+        int typeID = payload.get() & BYTE_MASK;
+        mType = typeID < NonEAPType.values().length ?
+                NonEAPType.values()[typeID] :
+                NonEAPType.Reserved;
+    }
+
+    public NonEAPInnerAuth(NonEAPType type) {
+        mType = type;
+    }
+
+    /**
+     * Construct from the OMA-DM PPS data
+     * @param eapType as defined in the HS2.0 spec.
+     */
+    public NonEAPInnerAuth(String eapType) {
+        mType = sRevOmaMap.get(eapType);
+    }
+
+    @Override
+    public EAP.AuthInfoID getAuthInfoID() {
+        return EAP.AuthInfoID.NonEAPInnerAuthType;
+    }
+
+    public NonEAPType getType() {
+        return mType;
+    }
+
+    public String getOMAtype() {
+        return sOmaMap.get(mType);
+    }
+
+    public static String mapInnerType(NonEAPType type) {
+        return sOmaMap.get(type);
+    }
+
+    @Override
+    public int hashCode() {
+        return mType.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (thatObject == this) {
+            return true;
+        } else if (thatObject == null || thatObject.getClass() != NonEAPInnerAuth.class) {
+            return false;
+        } else {
+            return ((NonEAPInnerAuth) thatObject).getType() == getType();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Auth method NonEAPInnerAuthEAP, inner = " + mType + '\n';
+    }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/VendorSpecificAuth.java b/packages/Osu/src/com/android/anqp/eap/VendorSpecificAuth.java
new file mode 100644
index 0000000..04a315d
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/VendorSpecificAuth.java
@@ -0,0 +1,47 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class VendorSpecificAuth implements AuthParam {
+
+    private final byte[] mData;
+
+    public VendorSpecificAuth(int length, ByteBuffer payload) throws ProtocolException {
+        mData = new byte[length];
+        payload.get(mData);
+    }
+
+    @Override
+    public EAP.AuthInfoID getAuthInfoID() {
+        return EAP.AuthInfoID.VendorSpecific;
+    }
+
+    public int hashCode() {
+        return Arrays.hashCode(mData);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (thatObject == this) {
+            return true;
+        } else if (thatObject == null || thatObject.getClass() != VendorSpecificAuth.class) {
+            return false;
+        } else {
+            return Arrays.equals(((VendorSpecificAuth) thatObject).getData(), getData());
+        }
+    }
+
+    public byte[] getData() {
+        return mData;
+    }
+
+    @Override
+    public String toString() {
+        return "Auth method VendorSpecificAuth, data = " + Arrays.toString(mData) + '\n';
+    }
+}
diff --git a/packages/Osu/src/com/android/configparse/ConfigBuilder.java b/packages/Osu/src/com/android/configparse/ConfigBuilder.java
new file mode 100644
index 0000000..b760ade
--- /dev/null
+++ b/packages/Osu/src/com/android/configparse/ConfigBuilder.java
@@ -0,0 +1,258 @@
+package com.android.configparse;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.anqp.eap.AuthParam;
+import com.android.anqp.eap.EAP;
+import com.android.anqp.eap.EAPMethod;
+import com.android.anqp.eap.NonEAPInnerAuth;
+import com.android.hotspot2.IMSIParameter;
+import com.android.hotspot2.pps.Credential;
+import com.android.hotspot2.pps.HomeSP;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+public class ConfigBuilder {
+    private static final String TAG = "WCFG";
+
+    private static void dropFile(Uri uri, Context context) {
+        context.getContentResolver().delete(uri, null, null);
+    }
+
+    public static WifiConfiguration buildConfig(HomeSP homeSP, X509Certificate caCert,
+                                                 List<X509Certificate> clientChain, PrivateKey key)
+            throws IOException, GeneralSecurityException {
+
+        Credential credential = homeSP.getCredential();
+
+        WifiConfiguration config;
+
+        EAP.EAPMethodID eapMethodID = credential.getEAPMethod().getEAPMethodID();
+        switch (eapMethodID) {
+            case EAP_TTLS:
+                if (key != null || clientChain != null) {
+                    Log.w(TAG, "Client cert and/or key included with EAP-TTLS profile");
+                }
+                config = buildTTLSConfig(homeSP);
+                break;
+            case EAP_TLS:
+                config = buildTLSConfig(homeSP, clientChain, key);
+                break;
+            case EAP_AKA:
+            case EAP_AKAPrim:
+            case EAP_SIM:
+                if (key != null || clientChain != null || caCert != null) {
+                    Log.i(TAG, "Client/CA cert and/or key included with " +
+                            eapMethodID + " profile");
+                }
+                config = buildSIMConfig(homeSP);
+                break;
+            default:
+                throw new IOException("Unsupported EAP Method: " + eapMethodID);
+        }
+
+        WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
+
+        enterpriseConfig.setCaCertificate(caCert);
+        enterpriseConfig.setAnonymousIdentity("anonymous@" + credential.getRealm());
+
+        return config;
+    }
+
+    // Retain for debugging purposes
+    /*
+    private static void xIterateCerts(KeyStore ks, X509Certificate caCert)
+            throws GeneralSecurityException {
+        Enumeration<String> aliases = ks.aliases();
+        while (aliases.hasMoreElements()) {
+            String alias = aliases.nextElement();
+            Certificate cert = ks.getCertificate(alias);
+            Log.d("HS2J", "Checking " + alias);
+            if (cert instanceof X509Certificate) {
+                X509Certificate x509Certificate = (X509Certificate) cert;
+                boolean sm = x509Certificate.getSubjectX500Principal().equals(
+                        caCert.getSubjectX500Principal());
+                boolean eq = false;
+                if (sm) {
+                    eq = Arrays.equals(x509Certificate.getEncoded(), caCert.getEncoded());
+                }
+                Log.d("HS2J", "Subject: " + x509Certificate.getSubjectX500Principal() +
+                        ": " + sm + "/" + eq);
+            }
+        }
+    }
+    */
+
+    private static WifiConfiguration buildTTLSConfig(HomeSP homeSP)
+            throws IOException {
+        Credential credential = homeSP.getCredential();
+
+        if (credential.getUserName() == null || credential.getPassword() == null) {
+            throw new IOException("EAP-TTLS provisioned without user name or password");
+        }
+
+        EAPMethod eapMethod = credential.getEAPMethod();
+
+        AuthParam authParam = eapMethod.getAuthParam();
+        if (authParam == null ||
+                authParam.getAuthInfoID() != EAP.AuthInfoID.NonEAPInnerAuthType) {
+            throw new IOException("Bad auth parameter for EAP-TTLS: " + authParam);
+        }
+
+        WifiConfiguration config = buildBaseConfiguration(homeSP);
+        NonEAPInnerAuth ttlsParam = (NonEAPInnerAuth) authParam;
+        WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
+        enterpriseConfig.setPhase2Method(remapInnerMethod(ttlsParam.getType()));
+        enterpriseConfig.setIdentity(credential.getUserName());
+        enterpriseConfig.setPassword(credential.getPassword());
+
+        return config;
+    }
+
+    private static WifiConfiguration buildTLSConfig(HomeSP homeSP,
+                                                    List<X509Certificate> clientChain,
+                                                    PrivateKey clientKey)
+            throws IOException, GeneralSecurityException {
+
+        Credential credential = homeSP.getCredential();
+
+        X509Certificate clientCertificate = null;
+
+        if (clientKey == null || clientChain == null) {
+            throw new IOException("No key and/or cert passed for EAP-TLS");
+        }
+        if (credential.getCertType() != Credential.CertType.x509v3) {
+            throw new IOException("Invalid certificate type for TLS: " +
+                    credential.getCertType());
+        }
+
+        byte[] reference = credential.getFingerPrint();
+        MessageDigest digester = MessageDigest.getInstance("SHA-256");
+        for (X509Certificate certificate : clientChain) {
+            digester.reset();
+            byte[] fingerprint = digester.digest(certificate.getEncoded());
+            if (Arrays.equals(reference, fingerprint)) {
+                clientCertificate = certificate;
+                break;
+            }
+        }
+        if (clientCertificate == null) {
+            throw new IOException("No certificate in chain matches supplied fingerprint");
+        }
+
+        String alias = Base64.encodeToString(reference, Base64.DEFAULT);
+
+        WifiConfiguration config = buildBaseConfiguration(homeSP);
+        WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
+        enterpriseConfig.setClientCertificateAlias(alias);
+        enterpriseConfig.setClientKeyEntry(clientKey, clientCertificate);
+
+        return config;
+    }
+
+    private static WifiConfiguration buildSIMConfig(HomeSP homeSP)
+            throws IOException {
+
+        Credential credential = homeSP.getCredential();
+        IMSIParameter credImsi = credential.getImsi();
+
+        /*
+         * Uncomment to enforce strict IMSI matching with currently installed SIM cards.
+         *
+        TelephonyManager tm = TelephonyManager.from(context);
+        SubscriptionManager sub = SubscriptionManager.from(context);
+        boolean match = false;
+
+        for (int subId : sub.getActiveSubscriptionIdList()) {
+            String imsi = tm.getSubscriberId(subId);
+            if (credImsi.matches(imsi)) {
+                match = true;
+                break;
+            }
+        }
+        if (!match) {
+            throw new IOException("Supplied IMSI does not match any SIM card");
+        }
+        */
+
+        WifiConfiguration config = buildBaseConfiguration(homeSP);
+        config.enterpriseConfig.setPlmn(credImsi.toString());
+        return config;
+    }
+
+    private static WifiConfiguration buildBaseConfiguration(HomeSP homeSP) throws IOException {
+        EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID();
+
+        WifiConfiguration config = new WifiConfiguration();
+
+        config.FQDN = homeSP.getFQDN();
+
+        HashSet<Long> roamingConsortiumIds = homeSP.getRoamingConsortiums();
+        config.roamingConsortiumIds = new long[roamingConsortiumIds.size()];
+        int i = 0;
+        for (long id : roamingConsortiumIds) {
+            config.roamingConsortiumIds[i] = id;
+            i++;
+        }
+        config.providerFriendlyName = homeSP.getFriendlyName();
+
+        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
+        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
+
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(remapEAPMethod(eapMethodID));
+        enterpriseConfig.setRealm(homeSP.getCredential().getRealm());
+        if (homeSP.getUpdateIdentifier() >= 0) {
+            config.updateIdentifier = Integer.toString(homeSP.getUpdateIdentifier());
+        }
+        config.enterpriseConfig = enterpriseConfig;
+        if (homeSP.getUpdateIdentifier() >= 0) {
+            config.updateIdentifier = Integer.toString(homeSP.getUpdateIdentifier());
+        }
+
+        return config;
+    }
+
+    private static int remapEAPMethod(EAP.EAPMethodID eapMethodID) throws IOException {
+        switch (eapMethodID) {
+            case EAP_TTLS:
+                return WifiEnterpriseConfig.Eap.TTLS;
+            case EAP_TLS:
+                return WifiEnterpriseConfig.Eap.TLS;
+            case EAP_SIM:
+                return WifiEnterpriseConfig.Eap.SIM;
+            case EAP_AKA:
+                return WifiEnterpriseConfig.Eap.AKA;
+            case EAP_AKAPrim:
+                return WifiEnterpriseConfig.Eap.AKA_PRIME;
+            default:
+                throw new IOException("Bad EAP method: " + eapMethodID);
+        }
+    }
+
+    private static int remapInnerMethod(NonEAPInnerAuth.NonEAPType type) throws IOException {
+        switch (type) {
+            case PAP:
+                return WifiEnterpriseConfig.Phase2.PAP;
+            case MSCHAP:
+                return WifiEnterpriseConfig.Phase2.MSCHAP;
+            case MSCHAPv2:
+                return WifiEnterpriseConfig.Phase2.MSCHAPV2;
+            case CHAP:
+            default:
+                throw new IOException("Inner method " + type + " not supported");
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/AppBridge.java b/packages/Osu/src/com/android/hotspot2/AppBridge.java
new file mode 100644
index 0000000..95f5970
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/AppBridge.java
@@ -0,0 +1,63 @@
+package com.android.hotspot2;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.hotspot2.osu.OSUInfo;
+import com.android.hotspot2.osu.OSUOperationStatus;
+
+import java.util.List;
+
+public class AppBridge {
+    public static final String ACTION_OSU_NOTIFICATION = "com.android.hotspot2.OSU_NOTIFICATION";
+    public static final String OSU_COUNT = "osu-count";
+    public static final String SP_NAME = "sp-name";
+    public static final String PROV_SUCCESS = "prov-success";
+    public static final String DEAUTH = "deauth";
+    public static final String DEAUTH_DELAY = "deauth-delay";
+    public static final String DEAUTH_URL = "deauth-url";
+    public static final String PROV_MESSAGE = "prov-message";
+    public static final String OSU_INFO = "osu-info";
+
+    public static final String GET_OSUS_ACTION = "com.android.hotspot2.GET_OSUS";
+
+    private final Context mContext;
+
+    public AppBridge(Context context) {
+        mContext = context;
+    }
+
+    public void showOsuCount(int osuCount, List<OSUInfo> osus) {
+        Intent intent = new Intent(ACTION_OSU_NOTIFICATION);
+        intent.putExtra(OSU_COUNT, osuCount);
+        intent.setFlags(
+                Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        mContext.startActivity(intent);
+    }
+
+    public void showStatus(OSUOperationStatus status, String spName, String message,
+                           String remoteStatus) {
+        Intent intent = new Intent(ACTION_OSU_NOTIFICATION);
+        intent.putExtra(SP_NAME, spName);
+        intent.putExtra(PROV_SUCCESS, status == OSUOperationStatus.ProvisioningSuccess);
+        if (message != null) {
+            intent.putExtra(PROV_MESSAGE, message);
+        }
+        intent.setFlags(
+                Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+
+    public void showDeauth(String spName, boolean ess, int delay, String url) {
+        Intent intent = new Intent(ACTION_OSU_NOTIFICATION);
+        intent.putExtra(SP_NAME, spName);
+        intent.putExtra(DEAUTH, ess);
+        intent.putExtra(DEAUTH_DELAY, delay);
+        intent.putExtra(DEAUTH_URL, url);
+        intent.setFlags(
+                Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/AuthMatch.java b/packages/Osu/src/com/android/hotspot2/AuthMatch.java
new file mode 100644
index 0000000..f9c1f42
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/AuthMatch.java
@@ -0,0 +1,39 @@
+package com.android.hotspot2;
+
+/**
+ * Match score for EAP credentials:
+ * None means that there is a distinct mismatch, i.e. realm, method or parameter is defined
+ * and mismatches that of the credential.
+ * Indeterminate means that there is no ANQP information to match against.
+ * Note: The numeric values given to the constants are used for preference comparison and
+ * must be maintained accordingly.
+ */
+public abstract class AuthMatch {
+    public static final int None = -1;
+    public static final int Indeterminate = 0;
+    public static final int Realm = 0x04;
+    public static final int Method = 0x02;
+    public static final int Param = 0x01;
+    public static final int MethodParam = Method | Param;
+    public static final int Exact = Realm | Method | Param;
+
+    public static String toString(int match) {
+        if (match < 0) {
+            return "None";
+        } else if (match == 0) {
+            return "Indeterminate";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        if ((match & Realm) != 0) {
+            sb.append("Realm");
+        }
+        if ((match & Method) != 0) {
+            sb.append("Method");
+        }
+        if ((match & Param) != 0) {
+            sb.append("Param");
+        }
+        return sb.toString();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/IMSIParameter.java b/packages/Osu/src/com/android/hotspot2/IMSIParameter.java
new file mode 100644
index 0000000..1d5d95d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/IMSIParameter.java
@@ -0,0 +1,96 @@
+package com.android.hotspot2;
+
+import java.io.IOException;
+
+public class IMSIParameter {
+    private final String mImsi;
+    private final boolean mPrefix;
+
+    public IMSIParameter(String imsi, boolean prefix) {
+        mImsi = imsi;
+        mPrefix = prefix;
+    }
+
+    public IMSIParameter(String imsi) throws IOException {
+        if (imsi == null || imsi.length() == 0) {
+            throw new IOException("Bad IMSI: '" + imsi + "'");
+        }
+
+        int nonDigit;
+        char stopChar = '\0';
+        for (nonDigit = 0; nonDigit < imsi.length(); nonDigit++) {
+            stopChar = imsi.charAt(nonDigit);
+            if (stopChar < '0' || stopChar > '9') {
+                break;
+            }
+        }
+
+        if (nonDigit == imsi.length()) {
+            mImsi = imsi;
+            mPrefix = false;
+        } else if (nonDigit == imsi.length() - 1 && stopChar == '*') {
+            mImsi = imsi.substring(0, nonDigit);
+            mPrefix = true;
+        } else {
+            throw new IOException("Bad IMSI: '" + imsi + "'");
+        }
+    }
+
+    public boolean matches(String fullIMSI) {
+        if (mPrefix) {
+            return mImsi.regionMatches(false, 0, fullIMSI, 0, mImsi.length());
+        } else {
+            return mImsi.equals(fullIMSI);
+        }
+    }
+
+    public boolean matchesMccMnc(String mccMnc) {
+        if (mPrefix) {
+            // For a prefix match, the entire prefix must match the mcc+mnc
+            return mImsi.regionMatches(false, 0, mccMnc, 0, mImsi.length());
+        } else {
+            // For regular match, the entire length of mcc+mnc must match this IMSI
+            return mImsi.regionMatches(false, 0, mccMnc, 0, mccMnc.length());
+        }
+    }
+
+    public boolean isPrefix() {
+        return mPrefix;
+    }
+
+    public String getImsi() {
+        return mImsi;
+    }
+
+    public int prefixLength() {
+        return mImsi.length();
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        } else if (thatObject == null || getClass() != thatObject.getClass()) {
+            return false;
+        }
+
+        IMSIParameter that = (IMSIParameter) thatObject;
+        return mPrefix == that.mPrefix && mImsi.equals(that.mImsi);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mImsi != null ? mImsi.hashCode() : 0;
+        result = 31 * result + (mPrefix ? 1 : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        if (mPrefix) {
+            return mImsi + '*';
+        } else {
+            return mImsi;
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/OMADMAdapter.java b/packages/Osu/src/com/android/hotspot2/OMADMAdapter.java
new file mode 100644
index 0000000..1429b0b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/OMADMAdapter.java
@@ -0,0 +1,601 @@
+package com.android.hotspot2;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.wifi.WifiManager;
+import android.os.SystemProperties;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.anqp.eap.EAP;
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMAConstructed;
+import com.android.hotspot2.osu.OSUManager;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.android.anqp.eap.NonEAPInnerAuth.NonEAPType;
+import static com.android.anqp.eap.NonEAPInnerAuth.mapInnerType;
+
+public class OMADMAdapter {
+    private final Context mContext;
+    private final String mImei;
+    private final String mImsi;
+    private final String mDevID;
+    private final List<PathAccessor> mDevInfo;
+    private final List<PathAccessor> mDevDetail;
+
+    private static final int IMEI_Length = 14;
+
+    private static final String[] ExtWiFiPath = {"DevDetail", "Ext", "org.wi-fi", "Wi-Fi"};
+
+    private static final Map<String, String> RTProps = new HashMap<>();
+
+    private MOTree mDevInfoTree;
+    private MOTree mDevDetailTree;
+
+    private static OMADMAdapter sInstance;
+
+    static {
+        RTProps.put(ExtWiFiPath[2], "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext:1.0");
+    }
+
+    private static abstract class PathAccessor {
+        private final String[] mPath;
+        private final int mHashCode;
+
+        protected PathAccessor(Object... path) {
+            int length = 0;
+            for (Object o : path) {
+                if (o.getClass() == String[].class) {
+                    length += ((String[]) o).length;
+                } else {
+                    length++;
+                }
+            }
+            mPath = new String[length];
+            int n = 0;
+            for (Object o : path) {
+                if (o.getClass() == String[].class) {
+                    for (String element : (String[]) o) {
+                        mPath[n++] = element;
+                    }
+                } else if (o.getClass() == Integer.class) {
+                    mPath[n++] = "x" + o.toString();
+                } else {
+                    mPath[n++] = o.toString();
+                }
+            }
+            mHashCode = Arrays.hashCode(mPath);
+        }
+
+        @Override
+        public int hashCode() {
+            return mHashCode;
+        }
+
+        @Override
+        public boolean equals(Object thatObject) {
+            return thatObject == this || (thatObject instanceof ConstPathAccessor &&
+                    Arrays.equals(mPath, ((PathAccessor) thatObject).mPath));
+        }
+
+        private String[] getPath() {
+            return mPath;
+        }
+
+        protected abstract Object getValue();
+    }
+
+    private static class ConstPathAccessor<T> extends PathAccessor {
+        private final T mValue;
+
+        protected ConstPathAccessor(T value, Object... path) {
+            super(path);
+            mValue = value;
+        }
+
+        protected Object getValue() {
+            return mValue;
+        }
+    }
+
+    public static OMADMAdapter getInstance(Context context) {
+        synchronized (OMADMAdapter.class) {
+            if (sInstance == null) {
+                sInstance = new OMADMAdapter(context);
+            }
+            return sInstance;
+        }
+    }
+
+    private OMADMAdapter(Context context) {
+        mContext = context;
+
+        TelephonyManager tm = (TelephonyManager) context
+                .getSystemService(Context.TELEPHONY_SERVICE);
+        String simOperator = tm.getSimOperator();
+        mImsi = tm.getSubscriberId();
+        mImei = tm.getImei();
+        String strDevId;
+
+        /* Use MEID for sprint */
+        if ("310120".equals(simOperator) || (mImsi != null && mImsi.startsWith("310120"))) {
+                /* MEID is 14 digits. If IMEI is returned as DevId, MEID can be extracted by taking
+                 * first 14 characters. This is not always true but should be the case for sprint */
+            strDevId = tm.getDeviceId().toUpperCase(Locale.US);
+            if (strDevId != null && strDevId.length() >= IMEI_Length) {
+                strDevId = strDevId.substring(0, IMEI_Length);
+            } else {
+                Log.w(OSUManager.TAG, "MEID cannot be extracted from DeviceId " + strDevId);
+            }
+        } else {
+            if (isPhoneTypeLTE()) {
+                strDevId = mImei;
+            } else {
+                strDevId = tm.getDeviceId();
+            }
+            if (strDevId == null) {
+                strDevId = "unknown";
+            }
+            strDevId = strDevId.toUpperCase(Locale.US);
+
+            if (!isPhoneTypeLTE()) {
+                strDevId = strDevId.substring(0, IMEI_Length);
+            }
+        }
+        mDevID = strDevId;
+
+        mDevInfo = new ArrayList<>();
+        mDevInfo.add(new ConstPathAccessor<>(strDevId, "DevInfo", "DevID"));
+        mDevInfo.add(new ConstPathAccessor<>(getProperty(context,
+                "Man", "ro.product.manufacturer", "unknown"), "DevInfo", "Man"));
+        mDevInfo.add(new ConstPathAccessor<>(getProperty(context,
+                "Mod", "ro.product.model", "generic"), "DevInfo", "Mod"));
+        mDevInfo.add(new ConstPathAccessor<>(getLocale(context), "DevInfo", "Lang"));
+        mDevInfo.add(new ConstPathAccessor<>("1.2", "DevInfo", "DmV"));
+
+        mDevDetail = new ArrayList<>();
+        mDevDetail.add(new ConstPathAccessor<>(getDeviceType(), "DevDetail", "DevType"));
+        mDevDetail.add(new ConstPathAccessor<>(SystemProperties.get("ro.product.brand"),
+                "DevDetail", "OEM"));
+        mDevDetail.add(new ConstPathAccessor<>(getVersion(context, false), "DevDetail", "FwV"));
+        mDevDetail.add(new ConstPathAccessor<>(getVersion(context, true), "DevDetail", "SwV"));
+        mDevDetail.add(new ConstPathAccessor<>(getHwV(), "DevDetail", "HwV"));
+        mDevDetail.add(new ConstPathAccessor<>("TRUE", "DevDetail", "LrgObj"));
+
+        mDevDetail.add(new ConstPathAccessor<>(32, "DevDetail", "URI", "MaxDepth"));
+        mDevDetail.add(new ConstPathAccessor<>(2048, "DevDetail", "URI", "MaxTotLen"));
+        mDevDetail.add(new ConstPathAccessor<>(64, "DevDetail", "URI", "MaxSegLen"));
+
+        AtomicInteger index = new AtomicInteger(1);
+        mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TTLS, ExtWiFiPath,
+                "EAPMethodList", index, "EAPType"));
+        mDevDetail.add(new ConstPathAccessor<>(mapInnerType(NonEAPType.MSCHAPv2), ExtWiFiPath,
+                "EAPMethodList", index, "InnerMethod"));
+
+        index.incrementAndGet();
+        mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TTLS, ExtWiFiPath,
+                "EAPMethodList", index, "EAPType"));
+        mDevDetail.add(new ConstPathAccessor<>(mapInnerType(NonEAPType.PAP), ExtWiFiPath,
+                "EAPMethodList", index, "InnerMethod"));
+
+        index.incrementAndGet();
+        mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TTLS, ExtWiFiPath,
+                "EAPMethodList", index, "EAPType"));
+        mDevDetail.add(new ConstPathAccessor<>(mapInnerType(NonEAPType.MSCHAP), ExtWiFiPath,
+                "EAPMethodList", index, "InnerMethod"));
+
+        index.incrementAndGet();
+        mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TLS, ExtWiFiPath,
+                "EAPMethodList", index, "EAPType"));
+        index.incrementAndGet();
+        mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_AKA, ExtWiFiPath,
+                "EAPMethodList", index, "EAPType"));
+        index.incrementAndGet();
+        mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_AKAPrim, ExtWiFiPath,
+                "EAPMethodList", index, "EAPType"));
+        index.incrementAndGet();
+        mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_SIM, ExtWiFiPath,
+                "EAPMethodList", index, "EAPType"));
+
+        mDevDetail.add(new ConstPathAccessor<>("FALSE", ExtWiFiPath, "ManufacturingCertificate"));
+        mDevDetail.add(new ConstPathAccessor<>(mImsi, ExtWiFiPath, "IMSI"));
+        mDevDetail.add(new ConstPathAccessor<>(mImei, ExtWiFiPath, "IMEI_MEID"));
+        mDevDetail.add(new PathAccessor(ExtWiFiPath, "Wi-FiMACAddress") {
+            @Override
+            protected String getValue() {
+                return getMAC();
+            }
+        });
+    }
+
+    private static void buildNode(PathAccessor pathAccessor, int depth, OMAConstructed parent)
+            throws IOException {
+        String[] path = pathAccessor.getPath();
+        String name = path[depth];
+        if (depth < path.length - 1) {
+            OMAConstructed node = (OMAConstructed) parent.getChild(name);
+            if (node == null) {
+                node = (OMAConstructed) parent.addChild(name, RTProps.get(name),
+                        null, null);
+            }
+            buildNode(pathAccessor, depth + 1, node);
+        } else if (pathAccessor.getValue() != null) {
+            parent.addChild(name, null, pathAccessor.getValue().toString(), null);
+        }
+    }
+
+    public String getMAC() {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        return wifiManager != null ?
+                String.format("%012x",
+                        Utils.parseMac(wifiManager.getConnectionInfo().getMacAddress())) :
+                null;
+    }
+
+    public String getImei() {
+        return mImei;
+    }
+
+    public byte[] getMeid() {
+        return Arrays.copyOf(mImei.getBytes(StandardCharsets.ISO_8859_1), IMEI_Length);
+    }
+
+    public String getDevID() {
+        return mDevID;
+    }
+
+    public MOTree getMO(String urn) {
+        try {
+            switch (urn) {
+                case OMAConstants.DevInfoURN:
+                    if (mDevInfoTree == null) {
+                        OMAConstructed root = new OMAConstructed(null, "DevInfo", urn);
+                        for (PathAccessor pathAccessor : mDevInfo) {
+                            buildNode(pathAccessor, 1, root);
+                        }
+                        mDevInfoTree = MOTree.buildMgmtTree(OMAConstants.DevInfoURN,
+                                OMAConstants.OMAVersion, root);
+                    }
+                    return mDevInfoTree;
+                case OMAConstants.DevDetailURN:
+                    if (mDevDetailTree == null) {
+                        OMAConstructed root = new OMAConstructed(null, "DevDetail", urn);
+                        for (PathAccessor pathAccessor : mDevDetail) {
+                            buildNode(pathAccessor, 1, root);
+                        }
+                        mDevDetailTree = MOTree.buildMgmtTree(OMAConstants.DevDetailURN,
+                                OMAConstants.OMAVersion, root);
+                    }
+                    return mDevDetailTree;
+                default:
+                    throw new IllegalArgumentException(urn);
+            }
+        } catch (IOException ioe) {
+            Log.e(OSUManager.TAG, "Caught exception building OMA Tree: " + ioe, ioe);
+            return null;
+        }
+
+        /*
+        switch (urn) {
+            case DevInfoURN: return DevInfo;
+            case DevDetailURN: return DevDetail;
+            default: throw new IllegalArgumentException(urn);
+        }
+        */
+    }
+
+    // TODO: For now, assume the device supports LTE.
+    private static boolean isPhoneTypeLTE() {
+        return true;
+    }
+
+    private static String getHwV() {
+        try {
+            return SystemProperties.get("ro.hardware", "Unknown")
+                    + "." + SystemProperties.get("ro.revision", "Unknown");
+        } catch (RuntimeException e) {
+            return "Unknown";
+        }
+    }
+
+    private static String getDeviceType() {
+        String devicetype = SystemProperties.get("ro.build.characteristics");
+        if ((((TextUtils.isEmpty(devicetype)) || (!devicetype.equals("tablet"))))) {
+            devicetype = "phone";
+        }
+        return devicetype;
+    }
+
+    private static String getVersion(Context context, boolean swv) {
+        String version;
+        try {
+            if (!isSprint(context) && swv) {
+                return "Android " + SystemProperties.get("ro.build.version.release");
+            } else {
+                version = SystemProperties.get("ro.build.version.full");
+                if (null == version || version.equals("")) {
+                    return SystemProperties.get("ro.build.id", null) + "~"
+                            + SystemProperties.get("ro.build.config.version", null) + "~"
+                            + SystemProperties.get("gsm.version.baseband", null) + "~"
+                            + SystemProperties.get("ro.gsm.flexversion", null);
+                }
+            }
+        } catch (RuntimeException e) {
+            return "Unknown";
+        }
+        return version;
+    }
+
+    private static boolean isSprint(Context context) {
+        TelephonyManager tm = (TelephonyManager) context
+                .getSystemService(Context.TELEPHONY_SERVICE);
+        String simOperator = tm.getSimOperator();
+        String imsi = tm.getSubscriberId();
+        /* Use MEID for sprint */
+        if ("310120".equals(simOperator) || (imsi != null && imsi.startsWith("310120"))) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private static String getLocale(Context context) {
+        String strLang = readValueFromFile(context, "Lang");
+        if (strLang == null) {
+            strLang = Locale.getDefault().toString();
+        }
+        return strLang;
+    }
+
+    private static String getProperty(Context context, String key, String propKey, String dflt) {
+        String strMan = readValueFromFile(context, key);
+        if (strMan == null) {
+            strMan = SystemProperties.get(propKey, dflt);
+        }
+        return strMan;
+    }
+
+    private static String readValueFromFile(Context context, String propName) {
+        String ret = null;
+        // use preference instead of the system property
+        SharedPreferences prefs = context.getSharedPreferences("dmconfig", 0);
+        if (prefs.contains(propName)) {
+            ret = prefs.getString(propName, "");
+            if (ret.length() == 0) {
+                ret = null;
+            }
+        }
+        return ret;
+    }
+
+    private static final String DevDetail =
+            "<MgmtTree>" +
+                    "<VerDTD>1.2</VerDTD>" +
+                    "<Node>" +
+                    "<NodeName>DevDetail</NodeName>" +
+                    "<RTProperties>" +
+                    "<Type>" +
+                    "<DDFName>urn:oma:mo:oma-dm-devdetail:1.0</DDFName>" +
+                    "</Type>" +
+                    "</RTProperties>" +
+                    "<Node>" +
+                    "<NodeName>Ext</NodeName>" +
+                    "<Node>" +
+                    "<NodeName>org.wi-fi</NodeName>" +
+                    "<RTProperties>" +
+                    "<Type>" +
+                    "<DDFName>" +
+                    "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext :1.0" +
+                    "</DDFName>" +
+                    "</Type>" +
+                    "</RTProperties>" +
+                    "<Node>" +
+                    "<NodeName>Wi-Fi</NodeName>" +
+                    "<Node>" +
+                    "<NodeName>EAPMethodList</NodeName>" +
+                    "<Node>" +
+                    "<NodeName>Method01</NodeName>" +
+                    "<!-- EAP-TTLS/MS-CHAPv2 -->" +
+                    "<Node>" +
+                    "<NodeName>EAPType</NodeName>" +
+                    "<Value>21</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>InnerMethod</NodeName>" +
+                    "<Value>MS-CHAP-V2</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Method02</NodeName>" +
+                    "<!-- EAP-TLS -->" +
+                    "<Node>" +
+                    "<NodeName>EAPType</NodeName>" +
+                    "<Value>13</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Method03</NodeName>" +
+                    "<!-- EAP-SIM -->" +
+                    "<Node>" +
+                    "<NodeName>EAPType</NodeName>" +
+                    "<Value>18</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Method04</NodeName>" +
+                    "<!-- EAP-AKA -->" +
+                    "<Node>" +
+                    "<NodeName>EAPType</NodeName>" +
+                    "<Value>23</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Method05</NodeName>" +
+                    "<!-- EAP-AKA' -->" +
+                    "<Node>" +
+                    "<NodeName>EAPType</NodeName>" +
+                    "<Value>50</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Method06</NodeName>" +
+                    "<!-- Supported method (EAP-TTLS/PAP) not mandated by Hotspot2.0-->" +
+                    "<Node>" +
+                    "<NodeName>EAPType</NodeName>" +
+                    "<Value>21</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>InnerMethod</NodeName>" +
+                    "<Value>PAP</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Method07</NodeName>" +
+                    "<!-- Supported method (PEAP/EAP-GTC) not mandated by Hotspot 2.0-->" +
+                    "<Node>" +
+                    "<NodeName>EAPType</NodeName>" +
+                    "<Value>25</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>InnerEAPType</NodeName>" +
+                    "<Value>6</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>SPCertificate</NodeName>" +
+                    "<Node>" +
+                    "<NodeName>Cert01</NodeName>" +
+                    "<Node>" +
+                    "<NodeName>CertificateIssuerName</NodeName>" +
+                    "<Value>CN=RuckusCA</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>ManufacturingCertificate</NodeName>" +
+                    "<Value>FALSE</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Wi-FiMACAddress</NodeName>" +
+                    "<Value>001d2e112233</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>ClientTriggerRedirectURI</NodeName>" +
+                    "<Value>http://127.0.0.1:12345/index.htm</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Ops</NodeName>" +
+                    "<Node>" +
+                    "<NodeName>launchBrowserToURI</NodeName>" +
+                    "<Value></Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>negotiateClientCertTLS</NodeName>" +
+                    "<Value></Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>getCertificate</NodeName>" +
+                    "<Value></Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<!-- End of Wi-Fi node -->" +
+                    "</Node>" +
+                    "<!-- End of org.wi-fi node -->" +
+                    "</Node>" +
+                    "<!-- End of Ext node -->" +
+                    "<Node>" +
+                    "<NodeName>URI</NodeName>" +
+                    "<Node>" +
+                    "<NodeName>MaxDepth</NodeName>" +
+                    "<Value>32</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>MaxTotLen</NodeName>" +
+                    "<Value>2048</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>MaxSegLen</NodeName>" +
+                    "<Value>64</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>DevType</NodeName>" +
+                    "<Value>Smartphone</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>OEM</NodeName>" +
+                    "<Value>ACME</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>FwV</NodeName>" +
+                    "<Value>1.2.100.5</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>SwV</NodeName>" +
+                    "<Value>9.11.130</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>HwV</NodeName>" +
+                    "<Value>1.0</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>LrgObj</NodeName>" +
+                    "<Value>TRUE</Value>" +
+                    "</Node>" +
+                    "</Node>" +
+                    "</MgmtTree>";
+
+
+    private static final String DevInfo =
+            "<MgmtTree>" +
+                    "<VerDTD>1.2</VerDTD>" +
+                    "<Node>" +
+                    "<NodeName>DevInfo</NodeName>" +
+                    "<RTProperties>" +
+                    "<Type>" +
+                    "<DDFName>urn:oma:mo:oma-dm-devinfo:1.0" +
+                    "</DDFName>" +
+                    "</Type>" +
+                    "</RTProperties>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>DevID</NodeName>" +
+                    "<Path>DevInfo</Path>" +
+                    "<Value>urn:acme:00-11-22-33-44-55</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Man</NodeName>" +
+                    "<Path>DevInfo</Path>" +
+                    "<Value>ACME</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Mod</NodeName>" +
+                    "<Path>DevInfo</Path>" +
+                    "<Value>HS2.0-01</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>DmV</NodeName>" +
+                    "<Path>DevInfo</Path>" +
+                    "<Value>1.2</Value>" +
+                    "</Node>" +
+                    "<Node>" +
+                    "<NodeName>Lang</NodeName>" +
+                    "<Path>DevInfo</Path>" +
+                    "<Value>en-US</Value>" +
+                    "</Node>" +
+                    "</MgmtTree>";
+}
diff --git a/packages/Osu/src/com/android/hotspot2/PasspointMatch.java b/packages/Osu/src/com/android/hotspot2/PasspointMatch.java
new file mode 100644
index 0000000..8330283
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/PasspointMatch.java
@@ -0,0 +1,9 @@
+package com.android.hotspot2;
+
+public enum PasspointMatch {
+    HomeProvider,
+    RoamingProvider,
+    Incomplete,
+    None,
+    Declined
+}
diff --git a/packages/Osu/src/com/android/hotspot2/Utils.java b/packages/Osu/src/com/android/hotspot2/Utils.java
new file mode 100644
index 0000000..100b967
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/Utils.java
@@ -0,0 +1,300 @@
+package com.android.hotspot2;
+
+import com.android.anqp.Constants;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TimeZone;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.NIBBLE_MASK;
+
+public abstract class Utils {
+
+    public static final long UNSET_TIME = -1;
+
+    private static final int EUI48Length = 6;
+    private static final int EUI64Length = 8;
+    private static final long EUI48Mask = 0xffffffffffffL;
+    private static final String[] PLMNText = {"org", "3gppnetwork", "mcc*", "mnc*", "wlan"};
+
+    public static List<String> splitDomain(String domain) {
+
+        if (domain.endsWith("."))
+            domain = domain.substring(0, domain.length() - 1);
+        int at = domain.indexOf('@');
+        if (at >= 0)
+            domain = domain.substring(at + 1);
+
+        String[] labels = domain.toLowerCase().split("\\.");
+        LinkedList<String> labelList = new LinkedList<String>();
+        for (String label : labels) {
+            labelList.addFirst(label);
+        }
+
+        return labelList;
+    }
+
+    public static long parseMac(String s) {
+
+        long mac = 0;
+        int count = 0;
+        for (int n = 0; n < s.length(); n++) {
+            int nibble = Utils.fromHex(s.charAt(n), true);  // Set lenient to not blow up on ':'
+            if (nibble >= 0) {                              // ... and use only legit hex.
+                mac = (mac << 4) | nibble;
+                count++;
+            }
+        }
+        if (count < 12 || (count & 1) == 1) {
+            throw new IllegalArgumentException("Bad MAC address: '" + s + "'");
+        }
+        return mac;
+    }
+
+    public static String macToString(long mac) {
+        int len = (mac & ~EUI48Mask) != 0 ? EUI64Length : EUI48Length;
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for (int n = (len - 1) * Byte.SIZE; n >= 0; n -= Byte.SIZE) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append(':');
+            }
+            sb.append(String.format("%02x", (mac >>> n) & Constants.BYTE_MASK));
+        }
+        return sb.toString();
+    }
+
+    public static String getMccMnc(List<String> domain) {
+        if (domain.size() != PLMNText.length) {
+            return null;
+        }
+
+        for (int n = 0; n < PLMNText.length; n++) {
+            String expect = PLMNText[n];
+            int len = expect.endsWith("*") ? expect.length() - 1 : expect.length();
+            if (!domain.get(n).regionMatches(0, expect, 0, len)) {
+                return null;
+            }
+        }
+
+        String prefix = domain.get(2).substring(3) + domain.get(3).substring(3);
+        for (int n = 0; n < prefix.length(); n++) {
+            char ch = prefix.charAt(n);
+            if (ch < '0' || ch > '9') {
+                return null;
+            }
+        }
+        return prefix;
+    }
+
+    public static String bssidsToString(Collection<Long> bssids) {
+        StringBuilder sb = new StringBuilder();
+        for (Long bssid : bssids) {
+            sb.append(String.format(" %012x", bssid));
+        }
+        return sb.toString();
+    }
+
+    public static String roamingConsortiumsToString(long[] ois) {
+        if (ois == null) {
+            return "null";
+        }
+        List<Long> list = new ArrayList<Long>(ois.length);
+        for (long oi : ois) {
+            list.add(oi);
+        }
+        return roamingConsortiumsToString(list);
+    }
+
+    public static String roamingConsortiumsToString(Collection<Long> ois) {
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for (long oi : ois) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append(", ");
+            }
+            if (Long.numberOfLeadingZeros(oi) > 40) {
+                sb.append(String.format("%06x", oi));
+            } else {
+                sb.append(String.format("%010x", oi));
+            }
+        }
+        return sb.toString();
+    }
+
+    public static String toUnicodeEscapedString(String s) {
+        StringBuilder sb = new StringBuilder(s.length());
+        for (int n = 0; n < s.length(); n++) {
+            char ch = s.charAt(n);
+            if (ch >= ' ' && ch < 127) {
+                sb.append(ch);
+            } else {
+                sb.append("\\u").append(String.format("%04x", (int) ch));
+            }
+        }
+        return sb.toString();
+    }
+
+    public static String toHexString(byte[] data) {
+        if (data == null) {
+            return "null";
+        }
+        StringBuilder sb = new StringBuilder(data.length * 3);
+
+        boolean first = true;
+        for (byte b : data) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append(' ');
+            }
+            sb.append(String.format("%02x", b & BYTE_MASK));
+        }
+        return sb.toString();
+    }
+
+    public static String toHex(byte[] octets) {
+        StringBuilder sb = new StringBuilder(octets.length * 2);
+        for (byte o : octets) {
+            sb.append(String.format("%02x", o & BYTE_MASK));
+        }
+        return sb.toString();
+    }
+
+    public static byte[] hexToBytes(String text) {
+        if ((text.length() & 1) == 1) {
+            throw new NumberFormatException("Odd length hex string: " + text.length());
+        }
+        byte[] data = new byte[text.length() >> 1];
+        int position = 0;
+        for (int n = 0; n < text.length(); n += 2) {
+            data[position] =
+                    (byte) (((fromHex(text.charAt(n), false) & NIBBLE_MASK) << 4) |
+                            (fromHex(text.charAt(n + 1), false) & NIBBLE_MASK));
+            position++;
+        }
+        return data;
+    }
+
+    public static int fromHex(char ch, boolean lenient) throws NumberFormatException {
+        if (ch <= '9' && ch >= '0') {
+            return ch - '0';
+        } else if (ch >= 'a' && ch <= 'f') {
+            return ch + 10 - 'a';
+        } else if (ch <= 'F' && ch >= 'A') {
+            return ch + 10 - 'A';
+        } else if (lenient) {
+            return -1;
+        } else {
+            throw new NumberFormatException("Bad hex-character: " + ch);
+        }
+    }
+
+    private static char toAscii(int b) {
+        return b >= ' ' && b < 0x7f ? (char) b : '.';
+    }
+
+    static boolean isDecimal(String s) {
+        for (int n = 0; n < s.length(); n++) {
+            char ch = s.charAt(n);
+            if (ch < '0' || ch > '9') {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static <T extends Comparable> int compare(Comparable<T> c1, T c2) {
+        if (c1 == null) {
+            return c2 == null ? 0 : -1;
+        } else if (c2 == null) {
+            return 1;
+        } else {
+            return c1.compareTo(c2);
+        }
+    }
+
+    public static String bytesToBingoCard(ByteBuffer data, int len) {
+        ByteBuffer dup = data.duplicate();
+        dup.limit(dup.position() + len);
+        return bytesToBingoCard(dup);
+    }
+
+    public static String bytesToBingoCard(ByteBuffer data) {
+        ByteBuffer dup = data.duplicate();
+        StringBuilder sbx = new StringBuilder();
+        while (dup.hasRemaining()) {
+            sbx.append(String.format("%02x ", dup.get() & BYTE_MASK));
+        }
+        dup = data.duplicate();
+        sbx.append(' ');
+        while (dup.hasRemaining()) {
+            sbx.append(String.format("%c", toAscii(dup.get() & BYTE_MASK)));
+        }
+        return sbx.toString();
+    }
+
+    public static String toHMS(long millis) {
+        long time = millis >= 0 ? millis : -millis;
+        long tmp = time / 1000L;
+        long ms = time - tmp * 1000L;
+
+        time = tmp;
+        tmp /= 60L;
+        long s = time - tmp * 60L;
+
+        time = tmp;
+        tmp /= 60L;
+        long m = time - tmp * 60L;
+
+        return String.format("%s%d:%02d:%02d.%03d", millis < 0 ? "-" : "", tmp, m, s, ms);
+    }
+
+    public static String toUTCString(long ms) {
+        if (ms < 0) {
+            return "unset";
+        }
+        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+        c.setTimeInMillis(ms);
+        return String.format("%4d/%02d/%02d %2d:%02d:%02dZ",
+                c.get(Calendar.YEAR),
+                c.get(Calendar.MONTH) + 1,
+                c.get(Calendar.DAY_OF_MONTH),
+                c.get(Calendar.HOUR_OF_DAY),
+                c.get(Calendar.MINUTE),
+                c.get(Calendar.SECOND));
+    }
+
+    public static String unquote(String s) {
+        if (s == null) {
+            return null;
+        } else if (s.length() > 1 && s.startsWith("\"") && s.endsWith("\"")) {
+            return s.substring(1, s.length() - 1);
+        } else {
+            return s;
+        }
+    }
+
+
+    public static void delay(long ms) {
+        long until = System.currentTimeMillis() + ms;
+        for (; ; ) {
+            long remainder = until - System.currentTimeMillis();
+            if (remainder <= 0) {
+                break;
+            }
+            try {
+                Thread.sleep(remainder);
+            } catch (InterruptedException ie) { /**/ }
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java b/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java
new file mode 100644
index 0000000..518e64e
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java
@@ -0,0 +1,388 @@
+package com.android.hotspot2;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.CaptivePortal;
+import android.net.ConnectivityManager;
+import android.net.ICaptivePortal;
+import android.net.Network;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+
+import com.android.configparse.ConfigBuilder;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.OMAParser;
+import com.android.hotspot2.osu.OSUCertType;
+import com.android.hotspot2.osu.OSUInfo;
+import com.android.hotspot2.osu.OSUManager;
+import com.android.hotspot2.osu.commands.MOData;
+import com.android.hotspot2.pps.HomeSP;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class WifiNetworkAdapter {
+    private final Context mContext;
+    private final OSUManager mOSUManager;
+    private final Map<String, PasspointConfig> mPasspointConfigs = new HashMap<>();
+
+    private static class PasspointConfig {
+        private final WifiConfiguration mWifiConfiguration;
+        private final MOTree mMOTree;
+        private final HomeSP mHomeSP;
+
+        private PasspointConfig(WifiConfiguration config) throws IOException, SAXException {
+            mWifiConfiguration = config;
+            OMAParser omaParser = new OMAParser();
+            mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN);
+            List<HomeSP> spList = MOManager.buildSPs(mMOTree);
+            if (spList.size() != 1) {
+                throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
+            }
+            mHomeSP = spList.iterator().next();
+        }
+
+        public WifiConfiguration getWifiConfiguration() {
+            return mWifiConfiguration;
+        }
+
+        public HomeSP getHomeSP() {
+            return mHomeSP;
+        }
+
+        public MOTree getmMOTree() {
+            return mMOTree;
+        }
+    }
+
+    public WifiNetworkAdapter(Context context, OSUManager osuManager) {
+        mOSUManager = osuManager;
+        mContext = context;
+    }
+
+    public void initialize() {
+        loadAllSps();
+    }
+
+    public void networkConfigChange(WifiConfiguration configuration) {
+        loadAllSps();
+    }
+
+    private void loadAllSps() {
+        Log.d(OSUManager.TAG, "Loading all SPs");
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        for (WifiConfiguration config : wifiManager.getPrivilegedConfiguredNetworks()) {
+            String moTree = config.getMoTree();
+            if (moTree != null) {
+                try {
+                    mPasspointConfigs.put(config.FQDN, new PasspointConfig(config));
+                } catch (IOException | SAXException e) {
+                    Log.w(OSUManager.TAG, "Failed to parse MO: " + e);
+                }
+            }
+        }
+    }
+
+    public Collection<HomeSP> getLoadedSPs() {
+        List<HomeSP> homeSPs = new ArrayList<>();
+        for (PasspointConfig config : mPasspointConfigs.values()) {
+            homeSPs.add(config.getHomeSP());
+        }
+        return homeSPs;
+    }
+
+    public MOTree getMOTree(HomeSP homeSP) {
+        PasspointConfig config = mPasspointConfigs.get(homeSP.getFQDN());
+        return config != null ? config.getmMOTree() : null;
+    }
+
+    public void launchBrowser(URL target, Network network, URL endRedirect) {
+        Log.d(OSUManager.TAG, "Browser to " + target + ", land at " + endRedirect);
+
+        final Intent intent = new Intent(
+                ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network);
+        intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
+                new CaptivePortal(new ICaptivePortal.Stub() {
+                    @Override
+                    public void appResponse(int response) {
+                    }
+                }));
+        //intent.setData(Uri.parse(target.toString()));     !!! Doesn't work!
+        intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString());
+        intent.setFlags(
+                Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+
+    public HomeSP addSP(MOTree instanceTree) throws IOException, SAXException {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        String xml = instanceTree.toXml();
+        wifiManager.addPasspointManagementObject(xml);
+        return MOManager.buildSP(xml);
+    }
+
+    public void removeSP(String fqdn) throws IOException {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+    }
+
+    public HomeSP modifySP(HomeSP homeSP, Collection<MOData> mods)
+            throws IOException {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        return null;
+    }
+
+    public Network getCurrentNetwork() {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        return wifiManager.getCurrentNetwork();
+    }
+
+    public WifiConfiguration getActiveWifiConfig() {
+        WifiInfo wifiInfo = getConnectionInfo();
+        if (wifiInfo == null) {
+            return null;
+        }
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        for (WifiConfiguration config : wifiManager.getConfiguredNetworks()) {
+            if (config.networkId == wifiInfo.getNetworkId()) {
+                return config;
+            }
+        }
+        return null;
+    }
+
+    public WifiInfo getConnectionInfo() {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        return wifiManager.getConnectionInfo();
+    }
+
+    public PasspointMatch matchProviderWithCurrentNetwork(String fqdn) {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        int ordinal = wifiManager.matchProviderWithCurrentNetwork(fqdn);
+        return ordinal >= 0 && ordinal < PasspointMatch.values().length ?
+                PasspointMatch.values()[ordinal] : null;
+    }
+
+    public WifiConfiguration getWifiConfig(HomeSP homeSP) {
+        PasspointConfig passpointConfig = mPasspointConfigs.get(homeSP.getFQDN());
+        return passpointConfig != null ? passpointConfig.getWifiConfiguration() : null;
+    }
+
+    public WifiConfiguration getActivePasspointNetwork() {
+        PasspointConfig passpointConfig = getActivePasspointConfig();
+        return passpointConfig != null ? passpointConfig.getWifiConfiguration() : null;
+    }
+
+    private PasspointConfig getActivePasspointConfig() {
+        WifiInfo wifiInfo = getConnectionInfo();
+        if (wifiInfo == null) {
+            return null;
+        }
+
+        for (PasspointConfig passpointConfig : mPasspointConfigs.values()) {
+            if (passpointConfig.getWifiConfiguration().networkId == wifiInfo.getNetworkId()) {
+                return passpointConfig;
+            }
+        }
+        return null;
+    }
+
+    public HomeSP getCurrentSP() {
+        PasspointConfig passpointConfig = getActivePasspointConfig();
+        return passpointConfig != null ? passpointConfig.getHomeSP() : null;
+    }
+
+    public void doIconQuery(long bssid, String fileName) {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        Log.d("ZXZ", String.format("Icon query for %012x '%s'", bssid, fileName));
+        wifiManager.queryPasspointIcon(bssid, fileName);
+    }
+
+    public Integer addNetwork(HomeSP homeSP, Map<OSUCertType, List<X509Certificate>> certs,
+                              PrivateKey privateKey, Network osuNetwork)
+            throws IOException, GeneralSecurityException {
+
+        List<X509Certificate> aaaTrust = certs.get(OSUCertType.AAA);
+        if (aaaTrust.isEmpty()) {
+            aaaTrust = certs.get(OSUCertType.CA);   // Get the CAs from the EST flow.
+        }
+
+        WifiConfiguration config = ConfigBuilder.buildConfig(homeSP,
+                aaaTrust.iterator().next(),
+                certs.get(OSUCertType.Client), privateKey);
+
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        int nwkId = wifiManager.addNetwork(config);
+        boolean saved = false;
+        if (nwkId >= 0) {
+            saved = wifiManager.saveConfiguration();
+        }
+        Log.d(OSUManager.TAG, "Wifi configuration " + nwkId +
+                " " + (saved ? "saved" : "not saved"));
+
+        if (saved) {
+            reconnect(osuNetwork, nwkId);
+            return nwkId;
+        } else {
+            return null;
+        }
+    }
+
+    public void updateNetwork(HomeSP homeSP, X509Certificate caCert,
+                              List<X509Certificate> clientCerts, PrivateKey privateKey)
+            throws IOException, GeneralSecurityException {
+
+        WifiConfiguration config = getWifiConfig(homeSP);
+        if (config == null) {
+            throw new IOException("Failed to find matching network config");
+        }
+        Log.d(OSUManager.TAG, "Found matching config " + config.networkId + ", updating");
+
+        WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
+        WifiConfiguration newConfig = ConfigBuilder.buildConfig(homeSP,
+                caCert != null ? caCert : enterpriseConfig.getCaCertificate(),
+                clientCerts, privateKey);
+        newConfig.networkId = config.networkId;
+
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        wifiManager.save(newConfig, null);
+        wifiManager.saveConfiguration();
+    }
+
+    /**
+     * Connect to an OSU provisioning network. The connection should not bring down other existing
+     * connection and the network should not be made the default network since the connection
+     * is solely for sign up and is neither intended for nor likely provides access to any
+     * generic resources.
+     *
+     * @param osuInfo The OSU info object that defines the parameters for the network. An OSU
+     *                network is either an open network, or, if the OSU NAI is set, an "OSEN"
+     *                network, which is an anonymous EAP-TLS network with special keys.
+     * @param info    An opaque string that is passed on to any user notification. The string is used
+     *                for the name of the service provider.
+     * @return an Integer holding the network-id of the just added network configuration, or null
+     * if the network existed prior to this call (was not added by the OSU infrastructure).
+     * The value will be used at the end of the OSU flow to delete the network as applicable.
+     * @throws IOException Issues:
+     *                     1. The network id is not returned. addNetwork cannot be called from here since the method
+     *                     runs in the context of the app and doesn't have the appropriate permission.
+     *                     2. The connection is not immediately usable if the network was not previously selected
+     *                     manually.
+     */
+    public Integer connect(OSUInfo osuInfo, final String info) throws IOException {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+
+        WifiConfiguration config = new WifiConfiguration();
+        config.SSID = '"' + osuInfo.getSSID() + '"';
+        if (osuInfo.getOSUBssid() != 0) {
+            config.BSSID = Utils.macToString(osuInfo.getOSUBssid());
+            Log.d(OSUManager.TAG, String.format("Setting BSSID of '%s' to %012x",
+                    osuInfo.getSSID(), osuInfo.getOSUBssid()));
+        }
+
+        if (osuInfo.getOSUProvider().getOsuNai() == null) {
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+        } else {
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OSEN);
+            config.allowedProtocols.set(WifiConfiguration.Protocol.OSEN);
+            config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
+            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GTK_NOT_USED);
+            config.enterpriseConfig = new WifiEnterpriseConfig();
+            config.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.UNAUTH_TLS);
+            config.enterpriseConfig.setIdentity(osuInfo.getOSUProvider().getOsuNai());
+            // !!! OSEN CA Cert???
+        }
+
+        int networkId = wifiManager.addNetwork(config);
+        if (wifiManager.enableNetwork(networkId, true)) {
+            return networkId;
+        } else {
+            return null;
+        }
+
+        /* sequence of addNetwork(), enableNetwork(), saveConfiguration() and reconnect()
+        wifiManager.connect(config, new WifiManager.ActionListener() {
+            @Override
+            public void onSuccess() {
+                // Connection event comes from network change intent registered in initialize
+            }
+
+            @Override
+            public void onFailure(int reason) {
+                mOSUManager.notifyUser(OSUOperationStatus.ProvisioningFailure,
+                        "Cannot connect to OSU network: " + reason, info);
+            }
+        });
+        return null;
+
+        /*
+        try {
+            int nwkID = wifiManager.addOrUpdateOSUNetwork(config);
+            if (nwkID == WifiConfiguration.INVALID_NETWORK_ID) {
+                throw new IOException("Failed to add OSU network");
+            }
+            wifiManager.enableNetwork(nwkID, false);
+            wifiManager.reconnect();
+            return nwkID;
+        }
+        catch (SecurityException se) {
+            Log.d("ZXZ", "Blah: " + se, se);
+            wifiManager.connect(config, new WifiManager.ActionListener() {
+                @Override
+                public void onSuccess() {
+                    // Connection event comes from network change intent registered in initialize
+                }
+
+                @Override
+                public void onFailure(int reason) {
+                    mOSUManager.notifyUser(OSUOperationStatus.ProvisioningFailure,
+                            "Cannot connect to OSU network: " + reason, info);
+                }
+            });
+            return null;
+        }
+        */
+    }
+
+    private void reconnect(Network osuNetwork, int newNwkId) {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        if (osuNetwork != null) {
+            wifiManager.disableNetwork(osuNetwork.netId);
+        }
+        if (newNwkId != WifiConfiguration.INVALID_NETWORK_ID) {
+            wifiManager.enableNetwork(newNwkId, true);
+        }
+    }
+
+    public void deleteNetwork(int id) {
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        wifiManager.disableNetwork(id);
+        wifiManager.forget(id, null);
+    }
+
+    /**
+     * Set the re-authentication hold off time for the current network
+     *
+     * @param holdoff hold off time in milliseconds
+     * @param ess     set if the hold off pertains to an ESS rather than a BSS
+     */
+    public void setHoldoffTime(long holdoff, boolean ess) {
+
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Boolean.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Boolean.java
new file mode 100644
index 0000000..18af3b8
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Boolean.java
@@ -0,0 +1,31 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public class Asn1Boolean extends Asn1Object {
+    private final boolean mBoolean;
+
+    public Asn1Boolean(int tag, Asn1Class asn1Class, int length, ByteBuffer data)
+            throws DecodeException {
+        super(tag, asn1Class, false, length);
+        if (length != 1) {
+            throw new DecodeException("Boolean length != 1: " + length, data.position());
+        }
+        mBoolean = data.get() != 0;
+    }
+
+    public boolean getValue() {
+        return mBoolean;
+    }
+
+    @Override
+    public Collection<Asn1Object> getChildren() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + "=" + Boolean.toString(mBoolean);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Class.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Class.java
new file mode 100644
index 0000000..8a4d8a8
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Class.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.asn1;
+
+public enum Asn1Class {
+    Universal, Application, Context, Private
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Constructed.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Constructed.java
new file mode 100644
index 0000000..69b65dc
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Constructed.java
@@ -0,0 +1,53 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+
+public class Asn1Constructed extends Asn1Object {
+    private final int mTagPosition;
+    private final List<Asn1Object> mChildren;
+
+    public Asn1Constructed(int tag, Asn1Class asn1Class, int length,
+                           ByteBuffer payload, int tagPosition) {
+        super(tag, asn1Class, true, length, payload);
+        mTagPosition = tagPosition;
+        mChildren = new ArrayList<>();
+    }
+
+    public void addChild(Asn1Object object) {
+        mChildren.add(object);
+    }
+
+    @Override
+    public Collection<Asn1Object> getChildren() {
+        return Collections.unmodifiableCollection(mChildren);
+    }
+
+    public ByteBuffer getEncoding() {
+        return getPayload(mTagPosition);
+    }
+
+    private void toString(int level, StringBuilder sb) {
+        sb.append(indent(level)).append(super.toString()).append(":\n");
+        for (Asn1Object child : mChildren) {
+            if (child.isConstructed()) {
+                ((Asn1Constructed) child).toString(level + 1, sb);
+            } else {
+                sb.append(indent(level + 1)).append(child.toString()).append('\n');
+            }
+        }
+    }
+
+    public static String indent(int level) {
+        char[] indent = new char[level * 2];
+        Arrays.fill(indent, ' ');
+        return new String(indent);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        toString(0, sb);
+        return sb.toString();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Decoder.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Decoder.java
new file mode 100644
index 0000000..53452e7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Decoder.java
@@ -0,0 +1,211 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Asn1Decoder {
+    public static final int TAG_UNIVZERO = 0x00;
+    public static final int TAG_BOOLEAN = 0x01;
+    public static final int TAG_INTEGER = 0x02;
+    public static final int TAG_BITSTRING = 0x03;
+    public static final int TAG_OCTET_STRING = 0x04;
+    public static final int TAG_NULL = 0x05;
+    public static final int TAG_OID = 0x06;
+    public static final int TAG_ObjectDescriptor = 0x07;
+    public static final int TAG_EXTERNAL = 0x08;
+    public static final int TAG_REAL = 0x09;
+    public static final int TAG_ENUMERATED = 0x0a;
+    public static final int TAG_UTF8String = 0x0c;      // * (*) are X.509 DirectoryString's
+    public static final int TAG_RelativeOID = 0x0d;
+    public static final int TAG_SEQ = 0x10;             //   30 if constructed
+    public static final int TAG_SET = 0x11;
+    public static final int TAG_NumericString = 0x12;   //   [UNIVERSAL 18]
+    public static final int TAG_PrintableString = 0x13; // * [UNIVERSAL 19]
+    public static final int TAG_T61String = 0x14;       // * TeletexString [UNIVERSAL 20]
+    public static final int TAG_VideotexString = 0x15;  //   [UNIVERSAL 21]
+    public static final int TAG_IA5String = 0x16;       //   [UNIVERSAL 22]
+    public static final int TAG_UTCTime = 0x17;
+    public static final int TAG_GeneralizedTime = 0x18;
+    public static final int TAG_GraphicString = 0x19;   //   [UNIVERSAL 25]
+    public static final int TAG_VisibleString = 0x1a;   //   ISO64String [UNIVERSAL 26]
+    public static final int TAG_GeneralString = 0x1b;   //   [UNIVERSAL 27]
+    public static final int TAG_UniversalString = 0x1c; // * [UNIVERSAL 28]
+    public static final int TAG_BMPString = 0x1e;       // * [UNIVERSAL 30]
+
+    public static final int IntOverflow = 0xffff0000;
+    public static final int MoreBit = 0x80;
+    public static final int MoreData = 0x7f;
+    public static final int ConstructedBit = 0x20;
+    public static final int ClassShift = 6;
+    public static final int ClassMask = 0x3;
+    public static final int MoreWidth = 7;
+    public static final int ByteWidth = 8;
+    public static final int ByteMask = 0xff;
+    public static final int ContinuationTag = 31;
+
+    public static final int IndefiniteLength = -1;
+
+    private static final Map<Integer, Asn1Tag> sTagMap = new HashMap<>();
+
+    static {
+        sTagMap.put(TAG_UNIVZERO, Asn1Tag.UNIVZERO);
+        sTagMap.put(TAG_BOOLEAN, Asn1Tag.BOOLEAN);
+        sTagMap.put(TAG_INTEGER, Asn1Tag.INTEGER);
+        sTagMap.put(TAG_BITSTRING, Asn1Tag.BITSTRING);
+        sTagMap.put(TAG_OCTET_STRING, Asn1Tag.OCTET_STRING);
+        sTagMap.put(TAG_NULL, Asn1Tag.NULL);
+        sTagMap.put(TAG_OID, Asn1Tag.OID);
+        sTagMap.put(TAG_ObjectDescriptor, Asn1Tag.ObjectDescriptor);
+        sTagMap.put(TAG_EXTERNAL, Asn1Tag.EXTERNAL);
+        sTagMap.put(TAG_REAL, Asn1Tag.REAL);
+        sTagMap.put(TAG_ENUMERATED, Asn1Tag.ENUMERATED);
+        sTagMap.put(TAG_UTF8String, Asn1Tag.UTF8String);
+        sTagMap.put(TAG_RelativeOID, Asn1Tag.RelativeOID);
+        sTagMap.put(TAG_SEQ, Asn1Tag.SEQUENCE);
+        sTagMap.put(TAG_SET, Asn1Tag.SET);
+        sTagMap.put(TAG_NumericString, Asn1Tag.NumericString);
+        sTagMap.put(TAG_PrintableString, Asn1Tag.PrintableString);
+        sTagMap.put(TAG_T61String, Asn1Tag.T61String);
+        sTagMap.put(TAG_VideotexString, Asn1Tag.VideotexString);
+        sTagMap.put(TAG_IA5String, Asn1Tag.IA5String);
+        sTagMap.put(TAG_UTCTime, Asn1Tag.UTCTime);
+        sTagMap.put(TAG_GeneralizedTime, Asn1Tag.GeneralizedTime);
+        sTagMap.put(TAG_GraphicString, Asn1Tag.GraphicString);
+        sTagMap.put(TAG_VisibleString, Asn1Tag.VisibleString);
+        sTagMap.put(TAG_GeneralString, Asn1Tag.GeneralString);
+        sTagMap.put(TAG_UniversalString, Asn1Tag.UniversalString);
+        sTagMap.put(TAG_BMPString, Asn1Tag.BMPString);
+    }
+
+    public static Asn1Tag mapTag(int tag) {
+        return sTagMap.get(tag);
+    }
+
+    public static Collection<Asn1Object> decode(ByteBuffer data) throws DecodeException {
+        Asn1Constructed root =
+                new Asn1Constructed(0, null, data.remaining(), data, data.position());
+        decode(0, root);
+        return root.getChildren();
+    }
+
+    private static void decode(int level, Asn1Constructed parent) throws DecodeException {
+        ByteBuffer data = parent.getPayload();
+        while (data.hasRemaining()) {
+            int tagPosition = data.position();
+            int propMask = data.get(tagPosition) & ByteMask;
+            if (propMask == 0 && parent.isIndefiniteLength() && data.get(tagPosition + 1) == 0) {
+                parent.setEndOfData(tagPosition);
+                return;
+            }
+            Asn1Class asn1Class = Asn1Class.values()[(propMask >> ClassShift) & ClassMask];
+            boolean constructed = (propMask & ConstructedBit) != 0;
+
+            int tag = decodeTag(data);
+            int length = decodeLength(data);
+
+            if (constructed) {
+                ByteBuffer payload = peelOff(data, length);
+                Asn1Constructed root =
+                        new Asn1Constructed(tag, asn1Class, length, payload, tagPosition);
+                decode(level + 1, root);
+                if (length == IndefiniteLength) {
+                    data.position(root.getEndOfData() + 2);     // advance past '00'
+                }
+                parent.addChild(root);
+            } else {
+                if (asn1Class != Asn1Class.Universal) {
+                    parent.addChild(new Asn1Octets(tag, asn1Class, length, data));
+                } else {
+                    parent.addChild(buildScalar(tag, asn1Class, length, data));
+                }
+            }
+        }
+    }
+
+    private static ByteBuffer peelOff(ByteBuffer base, int length) {
+        ByteBuffer copy = base.duplicate();
+        if (length == IndefiniteLength) {
+            return copy;
+        }
+        copy.limit(copy.position() + length);
+        base.position(base.position() + length);
+        return copy;
+    }
+
+    private static Asn1Object buildScalar(int tag, Asn1Class asn1Class, int length, ByteBuffer data)
+            throws DecodeException {
+        switch (tag) {
+            case TAG_BOOLEAN:
+                return new Asn1Boolean(tag, asn1Class, length, data);
+            case TAG_INTEGER:
+            case TAG_ENUMERATED:
+                return new Asn1Integer(tag, asn1Class, length, data);
+            case TAG_BITSTRING:
+                int bitResidual = data.get() & ByteMask;
+                return new Asn1Octets(tag, asn1Class, length, data, bitResidual);
+            case TAG_OCTET_STRING:
+                return new Asn1Octets(tag, asn1Class, length, data);
+            case TAG_OID:
+                return new Asn1Oid(tag, asn1Class, length, data);
+            case TAG_UTF8String:
+            case TAG_NumericString:
+            case TAG_PrintableString:
+            case TAG_T61String:
+            case TAG_VideotexString:
+            case TAG_IA5String:
+            case TAG_GraphicString:
+            case TAG_VisibleString:
+            case TAG_GeneralString:
+            case TAG_UniversalString:
+            case TAG_BMPString:
+                return new Asn1String(tag, asn1Class, length, data);
+            case TAG_GeneralizedTime:
+            case TAG_UTCTime:
+                // Should really be a dedicated time object
+                return new Asn1String(tag, asn1Class, length, data);
+            default:
+                return new Asn1Octets(tag, asn1Class, length, data);
+        }
+    }
+
+    private static int decodeTag(ByteBuffer data) throws DecodeException {
+        int tag;
+        byte tag0 = data.get();
+
+        if ((tag = (tag0 & ContinuationTag)) == ContinuationTag) {
+            int tagByte;
+            tag = 0;
+            while (((tagByte = data.get() & ByteMask) & MoreBit) != 0) {
+                tag = (tag << MoreWidth) | (tagByte & MoreData);
+                if ((tag & IntOverflow) != 0)
+                    throw new DecodeException("Tag overflow", data.position());
+            }
+            tag = (tag << MoreWidth) | tagByte;
+        }
+        return tag;
+    }
+
+    private static int decodeLength(ByteBuffer data) throws DecodeException {
+        int length;
+        int lenlen = data.get() & ByteMask;
+
+        if ((lenlen & MoreBit) == 0)    // One byte encoding
+            length = lenlen;
+        else {
+            lenlen &= MoreData;
+            if (lenlen == 0) {
+                return IndefiniteLength;
+            }
+            length = 0;
+            while (lenlen-- > 0) {
+                length = (length << ByteWidth) | (data.get() & ByteMask);
+                if ((length & IntOverflow) != 0 && lenlen > 0)
+                    throw new DecodeException("Length overflow", data.position());
+            }
+        }
+        return length;
+    }
+
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1ID.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1ID.java
new file mode 100644
index 0000000..452d85c
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1ID.java
@@ -0,0 +1,19 @@
+package com.android.hotspot2.asn1;
+
+public class Asn1ID {
+    private final int mTag;
+    private final Asn1Class mClass;
+
+    public Asn1ID(int tag, Asn1Class asn1Class) {
+        mTag = tag;
+        mClass = asn1Class;
+    }
+
+    public int getTag() {
+        return mTag;
+    }
+
+    public Asn1Class getAsn1Class() {
+        return mClass;
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Integer.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Integer.java
new file mode 100644
index 0000000..5180a4d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Integer.java
@@ -0,0 +1,56 @@
+package com.android.hotspot2.asn1;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public class Asn1Integer extends Asn1Object {
+    private static final int SignBit = 0x80;
+
+    private final long mValue;
+    private final BigInteger mBigValue;
+
+    public Asn1Integer(int tag, Asn1Class asn1Class, int length, ByteBuffer data) {
+        super(tag, asn1Class, false, length);
+
+        if (length <= 8) {
+            long value = (data.get(data.position()) & SignBit) != 0 ? -1 : 0;
+            for (int n = 0; n < length; n++) {
+                value = (value << Byte.SIZE) | data.get();
+            }
+            mValue = value;
+            mBigValue = null;
+        } else {
+            byte[] payload = new byte[length];
+            data.get(payload);
+            mValue = 0;
+            mBigValue = new BigInteger(payload);
+        }
+    }
+
+    public boolean isBigValue() {
+        return mBigValue != null;
+    }
+
+    public long getValue() {
+        return mValue;
+    }
+
+    public BigInteger getBigValue() {
+        return mBigValue;
+    }
+
+    @Override
+    public Collection<Asn1Object> getChildren() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toString() {
+        if (isBigValue()) {
+            return super.toString() + '=' + mBigValue.toString(16);
+        } else {
+            return super.toString() + '=' + mValue;
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Object.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Object.java
new file mode 100644
index 0000000..8137583
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Object.java
@@ -0,0 +1,88 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public abstract class Asn1Object {
+    private final int mTag;
+    private final Asn1Class mClass;
+    private final boolean mConstructed;
+    private final int mLength;
+    private final ByteBuffer mPayload;
+
+    protected Asn1Object(int tag, Asn1Class asn1Class, boolean constructed, int length) {
+        this(tag, asn1Class, constructed, length, null);
+    }
+
+    protected Asn1Object(int tag, Asn1Class asn1Class, boolean constructed,
+                         int length, ByteBuffer payload) {
+        mTag = tag;
+        mClass = asn1Class;
+        mConstructed = constructed;
+        mLength = length;
+        mPayload = payload != null ? payload.duplicate() : null;
+    }
+
+    public int getTag() {
+        return mTag;
+    }
+
+    public Asn1Class getAsn1Class() {
+        return mClass;
+    }
+
+    public boolean isConstructed() {
+        return mConstructed;
+    }
+
+    public boolean isIndefiniteLength() {
+        return mLength == Asn1Decoder.IndefiniteLength;
+    }
+
+    public int getLength() {
+        return mLength;
+    }
+
+    public ByteBuffer getPayload() {
+        return mPayload != null ? mPayload.duplicate() : null;
+    }
+
+    protected ByteBuffer getPayload(int position) {
+        if (mPayload == null) {
+            return null;
+        }
+        ByteBuffer encoding = mPayload.duplicate();
+        encoding.position(position);
+        return encoding;
+    }
+
+    protected void setEndOfData(int position) {
+        mPayload.limit(position);
+    }
+
+    protected int getEndOfData() {
+        return mPayload.limit();
+    }
+
+    public boolean matches(Asn1ID id) {
+        return mTag == id.getTag() && mClass == id.getAsn1Class();
+    }
+
+    public String toSimpleString() {
+        Asn1Tag tag = mClass == Asn1Class.Universal ? Asn1Decoder.mapTag(mTag) : null;
+        if (tag != null) {
+            return tag.name();
+        } else if (mClass == Asn1Class.Universal) {
+            return String.format("[%d]", mTag);
+        } else {
+            return String.format("[%s %d]", mClass, mTag);
+        }
+    }
+
+    public abstract Collection<Asn1Object> getChildren();
+
+    @Override
+    public String toString() {
+        return toSimpleString();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Octets.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Octets.java
new file mode 100644
index 0000000..1e19953
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Octets.java
@@ -0,0 +1,47 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public class Asn1Octets extends Asn1Object {
+    private final byte[] mOctets;
+    private final int mBitResidual;
+
+    public Asn1Octets(int tag, Asn1Class asn1Class, int length, ByteBuffer data) {
+        super(tag, asn1Class, false, length);
+        mOctets = new byte[length];
+        data.get(mOctets);
+        mBitResidual = -1;
+    }
+
+    public Asn1Octets(int tag, Asn1Class asn1Class, int length, ByteBuffer data, int bitResidual) {
+        super(tag, asn1Class, false, length);
+        mOctets = new byte[length - 1];
+        data.get(mOctets);
+        mBitResidual = bitResidual;
+    }
+
+    public byte[] getOctets() {
+        return mOctets;
+    }
+
+    @Override
+    public Collection<Asn1Object> getChildren() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : mOctets) {
+            sb.append(String.format(" %02x", b & Asn1Decoder.ByteMask));
+        }
+        if (mBitResidual >= 0) {
+            return super.toString() + '=' + sb + '/' + mBitResidual;
+        } else if (getTag() == Asn1Decoder.TAG_NULL && getLength() == 0) {
+            return super.toString();
+        } else {
+            return super.toString() + '=' + sb;
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Oid.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Oid.java
new file mode 100644
index 0000000..50f0553
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Oid.java
@@ -0,0 +1,212 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Asn1Oid extends Asn1Object {
+    public static final int OidMaxOctet1 = 2;
+    public static final int OidOctet1Modulus = 40;
+
+    private final List<Long> mArcs;
+    private final int mHashcode;
+
+    private static final Map<Asn1Oid, String> sOidMap = new HashMap<>();
+
+    public Asn1Oid(int tag, Asn1Class asn1Class, int length, ByteBuffer data)
+            throws DecodeException {
+        super(tag, asn1Class, false, length);
+
+        if (length == 0)
+            throw new DecodeException("oid-encoding length is zero", data.position());
+
+        mArcs = new ArrayList<>();
+
+        ByteBuffer payload = data.duplicate();
+        payload.limit(payload.position() + length);
+        data.position(data.position() + length);
+
+        byte current = payload.get();
+        long seg01 = current & Asn1Decoder.ByteMask;
+        long segValue = seg01 / OidOctet1Modulus;
+        int hashcode = (int) segValue;
+        mArcs.add(segValue);
+        segValue = seg01 - segValue * OidOctet1Modulus;
+        hashcode = hashcode * 31 + (int) segValue;
+        mArcs.add(segValue);
+
+        current = 0;
+        segValue = 0L;
+
+        while (payload.hasRemaining()) {
+            current = payload.get();
+            segValue |= current & Asn1Decoder.MoreData;
+            if ((current & Asn1Decoder.MoreBit) == 0) {
+                hashcode = hashcode * 31 + (int) segValue;
+                mArcs.add(segValue);
+                segValue = 0L;
+            } else
+                segValue <<= Asn1Decoder.MoreWidth;
+        }
+        if ((current & Asn1Decoder.MoreBit) != 0)
+            throw new DecodeException("Illegal (end of) oid-encoding", payload.position());
+        mHashcode = hashcode;
+    }
+
+    public Asn1Oid(Long... arcs) {
+        super(Asn1Decoder.TAG_OID, Asn1Class.Universal, false, -1);
+        mArcs = Arrays.asList(arcs);
+        int hashcode = 0;
+        for (long arc : arcs) {
+            hashcode = hashcode * 31 + (int) arc;
+        }
+        mHashcode = hashcode;
+    }
+
+    @Override
+    public int hashCode() {
+        return mHashcode;
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        return !(thatObject == null || thatObject.getClass() != Asn1Oid.class) &&
+                mArcs.equals(((Asn1Oid) thatObject).mArcs);
+    }
+
+    public String toOIDString() {
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for (long arc : mArcs) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append('.');
+            }
+            sb.append(arc);
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(toOIDString());
+        String name = sOidMap.get(this);
+        if (name != null) {
+            sb.append(" (").append(name).append(')');
+        }
+        return super.toString() + '=' + sb.toString();
+    }
+
+    @Override
+    public Collection<Asn1Object> getChildren() {
+        throw new UnsupportedOperationException();
+    }
+
+    public static final Asn1Oid PKCS7Data = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 7L, 1L);
+    public static final Asn1Oid PKCS7SignedData = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 7L, 2L);
+    // encoded as an IA5STRING type
+    public static final Asn1Oid OidMacAddress = new Asn1Oid(1L, 3L, 6L, 1L, 1L, 1L, 1L, 22L);
+    // encoded as an IA5STRING type
+    public static final Asn1Oid OidImei = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 3L);
+    // encoded as a BITSTRING type
+    public static final Asn1Oid OidMeid = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 4L);
+    // encoded as a PRINTABLESTRING type
+    public static final Asn1Oid OidDevId = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 5L);
+
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 1L), "algo_id_dsa");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 3L), "algo_id_dsawithsha1");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 2L, 1L), "algo_id_ecPublicKey");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 3L), "eccdaWithSHA384");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 1L), "algo_id_rsaEncryption");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 2L), "algo_id_md2WithRSAEncryption");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 4L), "algo_id_md5WithRSAEncryption");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 5L), "algo_id_sha1WithRSAEncryption");
+    //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 11L),
+    // "algo_id_sha256WithRSAEncryption");
+
+    static {
+        sOidMap.put(new Asn1Oid(0L, 0L), "NullOid");
+        sOidMap.put(new Asn1Oid(0L, 9L, 2342L, 19200300L, 100L, 1L, 25L), "domComp");
+
+        sOidMap.put(OidMacAddress, "mac-address");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 1L), "algo_id_dsa");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 3L), "algo_id_dsawithsha1");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 2L, 1L), "algo_id_ecPublicKey");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 3L), "eccdaWithSHA384");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10046L, 2L, 1L), "algo_id_dhpublicnumber");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 1L), "algo_id_rsaEncryption");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 2L), "algo_id_md2WithRSAEncryption");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 4L), "algo_id_md5WithRSAEncryption");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 5L),
+                "algo_id_sha1WithRSAEncryption");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 11L),
+                "algo_id_sha256WithRSAEncryption");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 7L), "pkcs7");
+        sOidMap.put(PKCS7Data, "pkcs7-data");
+        sOidMap.put(PKCS7SignedData, "pkcs7-signedData");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 1L), "emailAddress");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 7L), "challengePassword");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 14L), "extensionRequest");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 2L), "algo_id_RC2_CBC");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 4L), "algo_id_RC4_ENC");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 7L), "algo_id_DES_EDE3_CBC");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 9L), "algo_id_RC5_CBC_PAD");
+        sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 10L), "algo_id_desCDMF");
+        sOidMap.put(new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 2L), "id-kp-HS2.0Auth");
+        sOidMap.put(OidImei, "imei");
+        sOidMap.put(OidMeid, "meid");
+        sOidMap.put(OidDevId, "DevId");
+        sOidMap.put(new Asn1Oid(1L, 3L, 6L, 1L, 5L, 5L, 7L, 1L, 1L),
+                "certAuthorityInfoAccessSyntax");
+        sOidMap.put(new Asn1Oid(1L, 3L, 6L, 1L, 5L, 5L, 7L, 1L, 11L),
+                "certSubjectInfoAccessSyntax");
+        sOidMap.put(new Asn1Oid(1L, 3L, 14L, 3L, 2L, 26L), "algo_id_SHA1");
+        sOidMap.put(new Asn1Oid(1L, 3L, 132L, 0L, 34L), "secp384r1");
+
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 3L), "x500_CN");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 4L), "x500_SN");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 5L), "x500_serialNum");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 6L), "x500_C");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 7L), "x500_L");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 8L), "x500_ST");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 9L), "x500_STREET");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 10L), "x500_O");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 11L), "x500_OU");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 12L), "x500_title");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 13L), "x500_description");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 17L), "x500_postalCode");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 18L), "x500_poBox");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 20L), "x500_phone");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 41L), "x500_name");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 42L), "x500_givenName");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 44L), "x500_genQual");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 43L), "x500_initials");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 46L), "x500_dnQualifier");
+        sOidMap.put(new Asn1Oid(2L, 5L, 4L, 65L), "x500_pseudonym");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 9L), "certSubjectDirectoryAttributes");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 14L), "certSubjectKeyIdentifier ");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 15L), "certKeyUsage");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 16L), "certPrivateKeyUsagePeriod");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 17L), "certSubjectAltName");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 18L), "certIssuerAltName");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 19L), "certBasicConstraints");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 30L), "certNameConstraints");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 31L), "certCRLDistributionPoints");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 32L), "certificatePolicies");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 33L), "certPolicyMappings");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 35L), "certAuthorityKeyIdentifier ");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 36L), "certPolicyConstraints");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 37L), "certExtKeyUsageSyntax");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 46L), "certFreshestCRL");
+        sOidMap.put(new Asn1Oid(2L, 5L, 29L, 54L), "certInhibitAnyPolicy");
+        sOidMap.put(new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 1L, 2L), "algo_id_aes128");
+        sOidMap.put(new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 1L, 22L), "algo_id_aes192");
+        sOidMap.put(new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 1L, 42L), "algo_id_aes256");
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1String.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1String.java
new file mode 100644
index 0000000..37ed2b2
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1String.java
@@ -0,0 +1,34 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+
+public class Asn1String extends Asn1Object {
+    private final String mString;
+
+    public Asn1String(int tag, Asn1Class asn1Class, int length, ByteBuffer data) {
+        super(tag, asn1Class, false, length);
+
+        byte[] octets = new byte[length];
+        data.get(octets);
+        Charset charset = tag == Asn1Decoder.TAG_UTF8String
+                ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1;
+        mString = new String(octets, charset);
+    }
+
+    public String getString() {
+        return mString;
+    }
+
+    @Override
+    public Collection<Asn1Object> getChildren() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + "='" + mString + '\'';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Tag.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Tag.java
new file mode 100644
index 0000000..8129481
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Tag.java
@@ -0,0 +1,31 @@
+package com.android.hotspot2.asn1;
+
+public enum Asn1Tag {
+    UNIVZERO,
+    BOOLEAN,
+    INTEGER,
+    BITSTRING,
+    OCTET_STRING,
+    NULL,
+    OID,
+    ObjectDescriptor,
+    EXTERNAL,
+    REAL,
+    ENUMERATED,
+    UTF8String,
+    RelativeOID,
+    SEQUENCE,
+    SET,
+    NumericString,
+    PrintableString,
+    T61String,
+    VideotexString,
+    IA5String,
+    UTCTime,
+    GeneralizedTime,
+    GraphicString,
+    VisibleString,
+    GeneralString,
+    UniversalString,
+    BMPString
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/DecodeException.java b/packages/Osu/src/com/android/hotspot2/asn1/DecodeException.java
new file mode 100644
index 0000000..1f10ee4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/DecodeException.java
@@ -0,0 +1,17 @@
+package com.android.hotspot2.asn1;
+
+import java.io.IOException;
+
+public class DecodeException extends IOException {
+    private final int mOffset;
+
+    public DecodeException(String message, int offset) {
+        super(message);
+        mOffset = offset;
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + " at " + mOffset;
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/OidMappings.java b/packages/Osu/src/com/android/hotspot2/asn1/OidMappings.java
new file mode 100644
index 0000000..01a6fd6
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/OidMappings.java
@@ -0,0 +1,197 @@
+package com.android.hotspot2.asn1;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class OidMappings {
+    public static class SigEntry {
+        private final String mSigAlgo;
+        private final Asn1Oid mKeyAlgo;
+
+        private SigEntry(String sigAlgo, Asn1Oid keyAlgo) {
+            mSigAlgo = sigAlgo;
+            mKeyAlgo = keyAlgo;
+        }
+
+        public String getSigAlgo() {
+            return mSigAlgo;
+        }
+
+        public Asn1Oid getKeyAlgo() {
+            return mKeyAlgo;
+        }
+    }
+
+    public static final String IdPeLogotype = "1.3.6.1.5.5.7.1.12";
+    public static final String IdCeSubjectAltName = "2.5.29.17";
+
+    private static final Map<Asn1Oid, String> sCryptoMapping = new HashMap<>();
+    private static final Map<Asn1Oid, String> sNameMapping = new HashMap<>();
+    private static final Set<Asn1Oid> sIDMapping = new HashSet<>();
+    private static final Map<Asn1Oid, SigEntry> sSigAlgos = new HashMap<>();
+
+    // DSA
+    private static final Asn1Oid sAlgo_DSA = new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 1L);
+    private static final Asn1Oid sAlgo_SHA1withDSA = new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 3L);
+
+    // RSA
+    public static final Asn1Oid sAlgo_RSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 1L);
+    private static final Asn1Oid sAlgo_MD2withRSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 2L);
+    private static final Asn1Oid sAlgo_MD5withRSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 4L);
+    private static final Asn1Oid sAlgo_SHA1withRSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 5L);
+    private static final Asn1Oid sAlgo_SHA224withRSA =
+            new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 14L);   // n/a
+    private static final Asn1Oid sAlgo_SHA256withRSA =
+            new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 11L);
+    private static final Asn1Oid sAlgo_SHA384withRSA =
+            new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 12L);
+    private static final Asn1Oid sAlgo_SHA512withRSA =
+            new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 13L);
+
+    // ECC
+    public static final Asn1Oid sAlgo_EC = new Asn1Oid(1L, 2L, 840L, 10045L, 2L, 1L);
+    private static final Asn1Oid sAlgo_SHA1withECDSA = new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 1L);
+    private static final Asn1Oid sAlgo_SHA224withECDSA =
+            new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 1L);     // n/a
+    private static final Asn1Oid sAlgo_SHA256withECDSA =
+            new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 2L);
+    private static final Asn1Oid sAlgo_SHA384withECDSA =
+            new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 3L);
+    private static final Asn1Oid sAlgo_SHA512withECDSA =
+            new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 4L);
+
+    private static final Asn1Oid sAlgo_MD2 = new Asn1Oid(1L, 2L, 840L, 113549L, 2L, 2L);
+    private static final Asn1Oid sAlgo_MD5 = new Asn1Oid(1L, 2L, 840L, 113549L, 2L, 5L);
+    private static final Asn1Oid sAlgo_SHA1 = new Asn1Oid(1L, 3L, 14L, 3L, 2L, 26L);
+    private static final Asn1Oid sAlgo_SHA256 =
+            new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 2L, 1L);
+    private static final Asn1Oid sAlgo_SHA384 =
+            new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 2L, 2L);
+    private static final Asn1Oid sAlgo_SHA512 =
+            new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 2L, 3L);
+
+    // HS2.0 stuff:
+    public static final Asn1Oid sPkcs9AtChallengePassword =
+            new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 7L);
+    public static final Asn1Oid sExtensionRequest = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 14L);
+
+    public static final Asn1Oid sMAC = new Asn1Oid(1L, 3L, 6L, 1L, 1L, 1L, 1L, 22L);
+    public static final Asn1Oid sIMEI = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 3L);
+    public static final Asn1Oid sMEID = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 4L);
+    public static final Asn1Oid sDevID = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 5L);
+
+    public static final Asn1Oid sIdWfaHotspotFriendlyName =
+            new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 1L);
+
+    static {
+        sCryptoMapping.put(sAlgo_DSA, "DSA");
+        sCryptoMapping.put(sAlgo_RSA, "RSA");
+        sCryptoMapping.put(sAlgo_EC, "EC");
+
+        sSigAlgos.put(sAlgo_SHA1withDSA, new SigEntry("SHA1withDSA", sAlgo_DSA));
+
+        sSigAlgos.put(sAlgo_MD2withRSA, new SigEntry("MD2withRSA", sAlgo_RSA));
+        sSigAlgos.put(sAlgo_MD5withRSA, new SigEntry("MD5withRSA", sAlgo_RSA));
+        sSigAlgos.put(sAlgo_SHA1withRSA, new SigEntry("SHA1withRSA", sAlgo_RSA));
+        sSigAlgos.put(sAlgo_SHA224withRSA, new SigEntry(null, sAlgo_RSA));
+        sSigAlgos.put(sAlgo_SHA256withRSA, new SigEntry("SHA256withRSA", sAlgo_RSA));
+        sSigAlgos.put(sAlgo_SHA384withRSA, new SigEntry("SHA384withRSA", sAlgo_RSA));
+        sSigAlgos.put(sAlgo_SHA512withRSA, new SigEntry("SHA512withRSA", sAlgo_RSA));
+
+        sSigAlgos.put(sAlgo_SHA1withECDSA, new SigEntry("SHA1withECDSA", sAlgo_EC));
+        sSigAlgos.put(sAlgo_SHA224withECDSA, new SigEntry(null, sAlgo_EC));
+        sSigAlgos.put(sAlgo_SHA256withECDSA, new SigEntry("SHA256withECDSA", sAlgo_EC));
+        sSigAlgos.put(sAlgo_SHA384withECDSA, new SigEntry("SHA384withECDSA", sAlgo_EC));
+        sSigAlgos.put(sAlgo_SHA512withECDSA, new SigEntry("SHA512withECDSA", sAlgo_EC));
+
+        sIDMapping.add(sMAC);
+        sIDMapping.add(sIMEI);
+        sIDMapping.add(sMEID);
+        sIDMapping.add(sDevID);
+
+        for (Map.Entry<Asn1Oid, String> entry : sCryptoMapping.entrySet()) {
+            sNameMapping.put(entry.getKey(), entry.getValue());
+        }
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 1L), "sect163k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 2L), "sect163r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 3L), "sect239k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 4L), "sect113r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 5L), "sect113r2");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 6L), "secp112r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 7L), "secp112r2");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 8L), "secp160r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 9L), "secp160k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 10L), "secp256k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 15L), "sect163r2");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 16L), "sect283k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 17L), "sect283r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 22L), "sect131r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 23L), "sect131r2");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 24L), "sect193r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 25L), "sect193r2");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 26L), "sect233k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 27L), "sect233r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 28L), "secp128r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 29L), "secp128r2");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 30L), "secp160r2");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 31L), "secp192k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 32L), "secp224k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 33L), "secp224r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 34L), "secp384r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 35L), "secp521r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 36L), "sect409k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 37L), "sect409r1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 38L), "sect571k1");
+        sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 39L), "sect571r1");
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 1L), "secp192r1");
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 7L), "secp256r1");
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 2L), "prime192v2");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 3L), "prime192v3");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 4L), "prime239v1");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 5L), "prime239v2");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 6L), "prime239v3");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 5L), "c2tnb191v1");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 6L), "c2tnb191v2");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 7L), "c2tnb191v3");    // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 11L), "c2tnb239v1");   // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 12L), "c2tnb239v2");   // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 13L), "c2tnb239v3");   // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 18L), "c2tnb359v1");   // X9.62
+        sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 20L), "c2tnb431r1");   // X9.62
+
+        sNameMapping.put(sAlgo_MD2, "MD2");
+        sNameMapping.put(sAlgo_MD5, "MD5");
+        sNameMapping.put(sAlgo_SHA1, "SHA-1");
+        sNameMapping.put(sAlgo_SHA256, "SHA-256");
+        sNameMapping.put(sAlgo_SHA384, "SHA-384");
+        sNameMapping.put(sAlgo_SHA512, "SHA-512");
+    }
+
+    public static SigEntry getSigEntry(Asn1Oid oid) {
+        return sSigAlgos.get(oid);
+    }
+
+    public static String getCryptoID(Asn1Oid oid) {
+        return sCryptoMapping.get(oid);
+    }
+
+    public static String getJCEName(Asn1Oid oid) {
+        return sNameMapping.get(oid);
+    }
+
+    public static String getSigAlgoName(Asn1Oid oid) {
+        SigEntry sigEntry = sSigAlgos.get(oid);
+        return sigEntry != null ? sigEntry.getSigAlgo() : null;
+    }
+
+    public static String getKeyAlgoName(Asn1Oid oid) {
+        SigEntry sigEntry = sSigAlgos.get(oid);
+        return sigEntry != null ? sNameMapping.get(sigEntry.getKeyAlgo()) : null;
+    }
+
+    public static boolean isIDAttribute(Asn1Oid oid) {
+        return sIDMapping.contains(oid);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java b/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java
new file mode 100644
index 0000000..b305f4b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java
@@ -0,0 +1,500 @@
+package com.android.hotspot2.est;
+
+import android.net.Network;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.hotspot2.OMADMAdapter;
+import com.android.hotspot2.asn1.Asn1Class;
+import com.android.hotspot2.asn1.Asn1Constructed;
+import com.android.hotspot2.asn1.Asn1Decoder;
+import com.android.hotspot2.asn1.Asn1ID;
+import com.android.hotspot2.asn1.Asn1Integer;
+import com.android.hotspot2.asn1.Asn1Object;
+import com.android.hotspot2.asn1.Asn1Oid;
+import com.android.hotspot2.asn1.OidMappings;
+import com.android.hotspot2.osu.HTTPHandler;
+import com.android.hotspot2.osu.OSUSocketFactory;
+import com.android.hotspot2.osu.commands.GetCertData;
+import com.android.hotspot2.pps.HomeSP;
+import com.android.hotspot2.utils.HTTPMessage;
+import com.android.hotspot2.utils.HTTPResponse;
+import com.android.org.bouncycastle.asn1.ASN1Encodable;
+import com.android.org.bouncycastle.asn1.ASN1EncodableVector;
+import com.android.org.bouncycastle.asn1.ASN1Set;
+import com.android.org.bouncycastle.asn1.DERBitString;
+import com.android.org.bouncycastle.asn1.DEREncodableVector;
+import com.android.org.bouncycastle.asn1.DERIA5String;
+import com.android.org.bouncycastle.asn1.DERObjectIdentifier;
+import com.android.org.bouncycastle.asn1.DERPrintableString;
+import com.android.org.bouncycastle.asn1.DERSet;
+import com.android.org.bouncycastle.asn1.x509.Attribute;
+import com.android.org.bouncycastle.jce.PKCS10CertificationRequest;
+import com.android.org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.KeyManager;
+import javax.security.auth.x500.X500Principal;
+
+//import com.android.org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+public class ESTHandler implements AutoCloseable {
+    private static final String TAG = "HS2EST";
+    private static final int MinRSAKeySize = 2048;
+
+    private static final String CACERT_PATH = "/cacerts";
+    private static final String CSR_PATH = "/csrattrs";
+    private static final String SIMPLE_ENROLL_PATH = "/simpleenroll";
+    private static final String SIMPLE_REENROLL_PATH = "/simplereenroll";
+
+    private final URL mURL;
+    private final String mUser;
+    private final byte[] mPassword;
+    private final OSUSocketFactory mSocketFactory;
+    private final OMADMAdapter mOMADMAdapter;
+
+    private final List<X509Certificate> mCACerts = new ArrayList<>();
+    private final List<X509Certificate> mClientCerts = new ArrayList<>();
+    private PrivateKey mClientKey;
+
+    public ESTHandler(GetCertData certData, Network network, OMADMAdapter omadmAdapter,
+                      KeyManager km, KeyStore ks, HomeSP homeSP, int flowType)
+            throws IOException, GeneralSecurityException {
+        mURL = new URL(certData.getServer());
+        mUser = certData.getUserName();
+        mPassword = certData.getPassword();
+        mSocketFactory = OSUSocketFactory.getSocketFactory(ks, homeSP, flowType,
+                network, mURL, km, true);
+        mOMADMAdapter = omadmAdapter;
+    }
+
+    @Override
+    public void close() throws IOException {
+    }
+
+    public List<X509Certificate> getCACerts() {
+        return mCACerts;
+    }
+
+    public List<X509Certificate> getClientCerts() {
+        return mClientCerts;
+    }
+
+    public PrivateKey getClientKey() {
+        return mClientKey;
+    }
+
+    private static String indent(int amount) {
+        char[] indent = new char[amount * 2];
+        Arrays.fill(indent, ' ');
+        return new String(indent);
+    }
+
+    public void execute(boolean reenroll) throws IOException, GeneralSecurityException {
+        URL caURL = new URL(mURL.getProtocol(), mURL.getHost(), mURL.getPort(),
+                mURL.getFile() + CACERT_PATH);
+
+        HTTPResponse response;
+        try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.ISO_8859_1, mSocketFactory,
+                mUser, mPassword)) {
+            response = httpHandler.doGetHTTP(caURL);
+
+            if (!"application/pkcs7-mime".equals(response.getHeaders().
+                    get(HTTPMessage.ContentTypeHeader))) {
+                throw new IOException("Unexpected Content-Type: " +
+                        response.getHeaders().get(HTTPMessage.ContentTypeHeader));
+            }
+            ByteBuffer octetBuffer = response.getBinaryPayload();
+            Collection<Asn1Object> pkcs7Content1 = Asn1Decoder.decode(octetBuffer);
+            for (Asn1Object asn1Object : pkcs7Content1) {
+                Log.d(TAG, "---");
+                Log.d(TAG, asn1Object.toString());
+            }
+            Log.d(TAG, CACERT_PATH);
+
+            mCACerts.addAll(unpackPkcs7(octetBuffer));
+            for (X509Certificate certificate : mCACerts) {
+                Log.d(TAG, "CA-Cert: " + certificate.getSubjectX500Principal());
+            }
+
+            /*
+            byte[] octets = new byte[octetBuffer.remaining()];
+            octetBuffer.duplicate().get(octets);
+            for (byte b : octets) {
+                System.out.printf("%02x ", b & 0xff);
+            }
+            Log.d(TAG, );
+            */
+
+            /* + BC
+            try {
+                byte[] octets = new byte[octetBuffer.remaining()];
+                octetBuffer.duplicate().get(octets);
+                ASN1InputStream asnin = new ASN1InputStream(octets);
+                for (int n = 0; n < 100; n++) {
+                    ASN1Primitive object = asnin.readObject();
+                    if (object == null) {
+                        break;
+                    }
+                    parseObject(object, 0);
+                }
+            }
+            catch (Throwable t) {
+                t.printStackTrace();
+            }
+
+            Collection<Asn1Object> pkcs7Content = Asn1Decoder.decode(octetBuffer);
+            for (Asn1Object asn1Object : pkcs7Content) {
+                Log.d(TAG, asn1Object);
+            }
+
+            if (pkcs7Content.size() != 1) {
+                throw new IOException("Unexpected pkcs 7 container: " + pkcs7Content.size());
+            }
+
+            Asn1Constructed pkcs7Root = (Asn1Constructed) pkcs7Content.iterator().next();
+            Iterator<Asn1ID> certPath = Arrays.asList(Pkcs7CertPath).iterator();
+            Asn1Object certObject = pkcs7Root.findObject(certPath);
+            if (certObject == null || certPath.hasNext()) {
+                throw new IOException("Failed to find cert; returned object " + certObject +
+                        ", path " + (certPath.hasNext() ? "short" : "exhausted"));
+            }
+
+            ByteBuffer certOctets = certObject.getPayload();
+            if (certOctets == null) {
+                throw new IOException("No cert payload in: " + certObject);
+            }
+
+            byte[] certBytes = new byte[certOctets.remaining()];
+            certOctets.get(certBytes);
+
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
+            Log.d(TAG, "EST Cert: " + cert);
+            */
+
+            URL csrURL = new URL(mURL.getProtocol(), mURL.getHost(), mURL.getPort(),
+                    mURL.getFile() + CSR_PATH);
+            response = httpHandler.doGetHTTP(csrURL);
+
+            octetBuffer = response.getBinaryPayload();
+            byte[] csrData = buildCSR(octetBuffer, mOMADMAdapter, httpHandler);
+
+        /**/
+            Collection<Asn1Object> o = Asn1Decoder.decode(ByteBuffer.wrap(csrData));
+            Log.d(TAG, "CSR:");
+            Log.d(TAG, o.iterator().next().toString());
+            Log.d(TAG, "End CSR.");
+        /**/
+
+            URL enrollURL = new URL(mURL.getProtocol(), mURL.getHost(), mURL.getPort(),
+                    mURL.getFile() + (reenroll ? SIMPLE_REENROLL_PATH : SIMPLE_ENROLL_PATH));
+            String data = Base64.encodeToString(csrData, Base64.DEFAULT);
+            octetBuffer = httpHandler.exchangeBinary(enrollURL, data, "application/pkcs10");
+
+            Collection<Asn1Object> pkcs7Content2 = Asn1Decoder.decode(octetBuffer);
+            for (Asn1Object asn1Object : pkcs7Content2) {
+                Log.d(TAG, "---");
+                Log.d(TAG, asn1Object.toString());
+            }
+            mClientCerts.addAll(unpackPkcs7(octetBuffer));
+            for (X509Certificate cert : mClientCerts) {
+                Log.d(TAG, cert.toString());
+            }
+        }
+    }
+
+    private static final Asn1ID sSEQUENCE = new Asn1ID(Asn1Decoder.TAG_SEQ, Asn1Class.Universal);
+    private static final Asn1ID sCTXT0 = new Asn1ID(0, Asn1Class.Context);
+    private static final int PKCS7DataVersion = 1;
+    private static final int PKCS7SignedDataVersion = 3;
+
+    private static List<X509Certificate> unpackPkcs7(ByteBuffer pkcs7)
+            throws IOException, GeneralSecurityException {
+        Collection<Asn1Object> pkcs7Content = Asn1Decoder.decode(pkcs7);
+
+        if (pkcs7Content.size() != 1) {
+            throw new IOException("Unexpected pkcs 7 container: " + pkcs7Content.size());
+        }
+
+        Asn1Object data = pkcs7Content.iterator().next();
+        if (!data.isConstructed() || !data.matches(sSEQUENCE)) {
+            throw new IOException("Expected SEQ OF, got " + data.toSimpleString());
+        } else if (data.getChildren().size() != 2) {
+            throw new IOException("Expected content info to have two children, got " +
+                    data.getChildren().size());
+        }
+
+        Iterator<Asn1Object> children = data.getChildren().iterator();
+        Asn1Object contentType = children.next();
+        if (!contentType.equals(Asn1Oid.PKCS7SignedData)) {
+            throw new IOException("Content not PKCS7 signed data");
+        }
+        Asn1Object content = children.next();
+        if (!content.isConstructed() || !content.matches(sCTXT0)) {
+            throw new IOException("Expected [CONTEXT 0] with one child, got " +
+                    content.toSimpleString() + ", " + content.getChildren().size());
+        }
+
+        Asn1Object signedData = content.getChildren().iterator().next();
+        Map<Integer, Asn1Object> itemMap = new HashMap<>();
+        for (Asn1Object item : signedData.getChildren()) {
+            if (itemMap.put(item.getTag(), item) != null && item.getTag() != Asn1Decoder.TAG_SET) {
+                throw new IOException("Duplicate item in SignedData: " + item.toSimpleString());
+            }
+        }
+
+        Asn1Object versionObject = itemMap.get(Asn1Decoder.TAG_INTEGER);
+        if (versionObject == null || !(versionObject instanceof Asn1Integer)) {
+            throw new IOException("Bad or missing PKCS7 version: " + versionObject);
+        }
+        int pkcs7version = (int) ((Asn1Integer) versionObject).getValue();
+        Asn1Object innerContentInfo = itemMap.get(Asn1Decoder.TAG_SEQ);
+        if (innerContentInfo == null ||
+                !innerContentInfo.isConstructed() ||
+                !innerContentInfo.matches(sSEQUENCE) ||
+                innerContentInfo.getChildren().size() != 1) {
+            throw new IOException("Bad or missing PKCS7 contentInfo");
+        }
+        Asn1Object contentID = innerContentInfo.getChildren().iterator().next();
+        if (pkcs7version == PKCS7DataVersion && !contentID.equals(Asn1Oid.PKCS7Data) ||
+                pkcs7version == PKCS7SignedDataVersion && !contentID.equals(Asn1Oid.PKCS7SignedData)) {
+            throw new IOException("Inner PKCS7 content (" + contentID +
+                    ") not expected for version " + pkcs7version);
+        }
+        Asn1Object certWrapper = itemMap.get(0);
+        if (certWrapper == null || !certWrapper.isConstructed() || !certWrapper.matches(sCTXT0)) {
+            throw new IOException("Expected [CONTEXT 0], got: " + certWrapper);
+        }
+
+        List<X509Certificate> certList = new ArrayList<>(certWrapper.getChildren().size());
+        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+        for (Asn1Object certObject : certWrapper.getChildren()) {
+            ByteBuffer certOctets = ((Asn1Constructed) certObject).getEncoding();
+            if (certOctets == null) {
+                throw new IOException("No cert payload in: " + certObject);
+            }
+            byte[] certBytes = new byte[certOctets.remaining()];
+            certOctets.get(certBytes);
+
+            certList.add((X509Certificate) certFactory.
+                    generateCertificate(new ByteArrayInputStream(certBytes)));
+        }
+        return certList;
+    }
+
+    private byte[] buildCSR(ByteBuffer octetBuffer, OMADMAdapter omadmAdapter,
+                            HTTPHandler httpHandler) throws IOException, GeneralSecurityException {
+
+        //Security.addProvider(new BouncyCastleProvider());
+
+        Log.d(TAG, "/csrattrs:");
+        /*
+        byte[] octets = new byte[octetBuffer.remaining()];
+        octetBuffer.duplicate().get(octets);
+        for (byte b : octets) {
+            System.out.printf("%02x ", b & 0xff);
+        }
+        */
+        Collection<Asn1Object> csrs = Asn1Decoder.decode(octetBuffer);
+        for (Asn1Object asn1Object : csrs) {
+            Log.d(TAG, asn1Object.toString());
+        }
+
+        if (csrs.size() != 1) {
+            throw new IOException("Unexpected object count in CSR attributes response: " +
+                    csrs.size());
+        }
+        Asn1Object sequence = csrs.iterator().next();
+        if (sequence.getClass() != Asn1Constructed.class) {
+            throw new IOException("Unexpected CSR attribute container: " + sequence);
+        }
+
+        String keyAlgo = null;
+        Asn1Oid keyAlgoOID = null;
+        String sigAlgo = null;
+        String curveName = null;
+        Asn1Oid pubCrypto = null;
+        int keySize = -1;
+        Map<Asn1Oid, ASN1Encodable> idAttributes = new HashMap<>();
+
+        for (Asn1Object child : sequence.getChildren()) {
+            if (child.getTag() == Asn1Decoder.TAG_OID) {
+                Asn1Oid oid = (Asn1Oid) child;
+                OidMappings.SigEntry sigEntry = OidMappings.getSigEntry(oid);
+                if (sigEntry != null) {
+                    sigAlgo = sigEntry.getSigAlgo();
+                    keyAlgoOID = sigEntry.getKeyAlgo();
+                    keyAlgo = OidMappings.getJCEName(keyAlgoOID);
+                } else if (oid.equals(OidMappings.sPkcs9AtChallengePassword)) {
+                    byte[] tlsUnique = httpHandler.getTLSUnique();
+                    if (tlsUnique != null) {
+                        idAttributes.put(oid, new DERPrintableString(
+                                Base64.encodeToString(tlsUnique, Base64.DEFAULT)));
+                    } else {
+                        Log.w(TAG, "Cannot retrieve TLS unique channel binding");
+                    }
+                }
+            } else if (child.getTag() == Asn1Decoder.TAG_SEQ) {
+                Asn1Oid oid = null;
+                Set<Asn1Oid> oidValues = new HashSet<>();
+                List<Asn1Object> values = new ArrayList<>();
+
+                for (Asn1Object attributeSeq : child.getChildren()) {
+                    if (attributeSeq.getTag() == Asn1Decoder.TAG_OID) {
+                        oid = (Asn1Oid) attributeSeq;
+                    } else if (attributeSeq.getTag() == Asn1Decoder.TAG_SET) {
+                        for (Asn1Object value : attributeSeq.getChildren()) {
+                            if (value.getTag() == Asn1Decoder.TAG_OID) {
+                                oidValues.add((Asn1Oid) value);
+                            } else {
+                                values.add(value);
+                            }
+                        }
+                    }
+                }
+                if (oid == null) {
+                    throw new IOException("Invalid attribute, no OID");
+                }
+                if (oid.equals(OidMappings.sExtensionRequest)) {
+                    for (Asn1Oid subOid : oidValues) {
+                        if (OidMappings.isIDAttribute(subOid)) {
+                            if (subOid.equals(OidMappings.sMAC)) {
+                                idAttributes.put(subOid, new DERIA5String(omadmAdapter.getMAC()));
+                            } else if (subOid.equals(OidMappings.sIMEI)) {
+                                idAttributes.put(subOid, new DERIA5String(omadmAdapter.getImei()));
+                            } else if (subOid.equals(OidMappings.sMEID)) {
+                                idAttributes.put(subOid, new DERBitString(omadmAdapter.getMeid()));
+                            } else if (subOid.equals(OidMappings.sDevID)) {
+                                idAttributes.put(subOid,
+                                        new DERPrintableString(omadmAdapter.getDevID()));
+                            }
+                        }
+                    }
+                } else if (OidMappings.getCryptoID(oid) != null) {
+                    pubCrypto = oid;
+                    if (!values.isEmpty()) {
+                        for (Asn1Object value : values) {
+                            if (value.getTag() == Asn1Decoder.TAG_INTEGER) {
+                                keySize = (int) ((Asn1Integer) value).getValue();
+                            }
+                        }
+                    }
+                    if (oid.equals(OidMappings.sAlgo_EC)) {
+                        if (oidValues.isEmpty()) {
+                            throw new IOException("No ECC curve name provided");
+                        }
+                        for (Asn1Oid value : oidValues) {
+                            curveName = OidMappings.getJCEName(value);
+                            if (curveName != null) {
+                                break;
+                            }
+                        }
+                        if (curveName == null) {
+                            throw new IOException("Found no ECC curve for " + oidValues);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (keyAlgoOID == null) {
+            throw new IOException("No public key algorithm specified");
+        }
+        if (pubCrypto != null && !pubCrypto.equals(keyAlgoOID)) {
+            throw new IOException("Mismatching key algorithms");
+        }
+
+        if (keyAlgoOID.equals(OidMappings.sAlgo_RSA)) {
+            if (keySize < MinRSAKeySize) {
+                if (keySize >= 0) {
+                    Log.i(TAG, "Upgrading suggested RSA key size from " +
+                            keySize + " to " + MinRSAKeySize);
+                }
+                keySize = MinRSAKeySize;
+            }
+        }
+
+        Log.d(TAG, String.format("pub key '%s', signature '%s', ECC curve '%s', id-atts %s",
+                keyAlgo, sigAlgo, curveName, idAttributes));
+
+        /*
+          Ruckus:
+            SEQUENCE:
+              OID=1.2.840.113549.1.1.11 (algo_id_sha256WithRSAEncryption)
+
+          RFC-7030:
+            SEQUENCE:
+              OID=1.2.840.113549.1.9.7 (challengePassword)
+              SEQUENCE:
+                OID=1.2.840.10045.2.1 (algo_id_ecPublicKey)
+                SET:
+                  OID=1.3.132.0.34 (secp384r1)
+              SEQUENCE:
+                OID=1.2.840.113549.1.9.14 (extensionRequest)
+                SET:
+                  OID=1.3.6.1.1.1.1.22 (mac-address)
+              OID=1.2.840.10045.4.3.3 (eccdaWithSHA384)
+
+              1L, 3L, 6L, 1L, 1L, 1L, 1L, 22
+         */
+
+        // ECC Does not appear to be supported currently
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlgo);
+        if (curveName != null) {
+            AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(keyAlgo);
+            algorithmParameters.init(new ECNamedCurveGenParameterSpec(curveName));
+            kpg.initialize(algorithmParameters
+                    .getParameterSpec(ECNamedCurveGenParameterSpec.class));
+        } else {
+            kpg.initialize(keySize);
+        }
+        KeyPair kp = kpg.generateKeyPair();
+
+        X500Principal subject = new X500Principal("CN=Android, O=Google, C=US");
+
+        mClientKey = kp.getPrivate();
+
+        // !!! Map the idAttributes into an ASN1Set of values to pass to
+        // the PKCS10CertificationRequest - this code is using outdated BC classes and
+        // has *not* been tested.
+        ASN1Set attributes;
+        if (!idAttributes.isEmpty()) {
+            ASN1EncodableVector payload = new DEREncodableVector();
+            for (Map.Entry<Asn1Oid, ASN1Encodable> entry : idAttributes.entrySet()) {
+                DERObjectIdentifier type = new DERObjectIdentifier(entry.getKey().toOIDString());
+                ASN1Set values = new DERSet(entry.getValue());
+                Attribute attribute = new Attribute(type, values);
+                payload.add(attribute);
+            }
+            attributes = new DERSet(payload);
+        } else {
+            attributes = null;
+        }
+
+        return new PKCS10CertificationRequest(sigAlgo, subject, kp.getPublic(),
+                attributes, mClientKey).getEncoded();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MOManager.java b/packages/Osu/src/com/android/hotspot2/omadm/MOManager.java
new file mode 100644
index 0000000..6a748cd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MOManager.java
@@ -0,0 +1,992 @@
+package com.android.hotspot2.omadm;
+
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.anqp.eap.EAP;
+import com.android.anqp.eap.EAPMethod;
+import com.android.anqp.eap.ExpandedEAPMethod;
+import com.android.anqp.eap.InnerAuthEAP;
+import com.android.anqp.eap.NonEAPInnerAuth;
+import com.android.hotspot2.IMSIParameter;
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.osu.OSUManager;
+import com.android.hotspot2.osu.commands.MOData;
+import com.android.hotspot2.pps.Credential;
+import com.android.hotspot2.pps.HomeSP;
+import com.android.hotspot2.pps.Policy;
+import com.android.hotspot2.pps.SubscriptionParameters;
+import com.android.hotspot2.pps.UpdateInfo;
+
+import org.xml.sax.SAXException;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+/**
+ * Handles provisioning of PerProviderSubscription data.
+ */
+public class MOManager {
+
+    public static final String TAG_AAAServerTrustRoot = "AAAServerTrustRoot";
+    public static final String TAG_AbleToShare = "AbleToShare";
+    public static final String TAG_CertificateType = "CertificateType";
+    public static final String TAG_CertSHA256Fingerprint = "CertSHA256Fingerprint";
+    public static final String TAG_CertURL = "CertURL";
+    public static final String TAG_CheckAAAServerCertStatus = "CheckAAAServerCertStatus";
+    public static final String TAG_Country = "Country";
+    public static final String TAG_CreationDate = "CreationDate";
+    public static final String TAG_Credential = "Credential";
+    public static final String TAG_CredentialPriority = "CredentialPriority";
+    public static final String TAG_DataLimit = "DataLimit";
+    public static final String TAG_DigitalCertificate = "DigitalCertificate";
+    public static final String TAG_DLBandwidth = "DLBandwidth";
+    public static final String TAG_EAPMethod = "EAPMethod";
+    public static final String TAG_EAPType = "EAPType";
+    public static final String TAG_ExpirationDate = "ExpirationDate";
+    public static final String TAG_Extension = "Extension";
+    public static final String TAG_FQDN = "FQDN";
+    public static final String TAG_FQDN_Match = "FQDN_Match";
+    public static final String TAG_FriendlyName = "FriendlyName";
+    public static final String TAG_HESSID = "HESSID";
+    public static final String TAG_HomeOI = "HomeOI";
+    public static final String TAG_HomeOIList = "HomeOIList";
+    public static final String TAG_HomeOIRequired = "HomeOIRequired";
+    public static final String TAG_HomeSP = "HomeSP";
+    public static final String TAG_IconURL = "IconURL";
+    public static final String TAG_IMSI = "IMSI";
+    public static final String TAG_InnerEAPType = "InnerEAPType";
+    public static final String TAG_InnerMethod = "InnerMethod";
+    public static final String TAG_InnerVendorID = "InnerVendorID";
+    public static final String TAG_InnerVendorType = "InnerVendorType";
+    public static final String TAG_IPProtocol = "IPProtocol";
+    public static final String TAG_MachineManaged = "MachineManaged";
+    public static final String TAG_MaximumBSSLoadValue = "MaximumBSSLoadValue";
+    public static final String TAG_MinBackhaulThreshold = "MinBackhaulThreshold";
+    public static final String TAG_NetworkID = "NetworkID";
+    public static final String TAG_NetworkType = "NetworkType";
+    public static final String TAG_Other = "Other";
+    public static final String TAG_OtherHomePartners = "OtherHomePartners";
+    public static final String TAG_Password = "Password";
+    public static final String TAG_PerProviderSubscription = "PerProviderSubscription";
+    public static final String TAG_Policy = "Policy";
+    public static final String TAG_PolicyUpdate = "PolicyUpdate";
+    public static final String TAG_PortNumber = "PortNumber";
+    public static final String TAG_PreferredRoamingPartnerList = "PreferredRoamingPartnerList";
+    public static final String TAG_Priority = "Priority";
+    public static final String TAG_Realm = "Realm";
+    public static final String TAG_RequiredProtoPortTuple = "RequiredProtoPortTuple";
+    public static final String TAG_Restriction = "Restriction";
+    public static final String TAG_RoamingConsortiumOI = "RoamingConsortiumOI";
+    public static final String TAG_SIM = "SIM";
+    public static final String TAG_SoftTokenApp = "SoftTokenApp";
+    public static final String TAG_SPExclusionList = "SPExclusionList";
+    public static final String TAG_SSID = "SSID";
+    public static final String TAG_StartDate = "StartDate";
+    public static final String TAG_SubscriptionParameters = "SubscriptionParameters";
+    public static final String TAG_SubscriptionUpdate = "SubscriptionUpdate";
+    public static final String TAG_TimeLimit = "TimeLimit";
+    public static final String TAG_TrustRoot = "TrustRoot";
+    public static final String TAG_TypeOfSubscription = "TypeOfSubscription";
+    public static final String TAG_ULBandwidth = "ULBandwidth";
+    public static final String TAG_UpdateIdentifier = "UpdateIdentifier";
+    public static final String TAG_UpdateInterval = "UpdateInterval";
+    public static final String TAG_UpdateMethod = "UpdateMethod";
+    public static final String TAG_URI = "URI";
+    public static final String TAG_UsageLimits = "UsageLimits";
+    public static final String TAG_UsageTimePeriod = "UsageTimePeriod";
+    public static final String TAG_Username = "Username";
+    public static final String TAG_UsernamePassword = "UsernamePassword";
+    public static final String TAG_VendorId = "VendorId";
+    public static final String TAG_VendorType = "VendorType";
+
+    public static final long IntervalFactor = 60000L;  // All MO intervals are in minutes
+
+    private static final DateFormat DTFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
+    private static final Map<String, Map<String, Object>> sSelectionMap;
+
+    static {
+        DTFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+        sSelectionMap = new HashMap<>();
+
+        setSelections(TAG_FQDN_Match,
+                "exactmatch", Boolean.FALSE,
+                "includesubdomains", Boolean.TRUE);
+        setSelections(TAG_UpdateMethod,
+                "oma-dm-clientinitiated", Boolean.FALSE,
+                "spp-clientinitiated", Boolean.TRUE);
+        setSelections(TAG_Restriction,
+                "homesp", UpdateInfo.UpdateRestriction.HomeSP,
+                "roamingpartner", UpdateInfo.UpdateRestriction.RoamingPartner,
+                "unrestricted", UpdateInfo.UpdateRestriction.Unrestricted);
+    }
+
+    private static void setSelections(String key, Object... pairs) {
+        Map<String, Object> kvp = new HashMap<>();
+        sSelectionMap.put(key, kvp);
+        for (int n = 0; n < pairs.length; n += 2) {
+            kvp.put(pairs[n].toString(), pairs[n + 1]);
+        }
+    }
+
+    private final File mPpsFile;
+    private final boolean mEnabled;
+    private final Map<String, HomeSP> mSPs;
+
+    public MOManager(File ppsFile, boolean hs2enabled) {
+        mPpsFile = ppsFile;
+        mEnabled = hs2enabled;
+        mSPs = new HashMap<>();
+    }
+
+    public File getPpsFile() {
+        return mPpsFile;
+    }
+
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    public boolean isConfigured() {
+        return mEnabled && !mSPs.isEmpty();
+    }
+
+    public Map<String, HomeSP> getLoadedSPs() {
+        return Collections.unmodifiableMap(mSPs);
+    }
+
+    public List<HomeSP> loadAllSPs() throws IOException {
+
+        if (!mEnabled || !mPpsFile.exists()) {
+            return Collections.emptyList();
+        }
+
+        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+            MOTree moTree = MOTree.unmarshal(in);
+            mSPs.clear();
+            if (moTree == null) {
+                return Collections.emptyList();     // Empty file
+            }
+
+            List<HomeSP> sps = buildSPs(moTree);
+            if (sps != null) {
+                for (HomeSP sp : sps) {
+                    if (mSPs.put(sp.getFQDN(), sp) != null) {
+                        throw new OMAException("Multiple SPs for FQDN '" + sp.getFQDN() + "'");
+                    } else {
+                        Log.d(OSUManager.TAG, "retrieved " + sp.getFQDN() + " from PPS");
+                    }
+                }
+                return sps;
+
+            } else {
+                throw new OMAException("Failed to build HomeSP");
+            }
+        }
+    }
+
+    public static HomeSP buildSP(String xml) throws IOException, SAXException {
+        OMAParser omaParser = new OMAParser();
+        MOTree tree = omaParser.parse(xml, OMAConstants.PPS_URN);
+        List<HomeSP> spList = buildSPs(tree);
+        if (spList.size() != 1) {
+            throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
+        }
+        return spList.iterator().next();
+    }
+
+    public HomeSP addSP(String xml, OSUManager osuManager) throws IOException, SAXException {
+        OMAParser omaParser = new OMAParser();
+        return addSP(omaParser.parse(xml, OMAConstants.PPS_URN), osuManager);
+    }
+
+    private static final List<String> FQDNPath = Arrays.asList(TAG_HomeSP, TAG_FQDN);
+
+    /**
+     * R1 *only* addSP method.
+     *
+     * @param homeSP
+     * @throws IOException
+     */
+    public void addSP(HomeSP homeSP, OSUManager osuManager) throws IOException {
+        if (!mEnabled) {
+            throw new IOException("HS2.0 not enabled on this device");
+        }
+        if (mSPs.containsKey(homeSP.getFQDN())) {
+            Log.d(OSUManager.TAG, "HS20 profile for " +
+                    homeSP.getFQDN() + " already exists");
+            return;
+        }
+        Log.d(OSUManager.TAG, "Adding new HS20 profile for " + homeSP.getFQDN());
+
+        OMAConstructed dummyRoot = new OMAConstructed(null, TAG_PerProviderSubscription, null);
+        buildHomeSPTree(homeSP, dummyRoot, mSPs.size() + 1);
+        try {
+            addSP(dummyRoot, osuManager);
+        } catch (FileNotFoundException fnfe) {
+            MOTree tree =
+                    MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
+            // No file to load a pre-build MO tree from, create a new one and save it.
+            //MOTree tree = new MOTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
+            writeMO(tree, mPpsFile, osuManager);
+        }
+        mSPs.put(homeSP.getFQDN(), homeSP);
+    }
+
+    public HomeSP addSP(MOTree instanceTree, OSUManager osuManager) throws IOException {
+        List<HomeSP> spList = buildSPs(instanceTree);
+        if (spList.size() != 1) {
+            throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
+        }
+
+        HomeSP sp = spList.iterator().next();
+        String fqdn = sp.getFQDN();
+        if (mSPs.put(fqdn, sp) != null) {
+            throw new OMAException("SP " + fqdn + " already exists");
+        }
+
+        OMAConstructed pps = (OMAConstructed) instanceTree.getRoot().
+                getChild(TAG_PerProviderSubscription);
+
+        try {
+            addSP(pps, osuManager);
+        } catch (FileNotFoundException fnfe) {
+            MOTree tree = new MOTree(instanceTree.getUrn(), instanceTree.getDtdRev(),
+                    instanceTree.getRoot());
+            writeMO(tree, mPpsFile, osuManager);
+        }
+
+        return sp;
+    }
+
+    /**
+     * Add an SP sub-tree. mo must be PPS with an immediate instance child (e.g. Cred01) and an
+     * optional UpdateIdentifier,
+     *
+     * @param mo The new MO
+     * @throws IOException
+     */
+    private void addSP(OMANode mo, OSUManager osuManager) throws IOException {
+        MOTree moTree;
+        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+            moTree = MOTree.unmarshal(in);
+            moTree.getRoot().addChild(mo);
+
+                /*
+            OMAConstructed ppsRoot = (OMAConstructed)
+                    moTree.getRoot().addChild(TAG_PerProviderSubscription, "", null, null);
+            for (OMANode child : mo.getChildren()) {
+                ppsRoot.addChild(child);
+                if (!child.isLeaf()) {
+                    moTree.getRoot().addChild(child);
+                }
+                else if (child.getName().equals(TAG_UpdateIdentifier)) {
+                    OMANode currentUD = moTree.getRoot().getChild(TAG_UpdateIdentifier);
+                    if (currentUD != null) {
+                        moTree.getRoot().replaceNode(currentUD, child);
+                    }
+                    else {
+                        moTree.getRoot().addChild(child);
+                    }
+                }
+            }
+                */
+        }
+        writeMO(moTree, mPpsFile, osuManager);
+    }
+
+    private static OMAConstructed findTargetTree(MOTree moTree, String fqdn) throws OMAException {
+        OMANode pps = moTree.getRoot();
+        for (OMANode node : pps.getChildren()) {
+            OMANode instance = null;
+            if (node.getName().equals(TAG_PerProviderSubscription)) {
+                instance = getInstanceNode((OMAConstructed) node);
+            } else if (!node.isLeaf()) {
+                instance = node;
+            }
+            if (instance != null) {
+                String nodeFqdn = getString(instance.getListValue(FQDNPath.iterator()));
+                if (fqdn.equalsIgnoreCase(nodeFqdn)) {
+                    return (OMAConstructed) node;
+                    // targetTree is rooted at the PPS
+                }
+            }
+        }
+        return null;
+    }
+
+    private static OMAConstructed getInstanceNode(OMAConstructed root) throws OMAException {
+        for (OMANode child : root.getChildren()) {
+            if (!child.isLeaf()) {
+                return (OMAConstructed) child;
+            }
+        }
+        throw new OMAException("Cannot find instance node");
+    }
+
+    public HomeSP modifySP(HomeSP homeSP, Collection<MOData> mods, OSUManager osuManager)
+            throws IOException {
+
+        Log.d(OSUManager.TAG, "modifying SP: " + mods);
+        MOTree moTree;
+        int ppsMods = 0;
+        int updateIdentifier = 0;
+        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+            moTree = MOTree.unmarshal(in);
+            // moTree is PPS/?/provider-data
+
+            OMAConstructed targetTree = findTargetTree(moTree, homeSP.getFQDN());
+            if (targetTree == null) {
+                throw new IOException("Failed to find PPS tree for " + homeSP.getFQDN());
+            }
+            OMAConstructed instance = getInstanceNode(targetTree);
+
+            for (MOData mod : mods) {
+                LinkedList<String> tailPath =
+                        getTailPath(mod.getBaseURI(), TAG_PerProviderSubscription);
+                OMAConstructed modRoot = mod.getMOTree().getRoot();
+                // modRoot is the MgmtTree with the actual object as a direct child
+                // (e.g. Credential)
+
+                if (tailPath.getFirst().equals(TAG_UpdateIdentifier)) {
+                    updateIdentifier = getInteger(modRoot.getChildren().iterator().next());
+                    OMANode oldUdi = targetTree.getChild(TAG_UpdateIdentifier);
+                    if (getInteger(oldUdi) != updateIdentifier) {
+                        ppsMods++;
+                    }
+                    if (oldUdi != null) {
+                        targetTree.replaceNode(oldUdi, modRoot.getChild(TAG_UpdateIdentifier));
+                    } else {
+                        targetTree.addChild(modRoot.getChild(TAG_UpdateIdentifier));
+                    }
+                } else {
+                    tailPath.removeFirst();     // Drop the instance
+                    OMANode current = instance.getListValue(tailPath.iterator());
+                    if (current == null) {
+                        throw new IOException("No previous node for " + tailPath + " in " +
+                                homeSP.getFQDN());
+                    }
+                    for (OMANode newNode : modRoot.getChildren()) {
+                        // newNode is something like Credential
+                        // current is the same existing node
+                        OMANode old = current.getParent().replaceNode(current, newNode);
+                        ppsMods++;
+                    }
+                }
+            }
+        }
+        writeMO(moTree, mPpsFile, osuManager);
+
+        if (ppsMods == 0) {
+            return null;    // HomeSP not modified.
+        }
+
+        // Return a new rebuilt HomeSP
+        List<HomeSP> sps = buildSPs(moTree);
+        if (sps != null) {
+            for (HomeSP sp : sps) {
+                if (sp.getFQDN().equals(homeSP.getFQDN())) {
+                    return sp;
+                }
+            }
+        } else {
+            throw new OMAException("Failed to build HomeSP");
+        }
+        return null;
+    }
+
+    private static LinkedList<String> getTailPath(String pathString, String rootName)
+            throws IOException {
+        String[] path = pathString.split("/");
+        int pathIndex;
+        for (pathIndex = 0; pathIndex < path.length; pathIndex++) {
+            if (path[pathIndex].equalsIgnoreCase(rootName)) {
+                pathIndex++;
+                break;
+            }
+        }
+        if (pathIndex >= path.length) {
+            throw new IOException("Bad node-path: " + pathString);
+        }
+        LinkedList<String> tailPath = new LinkedList<>();
+        while (pathIndex < path.length) {
+            tailPath.add(path[pathIndex]);
+            pathIndex++;
+        }
+        return tailPath;
+    }
+
+    public HomeSP getHomeSP(String fqdn) {
+        return mSPs.get(fqdn);
+    }
+
+    public void removeSP(String fqdn, OSUManager osuManager) throws IOException {
+        if (mSPs.remove(fqdn) == null) {
+            Log.d(OSUManager.TAG, "No HS20 profile to delete for " + fqdn);
+            return;
+        }
+
+        Log.d(OSUManager.TAG, "Deleting HS20 profile for " + fqdn);
+
+        MOTree moTree;
+        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+            moTree = MOTree.unmarshal(in);
+            OMAConstructed tbd = findTargetTree(moTree, fqdn);
+            if (tbd == null) {
+                throw new IOException("Node " + fqdn + " doesn't exist in MO tree");
+            }
+            OMAConstructed pps = moTree.getRoot();
+            OMANode removed = pps.removeNode("?", tbd);
+            if (removed == null) {
+                throw new IOException("Failed to remove " + fqdn + " out of MO tree");
+            }
+        }
+        writeMO(moTree, mPpsFile, osuManager);
+        osuManager.spDeleted(fqdn);
+    }
+
+    public MOTree getMOTree(HomeSP homeSP) throws IOException {
+        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+            MOTree moTree = MOTree.unmarshal(in);
+            OMAConstructed target = findTargetTree(moTree, homeSP.getFQDN());
+            if (target == null) {
+                throw new IOException("Can't find " + homeSP.getFQDN() + " in MO tree");
+            }
+            return MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, target);
+        }
+    }
+
+    private static void writeMO(MOTree moTree, File f, OSUManager osuManager) throws IOException {
+        try (BufferedOutputStream out =
+                     new BufferedOutputStream(new FileOutputStream(f, false))) {
+            moTree.marshal(out);
+            out.flush();
+        }
+    }
+
+    private static String fqdnList(Collection<HomeSP> sps) {
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for (HomeSP sp : sps) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append(", ");
+            }
+            sb.append(sp.getFQDN());
+        }
+        return sb.toString();
+    }
+
+    private static OMANode buildHomeSPTree(HomeSP homeSP, OMAConstructed root, int instanceID)
+            throws IOException {
+        OMANode providerSubNode = root.addChild(getInstanceString(instanceID),
+                null, null, null);
+
+        // The HomeSP:
+        OMANode homeSpNode = providerSubNode.addChild(TAG_HomeSP, null, null, null);
+        if (!homeSP.getSSIDs().isEmpty()) {
+            OMAConstructed nwkIDNode =
+                    (OMAConstructed) homeSpNode.addChild(TAG_NetworkID, null, null, null);
+            int instance = 0;
+            for (Map.Entry<String, Long> entry : homeSP.getSSIDs().entrySet()) {
+                OMAConstructed inode =
+                        (OMAConstructed) nwkIDNode
+                                .addChild(getInstanceString(instance++), null, null, null);
+                inode.addChild(TAG_SSID, null, entry.getKey(), null);
+                if (entry.getValue() != null) {
+                    inode.addChild(TAG_HESSID, null,
+                            String.format("%012x", entry.getValue()), null);
+                }
+            }
+        }
+
+        homeSpNode.addChild(TAG_FriendlyName, null, homeSP.getFriendlyName(), null);
+
+        if (homeSP.getIconURL() != null) {
+            homeSpNode.addChild(TAG_IconURL, null, homeSP.getIconURL(), null);
+        }
+
+        homeSpNode.addChild(TAG_FQDN, null, homeSP.getFQDN(), null);
+
+        if (!homeSP.getMatchAllOIs().isEmpty() || !homeSP.getMatchAnyOIs().isEmpty()) {
+            OMAConstructed homeOIList =
+                    (OMAConstructed) homeSpNode.addChild(TAG_HomeOIList, null, null, null);
+
+            int instance = 0;
+            for (Long oi : homeSP.getMatchAllOIs()) {
+                OMAConstructed inode =
+                        (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
+                                null, null, null);
+                inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
+                inode.addChild(TAG_HomeOIRequired, null, "TRUE", null);
+            }
+            for (Long oi : homeSP.getMatchAnyOIs()) {
+                OMAConstructed inode =
+                        (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
+                                null, null, null);
+                inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
+                inode.addChild(TAG_HomeOIRequired, null, "FALSE", null);
+            }
+        }
+
+        if (!homeSP.getOtherHomePartners().isEmpty()) {
+            OMAConstructed otherPartners =
+                    (OMAConstructed) homeSpNode.addChild(TAG_OtherHomePartners, null, null, null);
+            int instance = 0;
+            for (String fqdn : homeSP.getOtherHomePartners()) {
+                OMAConstructed inode =
+                        (OMAConstructed) otherPartners.addChild(getInstanceString(instance++),
+                                null, null, null);
+                inode.addChild(TAG_FQDN, null, fqdn, null);
+            }
+        }
+
+        if (!homeSP.getRoamingConsortiums().isEmpty()) {
+            homeSpNode.addChild(TAG_RoamingConsortiumOI, null,
+                    getRCList(homeSP.getRoamingConsortiums()), null);
+        }
+
+        // The Credential:
+        OMANode credentialNode = providerSubNode.addChild(TAG_Credential, null, null, null);
+        Credential cred = homeSP.getCredential();
+        EAPMethod method = cred.getEAPMethod();
+
+        if (cred.getCtime() > 0) {
+            credentialNode.addChild(TAG_CreationDate,
+                    null, DTFormat.format(new Date(cred.getCtime())), null);
+        }
+        if (cred.getExpTime() > 0) {
+            credentialNode.addChild(TAG_ExpirationDate,
+                    null, DTFormat.format(new Date(cred.getExpTime())), null);
+        }
+
+        if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_SIM
+                || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKA
+                || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKAPrim) {
+
+            OMANode simNode = credentialNode.addChild(TAG_SIM, null, null, null);
+            simNode.addChild(TAG_IMSI, null, cred.getImsi().toString(), null);
+            simNode.addChild(TAG_EAPType, null,
+                    Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
+
+        } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TTLS) {
+
+            OMANode unpNode = credentialNode.addChild(TAG_UsernamePassword, null, null, null);
+            unpNode.addChild(TAG_Username, null, cred.getUserName(), null);
+            unpNode.addChild(TAG_Password, null,
+                    Base64.encodeToString(cred.getPassword().getBytes(StandardCharsets.UTF_8),
+                            Base64.DEFAULT), null);
+            OMANode eapNode = unpNode.addChild(TAG_EAPMethod, null, null, null);
+            eapNode.addChild(TAG_EAPType, null,
+                    Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
+            eapNode.addChild(TAG_InnerMethod, null,
+                    ((NonEAPInnerAuth) method.getAuthParam()).getOMAtype(), null);
+
+        } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TLS) {
+
+            OMANode certNode = credentialNode.addChild(TAG_DigitalCertificate, null, null, null);
+            certNode.addChild(TAG_CertificateType, null, Credential.CertTypeX509, null);
+            certNode.addChild(TAG_CertSHA256Fingerprint, null,
+                    Utils.toHex(cred.getFingerPrint()), null);
+
+        } else {
+            throw new OMAException("Invalid credential on " + homeSP.getFQDN());
+        }
+
+        credentialNode.addChild(TAG_Realm, null, cred.getRealm(), null);
+
+        // !!! Note: This node defines CRL checking through OSCP, I suspect we won't be able
+        // to do that so it is commented out:
+        //credentialNode.addChild(TAG_CheckAAAServerCertStatus, null, "TRUE", null);
+        return providerSubNode;
+    }
+
+    private static String getInstanceString(int instance) {
+        return "r1i" + instance;
+    }
+
+    private static String getRCList(Collection<Long> rcs) {
+        StringBuilder builder = new StringBuilder();
+        boolean first = true;
+        for (Long roamingConsortium : rcs) {
+            if (first) {
+                first = false;
+            } else {
+                builder.append(',');
+            }
+            builder.append(String.format("%x", roamingConsortium));
+        }
+        return builder.toString();
+    }
+
+    public static List<HomeSP> buildSPs(MOTree moTree) throws OMAException {
+        OMAConstructed spList;
+        List<HomeSP> homeSPs = new ArrayList<>();
+        if (moTree.getRoot().getName().equals(TAG_PerProviderSubscription)) {
+            // The old PPS file was rooted at PPS instead of MgmtTree to conserve space
+            spList = moTree.getRoot();
+
+            if (spList == null) {
+                return homeSPs;
+            }
+
+            for (OMANode node : spList.getChildren()) {
+                if (!node.isLeaf()) {
+                    homeSPs.add(buildHomeSP(node, 0));
+                }
+            }
+        } else {
+            for (OMANode ppsRoot : moTree.getRoot().getChildren()) {
+                if (ppsRoot.getName().equals(TAG_PerProviderSubscription)) {
+                    Integer updateIdentifier = null;
+                    OMANode instance = null;
+                    for (OMANode child : ppsRoot.getChildren()) {
+                        if (child.getName().equals(TAG_UpdateIdentifier)) {
+                            updateIdentifier = getInteger(child);
+                        } else if (!child.isLeaf()) {
+                            instance = child;
+                        }
+                    }
+                    if (instance == null) {
+                        throw new OMAException("PPS node missing instance node");
+                    }
+                    homeSPs.add(buildHomeSP(instance,
+                            updateIdentifier != null ? updateIdentifier : 0));
+                }
+            }
+        }
+
+        return homeSPs;
+    }
+
+    private static HomeSP buildHomeSP(OMANode ppsRoot, int updateIdentifier) throws OMAException {
+        OMANode spRoot = ppsRoot.getChild(TAG_HomeSP);
+
+        String fqdn = spRoot.getScalarValue(Arrays.asList(TAG_FQDN).iterator());
+        String friendlyName = spRoot.getScalarValue(Arrays.asList(TAG_FriendlyName).iterator());
+        String iconURL = spRoot.getScalarValue(Arrays.asList(TAG_IconURL).iterator());
+
+        HashSet<Long> roamingConsortiums = new HashSet<>();
+        String oiString = spRoot.getScalarValue(Arrays.asList(TAG_RoamingConsortiumOI).iterator());
+        if (oiString != null) {
+            for (String oi : oiString.split(",")) {
+                roamingConsortiums.add(Long.parseLong(oi.trim(), 16));
+            }
+        }
+
+        Map<String, Long> ssids = new HashMap<>();
+
+        OMANode ssidListNode = spRoot.getListValue(Arrays.asList(TAG_NetworkID).iterator());
+        if (ssidListNode != null) {
+            for (OMANode ssidRoot : ssidListNode.getChildren()) {
+                OMANode hessidNode = ssidRoot.getChild(TAG_HESSID);
+                ssids.put(ssidRoot.getChild(TAG_SSID).getValue(), getMac(hessidNode));
+            }
+        }
+
+        Set<Long> matchAnyOIs = new HashSet<>();
+        List<Long> matchAllOIs = new ArrayList<>();
+        OMANode homeOIListNode = spRoot.getListValue(Arrays.asList(TAG_HomeOIList).iterator());
+        if (homeOIListNode != null) {
+            for (OMANode homeOIRoot : homeOIListNode.getChildren()) {
+                String homeOI = homeOIRoot.getChild(TAG_HomeOI).getValue();
+                if (Boolean.parseBoolean(homeOIRoot.getChild(TAG_HomeOIRequired).getValue())) {
+                    matchAllOIs.add(Long.parseLong(homeOI, 16));
+                } else {
+                    matchAnyOIs.add(Long.parseLong(homeOI, 16));
+                }
+            }
+        }
+
+        Set<String> otherHomePartners = new HashSet<>();
+        OMANode otherListNode =
+                spRoot.getListValue(Arrays.asList(TAG_OtherHomePartners).iterator());
+        if (otherListNode != null) {
+            for (OMANode fqdnNode : otherListNode.getChildren()) {
+                otherHomePartners.add(fqdnNode.getChild(TAG_FQDN).getValue());
+            }
+        }
+
+        Credential credential = buildCredential(ppsRoot.getChild(TAG_Credential));
+
+        OMANode policyNode = ppsRoot.getChild(TAG_Policy);
+        Policy policy = policyNode != null ? new Policy(policyNode) : null;
+
+        Map<String, String> aaaTrustRoots;
+        OMANode aaaRootNode = ppsRoot.getChild(TAG_AAAServerTrustRoot);
+        if (aaaRootNode == null) {
+            aaaTrustRoots = null;
+        } else {
+            aaaTrustRoots = new HashMap<>(aaaRootNode.getChildren().size());
+            for (OMANode child : aaaRootNode.getChildren()) {
+                aaaTrustRoots.put(getString(child, TAG_CertURL),
+                        getString(child, TAG_CertSHA256Fingerprint));
+            }
+        }
+
+        OMANode updateNode = ppsRoot.getChild(TAG_SubscriptionUpdate);
+        UpdateInfo subscriptionUpdate = updateNode != null ? new UpdateInfo(updateNode) : null;
+        OMANode subNode = ppsRoot.getChild(TAG_SubscriptionParameters);
+        SubscriptionParameters subscriptionParameters = subNode != null ?
+                new SubscriptionParameters(subNode) : null;
+
+        return new HomeSP(ssids, fqdn, roamingConsortiums, otherHomePartners,
+                matchAnyOIs, matchAllOIs, friendlyName, iconURL, credential,
+                policy, getInteger(ppsRoot.getChild(TAG_CredentialPriority), 0),
+                aaaTrustRoots, subscriptionUpdate, subscriptionParameters, updateIdentifier);
+    }
+
+    private static Credential buildCredential(OMANode credNode) throws OMAException {
+        long ctime = getTime(credNode.getChild(TAG_CreationDate));
+        long expTime = getTime(credNode.getChild(TAG_ExpirationDate));
+        String realm = getString(credNode.getChild(TAG_Realm));
+        boolean checkAAACert = getBoolean(credNode.getChild(TAG_CheckAAAServerCertStatus));
+
+        OMANode unNode = credNode.getChild(TAG_UsernamePassword);
+        OMANode certNode = credNode.getChild(TAG_DigitalCertificate);
+        OMANode simNode = credNode.getChild(TAG_SIM);
+
+        int alternatives = 0;
+        alternatives += unNode != null ? 1 : 0;
+        alternatives += certNode != null ? 1 : 0;
+        alternatives += simNode != null ? 1 : 0;
+        if (alternatives != 1) {
+            throw new OMAException("Expected exactly one credential type, got " + alternatives);
+        }
+
+        if (unNode != null) {
+            String userName = getString(unNode.getChild(TAG_Username));
+            String password = getString(unNode.getChild(TAG_Password));
+            boolean machineManaged = getBoolean(unNode.getChild(TAG_MachineManaged));
+            String softTokenApp = getString(unNode.getChild(TAG_SoftTokenApp));
+            boolean ableToShare = getBoolean(unNode.getChild(TAG_AbleToShare));
+
+            OMANode eapMethodNode = unNode.getChild(TAG_EAPMethod);
+            int eapID = getInteger(eapMethodNode.getChild(TAG_EAPType));
+
+            EAP.EAPMethodID eapMethodID = EAP.mapEAPMethod(eapID);
+            if (eapMethodID == null) {
+                throw new OMAException("Unknown EAP method: " + eapID);
+            }
+
+            Long vid = getOptionalInteger(eapMethodNode.getChild(TAG_VendorId));
+            Long vtype = getOptionalInteger(eapMethodNode.getChild(TAG_VendorType));
+            Long innerEAPType = getOptionalInteger(eapMethodNode.getChild(TAG_InnerEAPType));
+            EAP.EAPMethodID innerEAPMethod = null;
+            if (innerEAPType != null) {
+                innerEAPMethod = EAP.mapEAPMethod(innerEAPType.intValue());
+                if (innerEAPMethod == null) {
+                    throw new OMAException("Bad inner EAP method: " + innerEAPType);
+                }
+            }
+
+            Long innerVid = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorID));
+            Long innerVtype = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorType));
+            String innerNonEAPMethod = getString(eapMethodNode.getChild(TAG_InnerMethod));
+
+            EAPMethod eapMethod;
+            if (innerEAPMethod != null) {
+                eapMethod = new EAPMethod(eapMethodID, new InnerAuthEAP(innerEAPMethod));
+            } else if (vid != null) {
+                eapMethod = new EAPMethod(eapMethodID,
+                        new ExpandedEAPMethod(EAP.AuthInfoID.ExpandedEAPMethod,
+                                vid.intValue(), vtype));
+            } else if (innerVid != null) {
+                eapMethod =
+                        new EAPMethod(eapMethodID, new ExpandedEAPMethod(EAP.AuthInfoID
+                                .ExpandedInnerEAPMethod, innerVid.intValue(), innerVtype));
+            } else if (innerNonEAPMethod != null) {
+                eapMethod = new EAPMethod(eapMethodID, new NonEAPInnerAuth(innerNonEAPMethod));
+            } else {
+                throw new OMAException("Incomplete set of EAP parameters");
+            }
+
+            return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, userName,
+                    password, machineManaged, softTokenApp, ableToShare);
+        }
+        if (certNode != null) {
+            try {
+                String certTypeString = getString(certNode.getChild(TAG_CertificateType));
+                byte[] fingerPrint = getOctets(certNode.getChild(TAG_CertSHA256Fingerprint));
+
+                EAPMethod eapMethod = new EAPMethod(EAP.EAPMethodID.EAP_TLS, null);
+
+                return new Credential(ctime, expTime, realm, checkAAACert, eapMethod,
+                        Credential.mapCertType(certTypeString), fingerPrint);
+            } catch (NumberFormatException nfe) {
+                throw new OMAException("Bad hex string: " + nfe.toString());
+            }
+        }
+        if (simNode != null) {
+            try {
+                IMSIParameter imsi = new IMSIParameter(getString(simNode.getChild(TAG_IMSI)));
+
+                EAPMethod eapMethod =
+                        new EAPMethod(EAP.mapEAPMethod(getInteger(simNode.getChild(TAG_EAPType))),
+                                null);
+
+                return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, imsi);
+            } catch (IOException ioe) {
+                throw new OMAException("Failed to parse IMSI: " + ioe);
+            }
+        }
+        throw new OMAException("Missing credential parameters");
+    }
+
+    public static OMANode getChild(OMANode node, String key) throws OMAException {
+        OMANode child = node.getChild(key);
+        if (child == null) {
+            throw new OMAException("No such node: " + key);
+        }
+        return child;
+    }
+
+    public static String getString(OMANode node, String key) throws OMAException {
+        OMANode child = node.getChild(key);
+        if (child == null) {
+            throw new OMAException("Missing value for " + key);
+        } else if (!child.isLeaf()) {
+            throw new OMAException(key + " is not a leaf node");
+        }
+        return child.getValue();
+    }
+
+    public static long getLong(OMANode node, String key, Long dflt) throws OMAException {
+        OMANode child = node.getChild(key);
+        if (child == null) {
+            if (dflt != null) {
+                return dflt;
+            } else {
+                throw new OMAException("Missing value for " + key);
+            }
+        } else {
+            if (!child.isLeaf()) {
+                throw new OMAException(key + " is not a leaf node");
+            }
+            String value = child.getValue();
+            try {
+                long result = Long.parseLong(value);
+                if (result < 0) {
+                    throw new OMAException("Negative value for " + key);
+                }
+                return result;
+            } catch (NumberFormatException nfe) {
+                throw new OMAException("Value for " + key + " is non-numeric: " + value);
+            }
+        }
+    }
+
+    public static <T> T getSelection(OMANode node, String key) throws OMAException {
+        OMANode child = node.getChild(key);
+        if (child == null) {
+            throw new OMAException("Missing value for " + key);
+        } else if (!child.isLeaf()) {
+            throw new OMAException(key + " is not a leaf node");
+        }
+        return getSelection(key, child.getValue());
+    }
+
+    public static <T> T getSelection(String key, String value) throws OMAException {
+        if (value == null) {
+            throw new OMAException("No value for " + key);
+        }
+        Map<String, Object> kvp = sSelectionMap.get(key);
+        T result = (T) kvp.get(value.toLowerCase());
+        if (result == null) {
+            throw new OMAException("Invalid value '" + value + "' for " + key);
+        }
+        return result;
+    }
+
+    private static boolean getBoolean(OMANode boolNode) {
+        return boolNode != null && Boolean.parseBoolean(boolNode.getValue());
+    }
+
+    public static String getString(OMANode stringNode) {
+        return stringNode != null ? stringNode.getValue() : null;
+    }
+
+    private static int getInteger(OMANode intNode, int dflt) throws OMAException {
+        if (intNode == null) {
+            return dflt;
+        }
+        return getInteger(intNode);
+    }
+
+    private static int getInteger(OMANode intNode) throws OMAException {
+        if (intNode == null) {
+            throw new OMAException("Missing integer value");
+        }
+        try {
+            return Integer.parseInt(intNode.getValue());
+        } catch (NumberFormatException nfe) {
+            throw new OMAException("Invalid integer: " + intNode.getValue());
+        }
+    }
+
+    private static Long getMac(OMANode macNode) throws OMAException {
+        if (macNode == null) {
+            return null;
+        }
+        try {
+            return Long.parseLong(macNode.getValue(), 16);
+        } catch (NumberFormatException nfe) {
+            throw new OMAException("Invalid MAC: " + macNode.getValue());
+        }
+    }
+
+    private static Long getOptionalInteger(OMANode intNode) throws OMAException {
+        if (intNode == null) {
+            return null;
+        }
+        try {
+            return Long.parseLong(intNode.getValue());
+        } catch (NumberFormatException nfe) {
+            throw new OMAException("Invalid integer: " + intNode.getValue());
+        }
+    }
+
+    public static long getTime(OMANode timeNode) throws OMAException {
+        if (timeNode == null) {
+            return Utils.UNSET_TIME;
+        }
+        String timeText = timeNode.getValue();
+        try {
+            Date date = DTFormat.parse(timeText);
+            return date.getTime();
+        } catch (ParseException pe) {
+            throw new OMAException("Badly formatted time: " + timeText);
+        }
+    }
+
+    private static byte[] getOctets(OMANode octetNode) throws OMAException {
+        if (octetNode == null) {
+            throw new OMAException("Missing byte value");
+        }
+        return Utils.hexToBytes(octetNode.getValue());
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MOTree.java b/packages/Osu/src/com/android/hotspot2/omadm/MOTree.java
new file mode 100644
index 0000000..93beaf4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MOTree.java
@@ -0,0 +1,269 @@
+package com.android.hotspot2.omadm;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class MOTree {
+    public static final String MgmtTreeTag = "MgmtTree";
+
+    public static final String NodeTag = "Node";
+    public static final String NodeNameTag = "NodeName";
+    public static final String PathTag = "Path";
+    public static final String ValueTag = "Value";
+    public static final String RTPropTag = "RTProperties";
+    public static final String TypeTag = "Type";
+    public static final String DDFNameTag = "DDFName";
+
+    private final String mUrn;
+    private final String mDtdRev;
+    private final OMAConstructed mRoot;
+
+    public MOTree(XMLNode node, String urn) throws IOException, SAXException {
+        Iterator<XMLNode> children = node.getChildren().iterator();
+
+        String dtdRev = null;
+
+        while (children.hasNext()) {
+            XMLNode child = children.next();
+            if (child.getTag().equals(OMAConstants.SyncMLVersionTag)) {
+                dtdRev = child.getText();
+                children.remove();
+                break;
+            }
+        }
+
+        mUrn = urn;
+        mDtdRev = dtdRev;
+
+        mRoot = new MgmtTreeRoot(node, dtdRev);
+
+        for (XMLNode child : node.getChildren()) {
+            buildNode(mRoot, child);
+        }
+    }
+
+    public MOTree(String urn, String rev, OMAConstructed root) throws IOException {
+        mUrn = urn;
+        mDtdRev = rev;
+        mRoot = root;
+    }
+
+    public static MOTree buildMgmtTree(String urn, String rev, OMAConstructed root)
+            throws IOException {
+        OMAConstructed realRoot;
+        switch (urn) {
+            case OMAConstants.PPS_URN:
+            case OMAConstants.DevInfoURN:
+            case OMAConstants.DevDetailURN:
+            case OMAConstants.DevDetailXURN:
+                realRoot = new OMAConstructed(null, MgmtTreeTag, urn, "xmlns", OMAConstants.SyncML);
+                realRoot.addChild(root);
+                return new MOTree(urn, rev, realRoot);
+            default:
+                return new MOTree(urn, rev, root);
+        }
+    }
+
+    public static boolean hasMgmtTreeTag(String text) {
+        for (int n = 0; n < text.length(); n++) {
+            char ch = text.charAt(n);
+            if (ch > ' ') {
+                return text.regionMatches(true, n, '<' + MgmtTreeTag + '>',
+                        0, MgmtTreeTag.length() + 2);
+            }
+        }
+        return false;
+    }
+
+    private static class NodeData {
+        private final String mName;
+        private String mPath;
+        private String mValue;
+
+        private NodeData(String name) {
+            mName = name;
+        }
+
+        private void setPath(String path) {
+            mPath = path;
+        }
+
+        private void setValue(String value) {
+            mValue = value;
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        public String getPath() {
+            return mPath;
+        }
+
+        public String getValue() {
+            return mValue;
+        }
+    }
+
+    private static void buildNode(OMANode parent, XMLNode node) throws IOException {
+        if (!node.getTag().equals(NodeTag))
+            throw new IOException("Node is a '" + node.getTag() + "' instead of a 'Node'");
+
+        Map<String, XMLNode> checkMap = new HashMap<>(3);
+        String context = null;
+        List<NodeData> values = new ArrayList<>();
+        List<XMLNode> children = new ArrayList<>();
+
+        NodeData curValue = null;
+
+        for (XMLNode child : node.getChildren()) {
+            XMLNode old = checkMap.put(child.getTag(), child);
+
+            switch (child.getTag()) {
+                case NodeNameTag:
+                    if (curValue != null)
+                        throw new IOException(NodeNameTag + " not expected");
+                    curValue = new NodeData(child.getText());
+
+                    break;
+                case PathTag:
+                    if (curValue == null || curValue.getPath() != null)
+                        throw new IOException(PathTag + " not expected");
+                    curValue.setPath(child.getText());
+
+                    break;
+                case ValueTag:
+                    if (!children.isEmpty())
+                        throw new IOException(ValueTag + " in constructed node");
+                    if (curValue == null || curValue.getValue() != null)
+                        throw new IOException(ValueTag + " not expected");
+                    curValue.setValue(child.getText());
+                    values.add(curValue);
+                    curValue = null;
+
+                    break;
+                case RTPropTag:
+                    if (old != null)
+                        throw new IOException("Duplicate " + RTPropTag);
+                    XMLNode typeNode = getNextNode(child, TypeTag);
+                    XMLNode ddfName = getNextNode(typeNode, DDFNameTag);
+                    context = ddfName.getText();
+                    if (context == null)
+                        throw new IOException("No text in " + DDFNameTag);
+
+                    break;
+                case NodeTag:
+                    if (!values.isEmpty())
+                        throw new IOException("Scalar node " + node.getText() + " has Node child");
+                    children.add(child);
+
+                    break;
+            }
+        }
+
+        if (values.isEmpty()) {
+            if (curValue == null)
+                throw new IOException("Missing name");
+
+            OMANode subNode = parent.addChild(curValue.getName(),
+                    context, null, curValue.getPath());
+
+            for (XMLNode child : children) {
+                buildNode(subNode, child);
+            }
+        } else {
+            if (!children.isEmpty())
+                throw new IOException("Got both sub nodes and value(s)");
+
+            for (NodeData nodeData : values) {
+                parent.addChild(nodeData.getName(), context,
+                        nodeData.getValue(), nodeData.getPath());
+            }
+        }
+    }
+
+    private static XMLNode getNextNode(XMLNode node, String tag) throws IOException {
+        if (node == null)
+            throw new IOException("No node for " + tag);
+        if (node.getChildren().size() != 1)
+            throw new IOException("Expected " + node.getTag() + " to have exactly one child");
+        XMLNode child = node.getChildren().iterator().next();
+        if (!child.getTag().equals(tag))
+            throw new IOException("Expected " + node.getTag() + " to have child '" + tag +
+                    "' instead of '" + child.getTag() + "'");
+        return child;
+    }
+
+    public String getUrn() {
+        return mUrn;
+    }
+
+    public String getDtdRev() {
+        return mDtdRev;
+    }
+
+    public OMAConstructed getRoot() {
+        return mRoot;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("MO Tree v").append(mDtdRev).append(", urn ").append(mUrn).append(")\n");
+        sb.append(mRoot);
+
+        return sb.toString();
+    }
+
+    public void marshal(OutputStream out) throws IOException {
+        out.write("tree ".getBytes(StandardCharsets.UTF_8));
+        OMAConstants.serializeString(mDtdRev, out);
+        out.write(String.format("(%s)\n", mUrn).getBytes(StandardCharsets.UTF_8));
+        mRoot.marshal(out, 0);
+    }
+
+    public static MOTree unmarshal(InputStream in) throws IOException {
+        boolean strip = true;
+        StringBuilder tree = new StringBuilder();
+        for (; ; ) {
+            int octet = in.read();
+            if (octet < 0) {
+                return null;
+            } else if (octet > ' ') {
+                tree.append((char) octet);
+                strip = false;
+            } else if (!strip) {
+                break;
+            }
+        }
+        if (!tree.toString().equals("tree")) {
+            throw new IOException("Not a tree: " + tree);
+        }
+
+        String version = OMAConstants.deserializeString(in);
+        int next = in.read();
+        if (next != '(') {
+            throw new IOException("Expected URN in tree definition");
+        }
+        String urn = OMAConstants.readURN(in);
+
+        OMAConstructed root = OMANode.unmarshal(in);
+
+        return new MOTree(urn, version, root);
+    }
+
+    public String toXml() {
+        StringBuilder sb = new StringBuilder();
+        mRoot.toXml(sb);
+        return sb.toString();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MgmtTreeRoot.java b/packages/Osu/src/com/android/hotspot2/omadm/MgmtTreeRoot.java
new file mode 100644
index 0000000..97fb7cd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MgmtTreeRoot.java
@@ -0,0 +1,32 @@
+package com.android.hotspot2.omadm;
+
+import java.util.Map;
+
+public class MgmtTreeRoot extends OMAConstructed {
+    private final String mDtdRev;
+
+    public MgmtTreeRoot(XMLNode node, String dtdRev) {
+        super(null, MOTree.MgmtTreeTag, null, new MultiValueMap<OMANode>(),
+                node.getTextualAttributes());
+        mDtdRev = dtdRev;
+    }
+
+    @Override
+    public void toXml(StringBuilder sb) {
+        sb.append('<').append(MOTree.MgmtTreeTag);
+        if (getAttributes() != null && !getAttributes().isEmpty()) {
+            for (Map.Entry<String, String> avp : getAttributes().entrySet()) {
+                sb.append(' ').append(avp.getKey()).append("=\"")
+                        .append(avp.getValue()).append('"');
+            }
+        }
+        sb.append(">\n");
+
+        sb.append('<').append(OMAConstants.SyncMLVersionTag).append('>').append(mDtdRev)
+                .append("</").append(OMAConstants.SyncMLVersionTag).append(">\n");
+        for (OMANode child : getChildren()) {
+            child.toXml(sb);
+        }
+        sb.append("</").append(MOTree.MgmtTreeTag).append(">\n");
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MultiValueMap.java b/packages/Osu/src/com/android/hotspot2/omadm/MultiValueMap.java
new file mode 100644
index 0000000..ead0dbc
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MultiValueMap.java
@@ -0,0 +1,117 @@
+package com.android.hotspot2.omadm;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MultiValueMap<T> {
+    private final Map<String, ArrayList<T>> mMap = new LinkedHashMap<>();
+
+    public void put(String key, T value) {
+        key = key.toLowerCase();
+        ArrayList<T> values = mMap.get(key);
+        if (values == null) {
+            values = new ArrayList<>();
+            mMap.put(key, values);
+        }
+        values.add(value);
+    }
+
+    public T get(String key) {
+        key = key.toLowerCase();
+        List<T> values = mMap.get(key);
+        if (values == null) {
+            return null;
+        } else if (values.size() == 1) {
+            return values.get(0);
+        } else {
+            throw new IllegalArgumentException("Cannot do get on multi-value");
+        }
+    }
+
+    public T replace(String key, T oldValue, T newValue) {
+        key = key.toLowerCase();
+        List<T> values = mMap.get(key);
+        if (values == null) {
+            return null;
+        }
+
+        for (int n = 0; n < values.size(); n++) {
+            T value = values.get(n);
+            if (value == oldValue) {
+                values.set(n, newValue);
+                return value;
+            }
+        }
+        return null;
+    }
+
+    public T remove(String key, T value) {
+        key = key.toLowerCase();
+        List<T> values = mMap.get(key);
+        if (values == null) {
+            return null;
+        }
+
+        T result = null;
+        Iterator<T> valueIterator = values.iterator();
+        while (valueIterator.hasNext()) {
+            if (valueIterator.next() == value) {
+                valueIterator.remove();
+                result = value;
+                break;
+            }
+        }
+        if (values.isEmpty()) {
+            mMap.remove(key);
+        }
+        return result;
+    }
+
+    public T remove(T value) {
+        T result = null;
+        Iterator<Map.Entry<String, ArrayList<T>>> iterator = mMap.entrySet().iterator();
+        while (iterator.hasNext()) {
+            ArrayList<T> values = iterator.next().getValue();
+            Iterator<T> valueIterator = values.iterator();
+            while (valueIterator.hasNext()) {
+                if (valueIterator.next() == value) {
+                    valueIterator.remove();
+                    result = value;
+                    break;
+                }
+            }
+            if (result != null) {
+                if (values.isEmpty()) {
+                    iterator.remove();
+                }
+                break;
+            }
+        }
+        return result;
+    }
+
+    public Collection<T> values() {
+        List<T> allValues = new ArrayList<>(mMap.size());
+        for (List<T> values : mMap.values()) {
+            for (T value : values) {
+                allValues.add(value);
+            }
+        }
+        return allValues;
+    }
+
+    public T getSingletonValue() {
+        if (mMap.size() != 1) {
+            throw new IllegalArgumentException("Map is not a single entry map");
+        }
+        List<T> values = mMap.values().iterator().next();
+        if (values.size() != 1) {
+            throw new IllegalArgumentException("Map is not a single entry map");
+        }
+        return values.iterator().next();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/NodeAttribute.java b/packages/Osu/src/com/android/hotspot2/omadm/NodeAttribute.java
new file mode 100644
index 0000000..e4a08b3
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/NodeAttribute.java
@@ -0,0 +1,30 @@
+package com.android.hotspot2.omadm;
+
+public class NodeAttribute {
+    private final String mName;
+    private final String mType;
+    private final String mValue;
+
+    public NodeAttribute(String name, String type, String value) {
+        mName = name;
+        mType = type;
+        mValue = value;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getValue() {
+        return mValue;
+    }
+
+    public String getType() {
+        return mType;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s (%s) = '%s'", mName, mType, mValue);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAConstants.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstants.java
new file mode 100644
index 0000000..92d8ed7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstants.java
@@ -0,0 +1,158 @@
+package com.android.hotspot2.omadm;
+
+import com.android.hotspot2.osu.OSUError;
+import com.android.hotspot2.osu.OSUStatus;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OMAConstants {
+    private OMAConstants() {
+    }
+
+    public static final String MOVersion = "1.0";
+    public static final String PPS_URN = "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0";
+    public static final String DevInfoURN = "urn:oma:mo:oma-dm-devinfo:1.0";
+    public static final String DevDetailURN = "urn:oma:mo:oma-dm-devdetail:1.0";
+    public static final String DevDetailXURN = "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext:1.0";
+
+    public static final String[] SupportedMO_URNs = {
+            PPS_URN, DevInfoURN, DevDetailURN, DevDetailXURN
+    };
+
+    public static final String SppMOAttribute = "spp:moURN";
+    public static final String TAG_PostDevData = "spp:sppPostDevData";
+    public static final String TAG_SupportedVersions = "spp:supportedSPPVersions";
+    public static final String TAG_SupportedMOs = "spp:supportedMOList";
+    public static final String TAG_UpdateResponse = "spp:sppUpdateResponse";
+    public static final String TAG_MOContainer = "spp:moContainer";
+    public static final String TAG_Version = "spp:sppVersion";
+
+    public static final String TAG_SessionID = "spp:sessionID";
+    public static final String TAG_Status = "spp:sppStatus";
+    public static final String TAG_Error = "spp:sppError";
+
+    public static final String SyncMLVersionTag = "VerDTD";
+    public static final String OMAVersion = "1.2";
+    public static final String SyncML = "syncml:dmddf1.2";
+
+    private static final byte[] INDENT = new byte[1024];
+
+    private static final Map<OSUStatus, String> sStatusStrings = new EnumMap<>(OSUStatus.class);
+    private static final Map<String, OSUStatus> sStatusEnums = new HashMap<>();
+    private static final Map<OSUError, String> sErrorStrings = new EnumMap<>(OSUError.class);
+    private static final Map<String, OSUError> sErrorEnums = new HashMap<>();
+
+    static {
+        sStatusStrings.put(OSUStatus.OK, "OK");
+        sStatusStrings.put(OSUStatus.ProvComplete,
+                "Provisioning complete, request sppUpdateResponse");
+        sStatusStrings.put(OSUStatus.RemediationComplete,
+                "Remediation complete, request sppUpdateResponse");
+        sStatusStrings.put(OSUStatus.UpdateComplete, "Update complete, request sppUpdateResponse");
+        sStatusStrings.put(OSUStatus.ExchangeComplete, "Exchange complete, release TLS connection");
+        sStatusStrings.put(OSUStatus.Unknown, "No update available at this time");
+        sStatusStrings.put(OSUStatus.Error, "Error occurred");
+
+        for (Map.Entry<OSUStatus, String> entry : sStatusStrings.entrySet()) {
+            sStatusEnums.put(entry.getValue().toLowerCase(), entry.getKey());
+        }
+
+        sErrorStrings.put(OSUError.SPPversionNotSupported, "SPP version not supported");
+        sErrorStrings.put(OSUError.MOsNotSupported, "One or more mandatory MOs not supported");
+        sErrorStrings.put(OSUError.CredentialsFailure,
+                "Credentials cannot be provisioned at this time");
+        sErrorStrings.put(OSUError.RemediationFailure,
+                "Remediation cannot be completed at this time");
+        sErrorStrings.put(OSUError.ProvisioningFailed,
+                "Provisioning cannot be completed at this time");
+        sErrorStrings.put(OSUError.ExistingCertificate, "Continue to use existing certificate");
+        sErrorStrings.put(OSUError.CookieInvalid, "Cookie invalid");
+        sErrorStrings.put(OSUError.WebSessionID,
+                "No corresponding web-browser-connection Session ID");
+        sErrorStrings.put(OSUError.PermissionDenied, "Permission denied");
+        sErrorStrings.put(OSUError.CommandFailed, "Command failed");
+        sErrorStrings.put(OSUError.MOaddOrUpdateFailed, "MO addition or update failed");
+        sErrorStrings.put(OSUError.DeviceFull, "Device full");
+        sErrorStrings.put(OSUError.BadTreeURI, "Bad management tree URI");
+        sErrorStrings.put(OSUError.TooLarge, "Requested entity too large");
+        sErrorStrings.put(OSUError.CommandNotAllowed, "Command not allowed");
+        sErrorStrings.put(OSUError.UserAborted, "Command not executed due to user");
+        sErrorStrings.put(OSUError.NotFound, "Not found");
+        sErrorStrings.put(OSUError.Other, "Other");
+
+        for (Map.Entry<OSUError, String> entry : sErrorStrings.entrySet()) {
+            sErrorEnums.put(entry.getValue().toLowerCase(), entry.getKey());
+        }
+        Arrays.fill(INDENT, (byte) ' ');
+    }
+
+    public static String mapStatus(OSUStatus status) {
+        return sStatusStrings.get(status);
+    }
+
+    public static OSUStatus mapStatus(String status) {
+        return sStatusEnums.get(status.toLowerCase());
+    }
+
+    public static String mapError(OSUError error) {
+        return sErrorStrings.get(error);
+    }
+
+    public static OSUError mapError(String error) {
+        return sErrorEnums.get(error.toLowerCase());
+    }
+
+    public static void serializeString(String s, OutputStream out) throws IOException {
+        byte[] octets = s.getBytes(StandardCharsets.UTF_8);
+        byte[] prefix = String.format("%x:", octets.length).getBytes(StandardCharsets.UTF_8);
+        out.write(prefix);
+        out.write(octets);
+    }
+
+    public static void indent(int level, OutputStream out) throws IOException {
+        out.write(INDENT, 0, level);
+    }
+
+    public static String deserializeString(InputStream in) throws IOException {
+        StringBuilder prefix = new StringBuilder();
+        for (; ; ) {
+            byte b = (byte) in.read();
+            if (b == '.')
+                return null;
+            else if (b == ':')
+                break;
+            else if (b > ' ')
+                prefix.append((char) b);
+        }
+        int length = Integer.parseInt(prefix.toString(), 16);
+        byte[] octets = new byte[length];
+        int offset = 0;
+        while (offset < octets.length) {
+            int amount = in.read(octets, offset, octets.length - offset);
+            if (amount <= 0)
+                throw new EOFException();
+            offset += amount;
+        }
+        return new String(octets, StandardCharsets.UTF_8);
+    }
+
+    public static String readURN(InputStream in) throws IOException {
+        StringBuilder urn = new StringBuilder();
+
+        for (; ; ) {
+            byte b = (byte) in.read();
+            if (b == ')')
+                break;
+            urn.append((char) b);
+        }
+        return urn.toString();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAConstructed.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstructed.java
new file mode 100644
index 0000000..e5285f2
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstructed.java
@@ -0,0 +1,169 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+public class OMAConstructed extends OMANode {
+    private final MultiValueMap<OMANode> mChildren;
+
+    public OMAConstructed(OMAConstructed parent, String name, String context, String... avps) {
+        this(parent, name, context, new MultiValueMap<OMANode>(), buildAttributes(avps));
+    }
+
+    protected OMAConstructed(OMAConstructed parent, String name, String context,
+                             MultiValueMap<OMANode> children, Map<String, String> avps) {
+        super(parent, name, context, avps);
+        mChildren = children;
+    }
+
+    @Override
+    public OMANode addChild(String name, String context, String value, String pathString)
+            throws IOException {
+        if (pathString == null) {
+            OMANode child = value != null ?
+                    new OMAScalar(this, name, context, value) :
+                    new OMAConstructed(this, name, context);
+            mChildren.put(name, child);
+            return child;
+        } else {
+            OMANode target = this;
+            while (target.getParent() != null)
+                target = target.getParent();
+
+            for (String element : pathString.split("/")) {
+                target = target.getChild(element);
+                if (target == null)
+                    throw new IOException("No child node '" + element + "' in " + getPathString());
+                else if (target.isLeaf())
+                    throw new IOException("Cannot add child to leaf node: " + getPathString());
+            }
+            return target.addChild(name, context, value, null);
+        }
+    }
+
+    @Override
+    public OMAConstructed reparent(OMAConstructed parent) {
+        return new OMAConstructed(parent, getName(), getContext(), mChildren, getAttributes());
+    }
+
+    public void addChild(OMANode child) {
+        mChildren.put(child.getName(), child.reparent(this));
+    }
+
+    public String getScalarValue(Iterator<String> path) throws OMAException {
+        if (!path.hasNext()) {
+            throw new OMAException("Path too short for " + getPathString());
+        }
+        String tag = path.next();
+        OMANode child = mChildren.get(tag);
+        if (child != null) {
+            return child.getScalarValue(path);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public OMANode getListValue(Iterator<String> path) throws OMAException {
+        if (!path.hasNext()) {
+            return null;
+        }
+        String tag = path.next();
+        OMANode child;
+        if (tag.equals("?")) {
+            child = mChildren.getSingletonValue();
+        } else {
+            child = mChildren.get(tag);
+        }
+
+        if (child == null) {
+            return null;
+        } else if (path.hasNext()) {
+            return child.getListValue(path);
+        } else {
+            return child;
+        }
+    }
+
+    @Override
+    public boolean isLeaf() {
+        return false;
+    }
+
+    @Override
+    public Collection<OMANode> getChildren() {
+        return Collections.unmodifiableCollection(mChildren.values());
+    }
+
+    public OMANode getChild(String name) {
+        return mChildren.get(name);
+    }
+
+    public OMANode replaceNode(OMANode oldNode, OMANode newNode) {
+        return mChildren.replace(oldNode.getName(), oldNode, newNode);
+    }
+
+    public OMANode removeNode(String key, OMANode node) {
+        if (key.equals("?")) {
+            return mChildren.remove(node);
+        } else {
+            return mChildren.remove(key, node);
+        }
+    }
+
+    @Override
+    public String getValue() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void toString(StringBuilder sb, int level) {
+        sb.append(getPathString());
+        if (getContext() != null) {
+            sb.append(" (").append(getContext()).append(')');
+        }
+        sb.append('\n');
+
+        for (OMANode node : mChildren.values()) {
+            node.toString(sb, level + 1);
+        }
+    }
+
+    @Override
+    public void marshal(OutputStream out, int level) throws IOException {
+        OMAConstants.indent(level, out);
+        OMAConstants.serializeString(getName(), out);
+        if (getContext() != null) {
+            out.write(String.format("(%s)", getContext()).getBytes(StandardCharsets.UTF_8));
+        }
+        out.write(new byte[]{'+', '\n'});
+
+        for (OMANode child : mChildren.values()) {
+            child.marshal(out, level + 1);
+        }
+        OMAConstants.indent(level, out);
+        out.write(".\n".getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public void fillPayload(StringBuilder sb) {
+        if (getContext() != null) {
+            sb.append('<').append(MOTree.RTPropTag).append(">\n");
+            sb.append('<').append(MOTree.TypeTag).append(">\n");
+            sb.append('<').append(MOTree.DDFNameTag).append(">");
+            sb.append(getContext());
+            sb.append("</").append(MOTree.DDFNameTag).append(">\n");
+            sb.append("</").append(MOTree.TypeTag).append(">\n");
+            sb.append("</").append(MOTree.RTPropTag).append(">\n");
+        }
+
+        for (OMANode child : getChildren()) {
+            child.toXml(sb);
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAException.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAException.java
new file mode 100644
index 0000000..33a6e37
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAException.java
@@ -0,0 +1,9 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+
+public class OMAException extends IOException {
+    public OMAException(String message) {
+        super(message);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMANode.java b/packages/Osu/src/com/android/hotspot2/omadm/OMANode.java
new file mode 100644
index 0000000..a428b2f
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMANode.java
@@ -0,0 +1,165 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public abstract class OMANode {
+    private final OMAConstructed mParent;
+    private final String mName;
+    private final String mContext;
+    private final Map<String, String> mAttributes;
+
+    protected OMANode(OMAConstructed parent, String name, String context,
+                      Map<String, String> avps) {
+        mParent = parent;
+        mName = name;
+        mContext = context;
+        mAttributes = avps;
+    }
+
+    protected static Map<String, String> buildAttributes(String[] avps) {
+        if (avps == null) {
+            return null;
+        }
+        Map<String, String> attributes = new HashMap<>();
+        for (int n = 0; n < avps.length; n += 2) {
+            attributes.put(avps[n], avps[n + 1]);
+        }
+        return attributes;
+    }
+
+    protected Map<String, String> getAttributes() {
+        return mAttributes;
+    }
+
+    public OMAConstructed getParent() {
+        return mParent;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getContext() {
+        return mContext;
+    }
+
+    public List<String> getPath() {
+        LinkedList<String> path = new LinkedList<>();
+        for (OMANode node = this; node != null; node = node.getParent()) {
+            path.addFirst(node.getName());
+        }
+        return path;
+    }
+
+    public String getPathString() {
+        StringBuilder sb = new StringBuilder();
+        for (String element : getPath()) {
+            sb.append('/').append(element);
+        }
+        return sb.toString();
+    }
+
+    public abstract OMANode reparent(OMAConstructed parent);
+
+    public abstract String getScalarValue(Iterator<String> path) throws OMAException;
+
+    public abstract OMANode getListValue(Iterator<String> path) throws OMAException;
+
+    public abstract boolean isLeaf();
+
+    public abstract Collection<OMANode> getChildren();
+
+    public abstract OMANode getChild(String name) throws OMAException;
+
+    public abstract String getValue();
+
+    public abstract OMANode addChild(String name, String context, String value, String path)
+            throws IOException;
+
+    public abstract void marshal(OutputStream out, int level) throws IOException;
+
+    public abstract void toString(StringBuilder sb, int level);
+
+    public abstract void fillPayload(StringBuilder sb);
+
+    public void toXml(StringBuilder sb) {
+        sb.append('<').append(MOTree.NodeTag);
+        if (mAttributes != null && !mAttributes.isEmpty()) {
+            for (Map.Entry<String, String> avp : mAttributes.entrySet()) {
+                sb.append(' ').append(avp.getKey()).append("=\"")
+                        .append(avp.getValue()).append('"');
+            }
+        }
+        sb.append(">\n");
+
+        sb.append('<').append(MOTree.NodeNameTag).append('>');
+        sb.append(getName());
+        sb.append("</").append(MOTree.NodeNameTag).append(">\n");
+
+        fillPayload(sb);
+
+        sb.append("</").append(MOTree.NodeTag).append(">\n");
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        toString(sb, 0);
+        return sb.toString();
+    }
+
+    public static OMAConstructed unmarshal(InputStream in) throws IOException {
+        OMANode node = buildNode(in, null);
+        if (node == null || node.isLeaf()) {
+            throw new IOException("Bad OMA tree");
+        }
+        unmarshal(in, (OMAConstructed) node);
+        return (OMAConstructed) node;
+    }
+
+    private static void unmarshal(InputStream in, OMAConstructed parent) throws IOException {
+        for (; ; ) {
+            OMANode node = buildNode(in, parent);
+            if (node == null) {
+                return;
+            } else if (!node.isLeaf()) {
+                unmarshal(in, (OMAConstructed) node);
+            }
+        }
+    }
+
+    private static OMANode buildNode(InputStream in, OMAConstructed parent) throws IOException {
+        String name = OMAConstants.deserializeString(in);
+        if (name == null) {
+            return null;
+        }
+
+        String urn = null;
+        int next = in.read();
+        if (next == '(') {
+            urn = OMAConstants.readURN(in);
+            next = in.read();
+        }
+
+        if (next == '=') {
+            String value = OMAConstants.deserializeString(in);
+            return parent.addChild(name, urn, value, null);
+        } else if (next == '+') {
+            if (parent != null) {
+                return parent.addChild(name, urn, null, null);
+            } else {
+                return new OMAConstructed(null, name, urn);
+            }
+        } else {
+            throw new IOException("Parse error: expected = or + after node name");
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAParser.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAParser.java
new file mode 100644
index 0000000..21cc19a
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAParser.java
@@ -0,0 +1,69 @@
+package com.android.hotspot2.omadm;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Parses an OMA-DM XML tree.
+ */
+public class OMAParser extends DefaultHandler {
+    private XMLNode mRoot;
+    private XMLNode mCurrent;
+
+    public OMAParser() {
+        mRoot = null;
+        mCurrent = null;
+    }
+
+    public MOTree parse(String text, String urn) throws IOException, SAXException {
+        try {
+            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
+            parser.parse(new InputSource(new StringReader(text)), this);
+            return new MOTree(mRoot, urn);
+        } catch (ParserConfigurationException pce) {
+            throw new SAXException(pce);
+        }
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes)
+            throws SAXException {
+        XMLNode parent = mCurrent;
+
+        mCurrent = new XMLNode(mCurrent, qName, attributes);
+
+        if (mRoot == null)
+            mRoot = mCurrent;
+        else
+            parent.addChild(mCurrent);
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        if (!qName.equals(mCurrent.getTag()))
+            throw new SAXException("End tag '" + qName + "' doesn't match current node: " +
+                    mCurrent);
+
+        try {
+            mCurrent.close();
+        } catch (IOException ioe) {
+            throw new SAXException("Failed to close element", ioe);
+        }
+
+        mCurrent = mCurrent.getParent();
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        mCurrent.addText(ch, start, length);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAScalar.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAScalar.java
new file mode 100644
index 0000000..a971ac4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAScalar.java
@@ -0,0 +1,87 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+public class OMAScalar extends OMANode {
+    private final String mValue;
+
+    public OMAScalar(OMAConstructed parent, String name, String context, String value,
+                     String ... avps) {
+        this(parent, name, context, value, buildAttributes(avps));
+    }
+
+    public OMAScalar(OMAConstructed parent, String name, String context, String value,
+                     Map<String, String> avps) {
+        super(parent, name, context, avps);
+        mValue = value;
+    }
+
+    @Override
+    public OMAScalar reparent(OMAConstructed parent) {
+        return new OMAScalar(parent, getName(), getContext(), mValue, getAttributes());
+    }
+
+    public String getScalarValue(Iterator<String> path) throws OMAException {
+        return mValue;
+    }
+
+    @Override
+    public OMANode getListValue(Iterator<String> path) throws OMAException {
+        throw new OMAException("Scalar encountered in list path: " + getPathString());
+    }
+
+    @Override
+    public boolean isLeaf() {
+        return true;
+    }
+
+    @Override
+    public Collection<OMANode> getChildren() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getValue() {
+        return mValue;
+    }
+
+    @Override
+    public OMANode getChild(String name) throws OMAException {
+        throw new OMAException("'" + getName() + "' is a scalar node");
+    }
+
+    @Override
+    public OMANode addChild(String name, String context, String value, String path)
+            throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void toString(StringBuilder sb, int level) {
+        sb.append(getPathString()).append('=').append(mValue);
+        if (getContext() != null) {
+            sb.append(" (").append(getContext()).append(')');
+        }
+        sb.append('\n');
+    }
+
+    @Override
+    public void marshal(OutputStream out, int level) throws IOException {
+        OMAConstants.indent(level, out);
+        OMAConstants.serializeString(getName(), out);
+        out.write((byte) '=');
+        OMAConstants.serializeString(getValue(), out);
+        out.write((byte) '\n');
+    }
+
+    @Override
+    public void fillPayload(StringBuilder sb) {
+        sb.append('<').append(MOTree.ValueTag).append('>');
+        sb.append(mValue);
+        sb.append("</").append(MOTree.ValueTag).append(">\n");
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/XMLNode.java b/packages/Osu/src/com/android/hotspot2/omadm/XMLNode.java
new file mode 100644
index 0000000..b77c820
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/XMLNode.java
@@ -0,0 +1,240 @@
+package com.android.hotspot2.omadm;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class XMLNode {
+    private final String mTag;
+    private final Map<String, NodeAttribute> mAttributes;
+    private final List<XMLNode> mChildren;
+    private final XMLNode mParent;
+    private MOTree mMO;
+    private StringBuilder mTextBuilder;
+    private String mText;
+
+    private static final String XML_SPECIAL_CHARS = "\"'<>&";
+    private static final Set<Character> XML_SPECIAL = new HashSet<>();
+    private static final String CDATA_OPEN = "<![CDATA[";
+    private static final String CDATA_CLOSE = "]]>";
+
+    static {
+        for (int n = 0; n < XML_SPECIAL_CHARS.length(); n++) {
+            XML_SPECIAL.add(XML_SPECIAL_CHARS.charAt(n));
+        }
+    }
+
+    public XMLNode(XMLNode parent, String tag, Attributes attributes) throws SAXException {
+        mTag = tag;
+
+        mAttributes = new HashMap<>();
+
+        if (attributes.getLength() > 0) {
+            for (int n = 0; n < attributes.getLength(); n++)
+                mAttributes.put(attributes.getQName(n), new NodeAttribute(attributes.getQName(n),
+                        attributes.getType(n), attributes.getValue(n)));
+        }
+
+        mParent = parent;
+        mChildren = new ArrayList<>();
+
+        mTextBuilder = new StringBuilder();
+    }
+
+    public XMLNode(XMLNode parent, String tag, Map<String, String> attributes) {
+        mTag = tag;
+
+        mAttributes = new HashMap<>(attributes == null ? 0 : attributes.size());
+
+        if (attributes != null) {
+            for (Map.Entry<String, String> entry : attributes.entrySet()) {
+                mAttributes.put(entry.getKey(),
+                        new NodeAttribute(entry.getKey(), "", entry.getValue()));
+            }
+        }
+
+        mParent = parent;
+        mChildren = new ArrayList<>();
+
+        mTextBuilder = new StringBuilder();
+    }
+
+    public void setText(String text) {
+        mText = text;
+        mTextBuilder = null;
+    }
+
+    public void addText(char[] chs, int start, int length) {
+        String s = new String(chs, start, length);
+        String trimmed = s.trim();
+        if (trimmed.isEmpty())
+            return;
+
+        if (s.charAt(0) != trimmed.charAt(0))
+            mTextBuilder.append(' ');
+        mTextBuilder.append(trimmed);
+        if (s.charAt(s.length() - 1) != trimmed.charAt(trimmed.length() - 1))
+            mTextBuilder.append(' ');
+    }
+
+    public void addChild(XMLNode child) {
+        mChildren.add(child);
+    }
+
+    public void close() throws IOException, SAXException {
+        String text = mTextBuilder.toString().trim();
+        StringBuilder filtered = new StringBuilder(text.length());
+        for (int n = 0; n < text.length(); n++) {
+            char ch = text.charAt(n);
+            if (ch >= ' ')
+                filtered.append(ch);
+        }
+
+        mText = filtered.toString();
+        mTextBuilder = null;
+
+        if (MOTree.hasMgmtTreeTag(mText)) {
+            try {
+                NodeAttribute urn = mAttributes.get(OMAConstants.SppMOAttribute);
+                OMAParser omaParser = new OMAParser();
+                mMO = omaParser.parse(mText, urn != null ? urn.getValue() : null);
+            } catch (SAXException | IOException e) {
+                mMO = null;
+            }
+        }
+    }
+
+    public String getTag() {
+        return mTag;
+    }
+
+    public String getNameSpace() throws OMAException {
+        String[] nsn = mTag.split(":");
+        if (nsn.length != 2) {
+            throw new OMAException("Non-namespaced tag: '" + mTag + "'");
+        }
+        return nsn[0];
+    }
+
+    public String getStrippedTag() throws OMAException {
+        String[] nsn = mTag.split(":");
+        if (nsn.length != 2) {
+            throw new OMAException("Non-namespaced tag: '" + mTag + "'");
+        }
+        return nsn[1].toLowerCase();
+    }
+
+    public XMLNode getSoleChild() throws OMAException {
+        if (mChildren.size() != 1) {
+            throw new OMAException("Expected exactly one child to " + mTag);
+        }
+        return mChildren.get(0);
+    }
+
+    public XMLNode getParent() {
+        return mParent;
+    }
+
+    public String getText() {
+        return mText;
+    }
+
+    public Map<String, NodeAttribute> getAttributes() {
+        return Collections.unmodifiableMap(mAttributes);
+    }
+
+    public Map<String, String> getTextualAttributes() {
+        Map<String, String> map = new HashMap<>(mAttributes.size());
+        for (Map.Entry<String, NodeAttribute> entry : mAttributes.entrySet()) {
+            map.put(entry.getKey(), entry.getValue().getValue());
+        }
+        return map;
+    }
+
+    public String getAttributeValue(String name) {
+        NodeAttribute nodeAttribute = mAttributes.get(name);
+        return nodeAttribute != null ? nodeAttribute.getValue() : null;
+    }
+
+    public List<XMLNode> getChildren() {
+        return mChildren;
+    }
+
+    public MOTree getMOTree() {
+        return mMO;
+    }
+
+    private void toString(char[] indent, StringBuilder sb) {
+        Arrays.fill(indent, ' ');
+
+        sb.append(indent).append('<').append(mTag);
+        for (Map.Entry<String, NodeAttribute> entry : mAttributes.entrySet()) {
+            sb.append(' ').append(entry.getKey()).append("='")
+                    .append(entry.getValue().getValue()).append('\'');
+        }
+
+        if (mText != null && !mText.isEmpty()) {
+            sb.append('>').append(escapeCdata(mText)).append("</").append(mTag).append(">\n");
+        } else if (mChildren.isEmpty()) {
+            sb.append("/>\n");
+        } else {
+            sb.append(">\n");
+            char[] subIndent = Arrays.copyOf(indent, indent.length + 2);
+            for (XMLNode child : mChildren) {
+                child.toString(subIndent, sb);
+            }
+            sb.append(indent).append("</").append(mTag).append(">\n");
+        }
+    }
+
+    private static String escapeCdata(String text) {
+        if (!escapable(text)) {
+            return text;
+        }
+
+        // Any appearance of ]]> in the text must be split into "]]" | "]]>" | <![CDATA[ | ">"
+        // i.e. "split the sequence by putting a close CDATA and a new open CDATA before the '>'
+        StringBuilder sb = new StringBuilder();
+        sb.append(CDATA_OPEN);
+        int start = 0;
+        for (; ; ) {
+            int etoken = text.indexOf(CDATA_CLOSE);
+            if (etoken >= 0) {
+                sb.append(text.substring(start, etoken + 2)).append(CDATA_CLOSE).append(CDATA_OPEN);
+                start = etoken + 2;
+            } else {
+                if (start < text.length() - 1) {
+                    sb.append(text.substring(start));
+                }
+                break;
+            }
+        }
+        sb.append(CDATA_CLOSE);
+        return sb.toString();
+    }
+
+    private static boolean escapable(String s) {
+        for (int n = 0; n < s.length(); n++) {
+            if (XML_SPECIAL.contains(s.charAt(n))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        toString(new char[0], sb);
+        return sb.toString();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java b/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java
new file mode 100644
index 0000000..f5d06d5
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java
@@ -0,0 +1,127 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.hotspot2.pps.HomeSP;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.X509KeyManager;
+
+public class ClientKeyManager implements X509KeyManager {
+    private final KeyStore mKeyStore;
+    private final Map<OSUCertType, String> mAliasMap;
+    private final Map<OSUCertType, Object> mTempKeys;
+
+    private static final String sTempAlias = "client-alias";
+
+    public ClientKeyManager(HomeSP homeSP, KeyStore keyStore) throws IOException {
+        mKeyStore = keyStore;
+        mAliasMap = new HashMap<>();
+        mAliasMap.put(OSUCertType.AAA, OSUManager.CERT_CLT_CA_ALIAS + homeSP.getFQDN());
+        mAliasMap.put(OSUCertType.Client, OSUManager.CERT_CLT_CERT_ALIAS + homeSP.getFQDN());
+        mAliasMap.put(OSUCertType.PrivateKey, OSUManager.CERT_CLT_KEY_ALIAS + homeSP.getFQDN());
+        mTempKeys = new HashMap<>();
+    }
+
+    public void reloadKeys(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
+            throws IOException {
+        List<X509Certificate> clientCerts = certs.get(OSUCertType.Client);
+        X509Certificate[] certArray = new X509Certificate[clientCerts.size()];
+        int n = 0;
+        for (X509Certificate cert : clientCerts) {
+            certArray[n++] = cert;
+        }
+        mTempKeys.put(OSUCertType.Client, certArray);
+        mTempKeys.put(OSUCertType.PrivateKey, key);
+    }
+
+    @Override
+    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+        if (mTempKeys.isEmpty()) {
+            return mAliasMap.get(OSUCertType.Client);
+        } else {
+            return sTempAlias;
+        }
+    }
+
+    @Override
+    public String[] getClientAliases(String keyType, Principal[] issuers) {
+        if (mTempKeys.isEmpty()) {
+            String alias = mAliasMap.get(OSUCertType.Client);
+            return alias != null ? new String[]{alias} : null;
+        } else {
+            return new String[]{sTempAlias};
+        }
+    }
+
+    @Override
+    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getServerAliases(String keyType, Principal[] issuers) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain(String alias) {
+        if (mTempKeys.isEmpty()) {
+            if (!mAliasMap.get(OSUCertType.Client).equals(alias)) {
+                Log.w(OSUManager.TAG, "Bad cert alias requested: '" + alias + "'");
+                return null;
+            }
+            try {
+                List<X509Certificate> certs = new ArrayList<>();
+                for (Certificate certificate :
+                        mKeyStore.getCertificateChain(mAliasMap.get(OSUCertType.Client))) {
+                    if (certificate instanceof X509Certificate) {
+                        certs.add((X509Certificate) certificate);
+                    }
+                }
+                return certs.toArray(new X509Certificate[certs.size()]);
+            } catch (KeyStoreException kse) {
+                Log.w(OSUManager.TAG, "Failed to retrieve certificates: " + kse);
+                return null;
+            }
+        } else if (sTempAlias.equals(alias)) {
+            return (X509Certificate[]) mTempKeys.get(OSUCertType.Client);
+        } else {
+            Log.w(OSUManager.TAG, "Bad cert alias requested: '" + alias + "'");
+            return null;
+        }
+    }
+
+    @Override
+    public PrivateKey getPrivateKey(String alias) {
+        if (mTempKeys.isEmpty()) {
+            if (!mAliasMap.get(OSUCertType.Client).equals(alias)) {
+                Log.w(OSUManager.TAG, "Bad key alias requested: '" + alias + "'");
+            }
+            try {
+                return (PrivateKey) mKeyStore.getKey(mAliasMap.get(OSUCertType.PrivateKey), null);
+            } catch (GeneralSecurityException gse) {
+                Log.w(OSUManager.TAG, "Failed to retrieve private key: " + gse);
+                return null;
+            }
+        } else if (sTempAlias.equals(alias)) {
+            return (PrivateKey) mTempKeys.get(OSUCertType.PrivateKey);
+        } else {
+            Log.w(OSUManager.TAG, "Bad cert alias requested: '" + alias + "'");
+            return null;
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ExchangeCompleteResponse.java b/packages/Osu/src/com/android/hotspot2/osu/ExchangeCompleteResponse.java
new file mode 100644
index 0000000..fe23b5c
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ExchangeCompleteResponse.java
@@ -0,0 +1,28 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+	/*
+	<xsd:element name="sppExchangeComplete">
+		<xsd:annotation>
+			<xsd:documentation>SOAP method used by SPP server to end session.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element ref="sppError" minOccurs="0"/>
+				<xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+			</xsd:sequence>
+			<xsd:attribute ref="sppVersion" use="required"/>
+			<xsd:attribute ref="sppStatus" use="required"/>
+			<xsd:attribute ref="sessionID" use="required"/>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+	 */
+
+public class ExchangeCompleteResponse extends OSUResponse {
+    public ExchangeCompleteResponse(XMLNode root) throws OMAException {
+        super(root, OSUMessageType.ExchangeComplete);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ExecCommand.java b/packages/Osu/src/com/android/hotspot2/osu/ExecCommand.java
new file mode 100644
index 0000000..38a3947
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ExecCommand.java
@@ -0,0 +1,3 @@
+package com.android.hotspot2.osu;
+
+public enum ExecCommand {Browser, GetCert, UseClientCertTLS, UploadMO}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java b/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java
new file mode 100644
index 0000000..1a66fcf
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java
@@ -0,0 +1,178 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.hotspot2.utils.HTTPMessage;
+import com.android.hotspot2.utils.HTTPRequest;
+import com.android.hotspot2.utils.HTTPResponse;
+
+import com.android.org.conscrypt.OpenSSLSocketImpl;
+
+import org.xml.sax.SAXException;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
+import javax.xml.parsers.ParserConfigurationException;
+
+public class HTTPHandler implements AutoCloseable {
+    private final Charset mCharset;
+    private final OSUSocketFactory mSocketFactory;
+    private Socket mSocket;
+    private BufferedOutputStream mOut;
+    private BufferedInputStream mIn;
+    private final String mUser;
+    private final byte[] mPassword;
+    private boolean mHTTPAuthPerformed;
+    private static final AtomicInteger sSequence = new AtomicInteger();
+
+    public HTTPHandler(Charset charset, OSUSocketFactory socketFactory) throws IOException {
+        this(charset, socketFactory, null, null);
+    }
+
+    public HTTPHandler(Charset charset, OSUSocketFactory socketFactory,
+                       String user, byte[] password) throws IOException {
+        mCharset = charset;
+        mSocketFactory = socketFactory;
+        mSocket = mSocketFactory.createSocket();
+        mOut = new BufferedOutputStream(mSocket.getOutputStream());
+        mIn = new BufferedInputStream(mSocket.getInputStream());
+        mUser = user;
+        mPassword = password;
+    }
+
+    public boolean isHTTPAuthPerformed() {
+        return mHTTPAuthPerformed;
+    }
+
+    public X509Certificate getOSUCertificate(URL osu) throws GeneralSecurityException {
+        return mSocketFactory.getOSUCertificate(osu);
+    }
+
+    public void renegotiate(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
+            throws IOException {
+        if (!(mSocket instanceof SSLSocket)) {
+            throw new IOException("Not a TLS connection");
+        }
+        if (certs != null) {
+            mSocketFactory.reloadKeys(certs, key);
+        }
+        ((SSLSocket) mSocket).startHandshake();
+    }
+
+    public byte[] getTLSUnique() throws SSLException {
+        if (mSocket instanceof OpenSSLSocketImpl) {
+            return ((OpenSSLSocketImpl) mSocket).getChannelId();
+        }
+        return null;
+    }
+
+    public OSUResponse exchangeSOAP(URL url, String message) throws IOException {
+        HTTPResponse response = exchangeWithRetry(url, message, HTTPMessage.Method.POST,
+                HTTPMessage.ContentTypeSOAP);
+        if (response.getStatusCode() >= 300) {
+            throw new IOException("Bad HTTP status code " + response.getStatusCode());
+        }
+        try {
+            SOAPParser parser = new SOAPParser(response.getPayloadStream());
+            return parser.getResponse();
+        } catch (ParserConfigurationException | SAXException e) {
+            ByteBuffer x = response.getPayload();
+            byte[] b = new byte[x.remaining()];
+            x.get(b);
+            Log.w("XML", "Bad: '" + new String(b, StandardCharsets.ISO_8859_1));
+            throw new IOException(e);
+        }
+    }
+
+    public ByteBuffer exchangeBinary(URL url, String message, String contentType)
+            throws IOException {
+        HTTPResponse response =
+                exchangeWithRetry(url, message, HTTPMessage.Method.POST, contentType);
+        return response.getBinaryPayload();
+    }
+
+    public InputStream doGet(URL url) throws IOException {
+        HTTPResponse response = exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
+        return response.getPayloadStream();
+    }
+
+    public HTTPResponse doGetHTTP(URL url) throws IOException {
+        return exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
+    }
+
+    private HTTPResponse exchangeWithRetry(URL url, String message, HTTPMessage.Method method,
+                                           String contentType) throws IOException {
+        HTTPResponse response = null;
+        int retry = 0;
+        for (; ; ) {
+            try {
+                response = httpExchange(url, message, method, contentType);
+                break;
+            } catch (IOException ioe) {
+                close();
+                retry++;
+                if (retry > 3) {
+                    break;
+                }
+                Log.d(OSUManager.TAG, "Failed HTTP exchange, retry " + retry);
+                mSocket = mSocketFactory.createSocket();
+                mOut = new BufferedOutputStream(mSocket.getOutputStream());
+                mIn = new BufferedInputStream(mSocket.getInputStream());
+            }
+        }
+        if (response == null) {
+            throw new IOException("Failed to establish connection to peer");
+        }
+        return response;
+    }
+
+    private HTTPResponse httpExchange(URL url, String message, HTTPMessage.Method method,
+                                      String contentType)
+            throws IOException {
+        HTTPRequest request = new HTTPRequest(message, mCharset, method, url, contentType, false);
+        request.send(mOut);
+        HTTPResponse response = new HTTPResponse(mIn);
+        Log.d(OSUManager.TAG, "HTTP code " + response.getStatusCode() + ", user " + mUser +
+                ", pw " + (mPassword != null ? '\'' + new String(mPassword) + '\'' : "-"));
+        if (response.getStatusCode() == 401) {
+            if (mUser == null) {
+                throw new IOException("Missing user name for HTTP authentication");
+            }
+            try {
+                request = new HTTPRequest(message, StandardCharsets.ISO_8859_1, method, url,
+                        contentType, true);
+                request.doAuthenticate(response, mUser, mPassword, url,
+                        sSequence.incrementAndGet());
+                request.send(mOut);
+                mHTTPAuthPerformed = true;
+            } catch (GeneralSecurityException gse) {
+                throw new IOException(gse);
+            }
+
+            response = new HTTPResponse(mIn);
+        }
+        return response;
+    }
+
+    public void close() throws IOException {
+        mIn.close();
+        mOut.close();
+        mSocket.close();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/IconCache.java b/packages/Osu/src/com/android/hotspot2/osu/IconCache.java
new file mode 100644
index 0000000..b1580ac
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/IconCache.java
@@ -0,0 +1,392 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.anqp.HSIconFileElement;
+import com.android.anqp.IconInfo;
+import com.android.hotspot2.Utils;
+
+import java.net.ProtocolException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static com.android.anqp.Constants.ANQPElementType.HSIconFile;
+
+public class IconCache extends Thread {
+    private static final int CacheSize = 64;
+    private static final int RetryCount = 3;
+
+    private final OSUManager mOSUManager;
+    private final Map<Long, LinkedList<QuerySet>> mBssQueues = new HashMap<>();
+
+    private final Map<IconKey, HSIconFileElement> mCache =
+            new LinkedHashMap<IconKey, HSIconFileElement>() {
+                @Override
+                protected boolean removeEldestEntry(Map.Entry eldest) {
+                    return size() > CacheSize;
+                }
+            };
+
+    private static class IconKey {
+        private final long mBSSID;
+        private final long mHESSID;
+        private final String mSSID;
+        private final int mAnqpDomID;
+        private final String mFileName;
+
+        private IconKey(OSUInfo osuInfo, String fileName) {
+            mBSSID = osuInfo.getBSSID();
+            mHESSID = osuInfo.getHESSID();
+            mSSID = osuInfo.getAdvertisingSSID();
+            mAnqpDomID = osuInfo.getAnqpDomID();
+            mFileName = fileName;
+        }
+
+        public String getFileName() {
+            return mFileName;
+        }
+
+        @Override
+        public boolean equals(Object thatObject) {
+            if (this == thatObject) {
+                return true;
+            }
+            if (thatObject == null || getClass() != thatObject.getClass()) {
+                return false;
+            }
+
+            IconKey that = (IconKey) thatObject;
+
+            return mFileName.equals(that.mFileName) && ((mBSSID == that.mBSSID) ||
+                    ((mAnqpDomID == that.mAnqpDomID) && (mAnqpDomID != 0) &&
+                            (mHESSID == that.mHESSID) && ((mHESSID != 0)
+                            || mSSID.equals(that.mSSID))));
+        }
+
+        @Override
+        public int hashCode() {
+            int result = (int) (mBSSID ^ (mBSSID >>> 32));
+            result = 31 * result + (int) (mHESSID ^ (mHESSID >>> 32));
+            result = 31 * result + mSSID.hashCode();
+            result = 31 * result + mAnqpDomID;
+            result = 31 * result + mFileName.hashCode();
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%012x:%012x '%s' [%d] + '%s'",
+                    mBSSID, mHESSID, mSSID, mAnqpDomID, mFileName);
+        }
+    }
+
+    private static class QueryEntry {
+        private final IconKey mKey;
+        private int mRetry;
+        private long mLastSent;
+
+        private QueryEntry(IconKey key) {
+            mKey = key;
+            mLastSent = System.currentTimeMillis();
+        }
+
+        private IconKey getKey() {
+            return mKey;
+        }
+
+        private int bumpRetry() {
+            mLastSent = System.currentTimeMillis();
+            return mRetry++;
+        }
+
+        private long age(long now) {
+            return now - mLastSent;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Entry %s, retry %d", mKey, mRetry);
+        }
+    }
+
+    private static class QuerySet {
+        private final OSUInfo mOsuInfo;
+        private final LinkedList<QueryEntry> mEntries;
+
+        private QuerySet(OSUInfo osuInfo, List<IconInfo> icons) {
+            mOsuInfo = osuInfo;
+            mEntries = new LinkedList<>();
+            for (IconInfo iconInfo : icons) {
+                mEntries.addLast(new QueryEntry(new IconKey(osuInfo, iconInfo.getFileName())));
+            }
+        }
+
+        private QueryEntry peek() {
+            return mEntries.getFirst();
+        }
+
+        private QueryEntry pop() {
+            mEntries.removeFirst();
+            return mEntries.isEmpty() ? null : mEntries.getFirst();
+        }
+
+        private boolean isEmpty() {
+            return mEntries.isEmpty();
+        }
+
+        private List<QueryEntry> getAllEntries() {
+            return Collections.unmodifiableList(mEntries);
+        }
+
+        private long getBssid() {
+            return mOsuInfo.getBSSID();
+        }
+
+        private OSUInfo getOsuInfo() {
+            return mOsuInfo;
+        }
+
+        private IconKey updateIcon(String fileName, HSIconFileElement iconFileElement) {
+            IconKey key = null;
+            for (QueryEntry queryEntry : mEntries) {
+                if (queryEntry.getKey().getFileName().equals(fileName)) {
+                    key = queryEntry.getKey();
+                }
+            }
+            if (key == null) {
+                return null;
+            }
+
+            if (iconFileElement != null) {
+                mOsuInfo.setIconFileElement(iconFileElement, fileName);
+            } else {
+                mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable);
+            }
+            return key;
+        }
+
+        private boolean updateIcon(IconKey key, HSIconFileElement iconFileElement) {
+            boolean match = false;
+            for (QueryEntry queryEntry : mEntries) {
+                if (queryEntry.getKey().equals(key)) {
+                    match = true;
+                    break;
+                }
+            }
+            if (!match) {
+                return false;
+            }
+
+            if (iconFileElement != null) {
+                mOsuInfo.setIconFileElement(iconFileElement, key.getFileName());
+            } else {
+                mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable);
+            }
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "OSU " + mOsuInfo + ": " + mEntries;
+        }
+    }
+
+    public IconCache(OSUManager osuManager) {
+        mOSUManager = osuManager;
+    }
+
+    public void clear() {
+        mBssQueues.clear();
+        mCache.clear();
+    }
+
+    private boolean enqueue(QuerySet querySet) {
+        boolean newEntry = false;
+        LinkedList<QuerySet> queries = mBssQueues.get(querySet.getBssid());
+        if (queries == null) {
+            queries = new LinkedList<>();
+            mBssQueues.put(querySet.getBssid(), queries);
+            newEntry = true;
+        }
+        queries.addLast(querySet);
+        return newEntry;
+    }
+
+    public void startIconQuery(OSUInfo osuInfo, List<IconInfo> icons) {
+        Log.d("ZXZ", String.format("Icon query on %012x for %s", osuInfo.getBSSID(), icons));
+        if (icons == null || icons.isEmpty()) {
+            return;
+        }
+
+        QuerySet querySet = new QuerySet(osuInfo, icons);
+        for (QueryEntry entry : querySet.getAllEntries()) {
+            HSIconFileElement iconElement = mCache.get(entry.getKey());
+            if (iconElement != null) {
+                osuInfo.setIconFileElement(iconElement, entry.getKey().getFileName());
+                mOSUManager.iconResults(Arrays.asList(osuInfo));
+                return;
+            }
+        }
+        if (enqueue(querySet)) {
+            initiateQuery(querySet.getBssid());
+        }
+    }
+
+    private void initiateQuery(long bssid) {
+        LinkedList<QuerySet> queryEntries = mBssQueues.get(bssid);
+        if (queryEntries == null) {
+            return;
+        } else if (queryEntries.isEmpty()) {
+            mBssQueues.remove(bssid);
+            return;
+        }
+
+        QuerySet querySet = queryEntries.getFirst();
+        QueryEntry queryEntry = querySet.peek();
+        if (queryEntry.bumpRetry() >= RetryCount) {
+            QueryEntry newEntry = querySet.pop();
+            if (newEntry == null) {
+                // No more entries in this QuerySet, advance to the next set.
+                querySet.getOsuInfo().setIconStatus(OSUInfo.IconStatus.NotAvailable);
+                queryEntries.removeFirst();
+                if (queryEntries.isEmpty()) {
+                    // No further QuerySet on this BSSID, drop the bucket and bail.
+                    mBssQueues.remove(bssid);
+                    return;
+                } else {
+                    querySet = queryEntries.getFirst();
+                    queryEntry = querySet.peek();
+                    queryEntry.bumpRetry();
+                }
+            }
+        }
+        mOSUManager.doIconQuery(bssid, queryEntry.getKey().getFileName());
+    }
+
+    public void notifyIconReceived(long bssid, String fileName, byte[] iconData) {
+        Log.d("ZXZ", String.format("Icon '%s':%d received from %012x",
+                fileName, iconData != null ? iconData.length : -1, bssid));
+        IconKey key;
+        HSIconFileElement iconFileElement = null;
+        List<OSUInfo> updates = new ArrayList<>();
+
+        LinkedList<QuerySet> querySets = mBssQueues.get(bssid);
+        if (querySets == null || querySets.isEmpty()) {
+            Log.d(OSUManager.TAG,
+                    String.format("Spurious icon response from %012x for '%s' (%d) bytes",
+                            bssid, fileName, iconData != null ? iconData.length : -1));
+            Log.d("ZXZ", "query set: " + querySets
+                    + ", BSS queues: " + Utils.bssidsToString(mBssQueues.keySet()));
+            return;
+        } else {
+            QuerySet querySet = querySets.removeFirst();
+            if (iconData != null) {
+                try {
+                    iconFileElement = new HSIconFileElement(HSIconFile,
+                            ByteBuffer.wrap(iconData).order(ByteOrder.LITTLE_ENDIAN));
+                } catch (ProtocolException | BufferUnderflowException e) {
+                    Log.e(OSUManager.TAG, "Failed to parse ANQP icon file: " + e);
+                }
+            }
+            key = querySet.updateIcon(fileName, iconFileElement);
+            if (key == null) {
+                Log.d(OSUManager.TAG,
+                        String.format("Spurious icon response from %012x for '%s' (%d) bytes",
+                                bssid, fileName, iconData != null ? iconData.length : -1));
+                Log.d("ZXZ", "query set: " + querySets + ", BSS queues: "
+                        + Utils.bssidsToString(mBssQueues.keySet()));
+                querySets.addFirst(querySet);
+                return;
+            }
+
+            if (iconFileElement != null) {
+                mCache.put(key, iconFileElement);
+            }
+
+            if (querySet.isEmpty()) {
+                mBssQueues.remove(bssid);
+            }
+            updates.add(querySet.getOsuInfo());
+        }
+
+        // Update any other pending entries that matches the ESS of the currently resolved icon
+        Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator =
+                mBssQueues.entrySet().iterator();
+        while (bssIterator.hasNext()) {
+            Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next();
+            Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator();
+            while (querySetIterator.hasNext()) {
+                QuerySet querySet = querySetIterator.next();
+                if (querySet.updateIcon(key, iconFileElement)) {
+                    querySetIterator.remove();
+                    updates.add(querySet.getOsuInfo());
+                }
+            }
+            if (bssEntries.getValue().isEmpty()) {
+                bssIterator.remove();
+            }
+        }
+
+        initiateQuery(bssid);
+
+        mOSUManager.iconResults(updates);
+    }
+
+    private static final long RequeryTimeLow = 6000L;
+    private static final long RequeryTimeHigh = 15000L;
+
+    public void tickle(boolean wifiOff) {
+        synchronized (mCache) {
+            if (wifiOff) {
+                mBssQueues.clear();
+            } else {
+                long now = System.currentTimeMillis();
+
+                Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator =
+                        mBssQueues.entrySet().iterator();
+                while (bssIterator.hasNext()) {
+                    // Get the list of entries for this BSSID
+                    Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next();
+                    Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator();
+                    while (querySetIterator.hasNext()) {
+                        QuerySet querySet = querySetIterator.next();
+                        QueryEntry queryEntry = querySet.peek();
+                        long age = queryEntry.age(now);
+                        if (age > RequeryTimeHigh) {
+                            // Timed out entry, move on to the next.
+                            queryEntry = querySet.pop();
+                            if (queryEntry == null) {
+                                // Empty query set, update status and remove it.
+                                querySet.getOsuInfo()
+                                        .setIconStatus(OSUInfo.IconStatus.NotAvailable);
+                                querySetIterator.remove();
+                            } else {
+                                // Start a query on the next entry and bail out of the set iteration
+                                initiateQuery(querySet.getBssid());
+                                break;
+                            }
+                        } else if (age > RequeryTimeLow) {
+                            // Re-issue queries for qualified entries and bail out of set iteration
+                            initiateQuery(querySet.getBssid());
+                            break;
+                        }
+                    }
+                    if (bssEntries.getValue().isEmpty()) {
+                        // Kill the whole bucket if the set list is empty
+                        bssIterator.remove();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java
new file mode 100644
index 0000000..dadda26
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java
@@ -0,0 +1,172 @@
+package com.android.hotspot2.osu;
+
+import android.net.wifi.AnqpInformationElement;
+import android.net.wifi.ScanResult;
+import android.util.Log;
+
+import com.android.anqp.Constants;
+import com.android.anqp.HSOsuProvidersElement;
+import com.android.anqp.OSUProvider;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class holds a stable set of OSU information as well as scan results based on a trail of
+ * scan results.
+ * The purpose of this class is to provide a stable set of information over a a limited span of
+ * time (SCAN_BATCH_HISTORY_SIZE scan batches) so that OSU entries in the selection list does not
+ * come and go with temporarily lost scan results.
+ * The stable set of scan results are used by the remediation flow to retrieve ANQP information
+ * for the current network to determine whether the currently associated network is a roaming
+ * network for the Home SP whose timer has currently fired.
+ */
+public class OSUCache {
+    private static final int SCAN_BATCH_HISTORY_SIZE = 8;
+
+    private int mInstant;
+    private final Map<OSUProvider, ScanResult> mBatchedOSUs = new HashMap<>();
+    private final Map<OSUProvider, ScanInstance> mCache = new HashMap<>();
+
+    private static class ScanInstance {
+        private final ScanResult mScanResult;
+        private int mInstant;
+
+        private ScanInstance(ScanResult scanResult, int instant) {
+            mScanResult = scanResult;
+            mInstant = instant;
+        }
+
+        public ScanResult getScanResult() {
+            return mScanResult;
+        }
+
+        public int getInstant() {
+            return mInstant;
+        }
+
+        private boolean bssidEqual(ScanResult scanResult) {
+            return mScanResult.BSSID.equals(scanResult.BSSID);
+        }
+
+        private void updateInstant(int newInstant) {
+            mInstant = newInstant;
+        }
+
+        @Override
+        public String toString() {
+            return mScanResult.SSID + " @ " + mInstant;
+        }
+    }
+
+    public OSUCache() {
+        mInstant = 0;
+    }
+
+    private void clear() {
+        mBatchedOSUs.clear();
+    }
+
+    public void clearAll() {
+        clear();
+        mCache.clear();
+    }
+
+    public Map<OSUProvider, ScanResult> pushScanResults(Collection<ScanResult> scanResults) {
+        for (ScanResult scanResult : scanResults) {
+            AnqpInformationElement[] osuInfo = scanResult.anqpElements;
+            if (osuInfo != null && osuInfo.length > 0) {
+                putResult(scanResult, osuInfo);
+            }
+        }
+        return scanEnd();
+    }
+
+    private void putResult(ScanResult scanResult, AnqpInformationElement[] elements) {
+        for (AnqpInformationElement ie : elements) {
+            if (ie.getElementId() == AnqpInformationElement.HS_OSU_PROVIDERS
+                    && ie.getVendorId() == AnqpInformationElement.HOTSPOT20_VENDOR_ID) {
+                try {
+                    HSOsuProvidersElement providers = new HSOsuProvidersElement(
+                            Constants.ANQPElementType.HSOSUProviders,
+                            ByteBuffer.wrap(ie.getPayload()).order(ByteOrder.LITTLE_ENDIAN));
+
+                    putProviders(scanResult, providers);
+                } catch (ProtocolException pe) {
+                    Log.w(OSUManager.TAG,
+                            "Failed to parse OSU element: " + pe);
+                }
+            }
+        }
+    }
+
+    private void putProviders(ScanResult scanResult, HSOsuProvidersElement osuProviders) {
+        for (OSUProvider provider : osuProviders.getProviders()) {
+            // Make a predictive put
+            ScanResult existing = mBatchedOSUs.put(provider, scanResult);
+            if (existing != null && existing.level > scanResult.level) {
+                // But undo it if the entry already held a better RSSI
+                mBatchedOSUs.put(provider, existing);
+            }
+        }
+    }
+
+    private Map<OSUProvider, ScanResult> scanEnd() {
+        // Update the trail of OSU Providers:
+        int changes = 0;
+        Map<OSUProvider, ScanInstance> aged = new HashMap<>(mCache);
+        for (Map.Entry<OSUProvider, ScanResult> entry : mBatchedOSUs.entrySet()) {
+            ScanInstance current = aged.remove(entry.getKey());
+            if (current == null || !current.bssidEqual(entry.getValue())) {
+                mCache.put(entry.getKey(), new ScanInstance(entry.getValue(), mInstant));
+                changes++;
+                if (current == null) {
+                    Log.d("ZXZ", "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID);
+                } else {
+                    Log.d("ZXZ", "Update OSU " + entry.getKey() + " with " +
+                            entry.getValue().SSID + " to " + current);
+                }
+            } else {
+                Log.d("ZXZ", "Existing OSU " + entry.getKey() + ", "
+                        + current.getInstant() + " -> " + mInstant);
+                current.updateInstant(mInstant);
+            }
+        }
+
+        for (Map.Entry<OSUProvider, ScanInstance> entry : aged.entrySet()) {
+            if (mInstant - entry.getValue().getInstant() > SCAN_BATCH_HISTORY_SIZE) {
+                Log.d("ZXZ", "Remove OSU " + entry.getKey() + ", "
+                        + entry.getValue().getInstant() + " @ " + mInstant);
+                mCache.remove(entry.getKey());
+                changes++;
+            }
+        }
+
+        mInstant++;
+        clear();
+
+        // Return the latest results if there were any changes from last batch
+        if (changes > 0) {
+            Map<OSUProvider, ScanResult> results = new HashMap<>(mCache.size());
+            for (Map.Entry<OSUProvider, ScanInstance> entry : mCache.entrySet()) {
+                results.put(entry.getKey(), entry.getValue().getScanResult());
+            }
+            return results;
+        } else {
+            return null;
+        }
+    }
+
+    private static String toBSSIDStrings(Set<Long> bssids) {
+        StringBuilder sb = new StringBuilder();
+        for (Long bssid : bssids) {
+            sb.append(String.format(" %012x", bssid));
+        }
+        return sb.toString();
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCertType.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCertType.java
new file mode 100644
index 0000000..91d7f72
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCertType.java
@@ -0,0 +1,10 @@
+package com.android.hotspot2.osu;
+
+public enum OSUCertType {
+    CA,
+    Client,
+    AAA,
+    Remediation,
+    Policy,
+    PrivateKey
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java b/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java
new file mode 100644
index 0000000..12dffe3
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java
@@ -0,0 +1,485 @@
+package com.android.hotspot2.osu;
+
+/*
+ * policy-server.r2-testbed             IN      A       10.123.107.107
+ * remediation-server.r2-testbed        IN      A       10.123.107.107
+ * subscription-server.r2-testbed       IN      A       10.123.107.107
+ * www.r2-testbed                       IN      A       10.123.107.107
+ * osu-server.r2-testbed-rks            IN      A       10.123.107.107
+ * policy-server.r2-testbed-rks         IN      A       10.123.107.107
+ * remediation-server.r2-testbed-rks    IN      A       10.123.107.107
+ * subscription-server.r2-testbed-rks   IN      A       10.123.107.107
+ */
+
+import android.net.Network;
+import android.util.Log;
+
+import com.android.hotspot2.OMADMAdapter;
+import com.android.hotspot2.est.ESTHandler;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMANode;
+import com.android.hotspot2.osu.commands.BrowserURI;
+import com.android.hotspot2.osu.commands.ClientCertInfo;
+import com.android.hotspot2.osu.commands.GetCertData;
+import com.android.hotspot2.osu.commands.MOData;
+import com.android.hotspot2.pps.Credential;
+import com.android.hotspot2.pps.HomeSP;
+import com.android.hotspot2.pps.UpdateInfo;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.net.ssl.KeyManager;
+
+public class OSUClient {
+    private static final String TAG = "OSUCLT";
+    private static final String TTLS_OSU =
+            "https://osu-server.r2-testbed-rks.wi-fi.org:9447/OnlineSignup/services/newUser/digest";
+    private static final String TLS_OSU =
+            "https://osu-server.r2-testbed-rks.wi-fi.org:9446/OnlineSignup/services/newUser/certificate";
+
+    private final OSUInfo mOSUInfo;
+    private final URL mURL;
+    private final KeyStore mKeyStore;
+
+    public OSUClient(OSUInfo osuInfo, KeyStore ks) throws MalformedURLException {
+        mOSUInfo = osuInfo;
+        mURL = new URL(osuInfo.getOSUProvider().getOSUServer());
+        mKeyStore = ks;
+    }
+
+    public OSUClient(String osu, KeyStore ks) throws MalformedURLException {
+        mOSUInfo = null;
+        mURL = new URL(osu);
+        mKeyStore = ks;
+    }
+
+    public void provision(OSUManager osuManager, Network network, KeyManager km)
+            throws IOException, GeneralSecurityException {
+        try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8,
+                OSUSocketFactory.getSocketFactory(mKeyStore, null, OSUManager.FLOW_PROVISIONING,
+                        network, mURL, km, true))) {
+
+            SPVerifier spVerifier = new SPVerifier(mOSUInfo);
+            spVerifier.verify(httpHandler.getOSUCertificate(mURL));
+
+            URL redirectURL = osuManager.prepareUserInput(mOSUInfo.getName(Locale.getDefault()));
+            OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter();
+
+            String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration,
+                    null,
+                    redirectURL.toString(),
+                    omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                    omadmAdapter.getMO(OMAConstants.DevDetailURN));
+            Log.d(TAG, "Registration request: " + regRequest);
+            OSUResponse osuResponse = httpHandler.exchangeSOAP(mURL, regRequest);
+
+            Log.d(TAG, "Response: " + osuResponse);
+            if (osuResponse.getMessageType() != OSUMessageType.PostDevData) {
+                throw new IOException("Expected a PostDevDataResponse");
+            }
+            PostDevDataResponse regResponse = (PostDevDataResponse) osuResponse;
+            String sessionID = regResponse.getSessionID();
+            if (regResponse.getExecCommand() == ExecCommand.UseClientCertTLS) {
+                ClientCertInfo ccInfo = (ClientCertInfo) regResponse.getCommandData();
+                if (ccInfo.doesAcceptMfgCerts()) {
+                    throw new IOException("Mfg certs are not supported in Android");
+                } else if (ccInfo.doesAcceptProviderCerts()) {
+                    ((WiFiKeyManager) km).enableClientAuth(ccInfo.getIssuerNames());
+                    httpHandler.renegotiate(null, null);
+                } else {
+                    throw new IOException("Neither manufacturer nor provider cert specified");
+                }
+                regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration,
+                        sessionID,
+                        redirectURL.toString(),
+                        omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                        omadmAdapter.getMO(OMAConstants.DevDetailURN));
+
+                osuResponse = httpHandler.exchangeSOAP(mURL, regRequest);
+                if (osuResponse.getMessageType() != OSUMessageType.PostDevData) {
+                    throw new IOException("Expected a PostDevDataResponse");
+                }
+                regResponse = (PostDevDataResponse) osuResponse;
+            }
+
+            if (regResponse.getExecCommand() != ExecCommand.Browser) {
+                throw new IOException("Expected a launchBrowser command");
+            }
+            Log.d(TAG, "Exec: " + regResponse.getExecCommand() + ", for '" +
+                    regResponse.getCommandData() + "'");
+
+            if (!osuResponse.getSessionID().equals(sessionID)) {
+                throw new IOException("Mismatching session IDs");
+            }
+            String webURL = ((BrowserURI) regResponse.getCommandData()).getURI();
+
+            if (webURL == null) {
+                throw new IOException("No web-url");
+            } else if (!webURL.contains(sessionID)) {
+                throw new IOException("Bad or missing session ID in webURL");
+            }
+
+            if (!osuManager.startUserInput(new URL(webURL), network)) {
+                throw new IOException("User session failed");
+            }
+
+            Log.d(TAG, " -- Sending user input complete:");
+            String userComplete = SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete,
+                    sessionID, null,
+                    omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                    omadmAdapter.getMO(OMAConstants.DevDetailURN));
+            OSUResponse moResponse1 = httpHandler.exchangeSOAP(mURL, userComplete);
+            if (moResponse1.getMessageType() != OSUMessageType.PostDevData) {
+                throw new IOException("Bad user input complete response: " + moResponse1);
+            }
+            PostDevDataResponse provResponse = (PostDevDataResponse) moResponse1;
+            GetCertData estData = checkResponse(provResponse);
+
+            Map<OSUCertType, List<X509Certificate>> certs = new HashMap<>();
+            PrivateKey clientKey = null;
+
+            MOData moData;
+            if (estData == null) {
+                moData = (MOData) provResponse.getCommandData();
+            } else {
+                try (ESTHandler estHandler = new ESTHandler((GetCertData) provResponse.
+                        getCommandData(), network, osuManager.getOMADMAdapter(),
+                        km, mKeyStore, null, OSUManager.FLOW_PROVISIONING)) {
+                    estHandler.execute(false);
+                    certs.put(OSUCertType.CA, estHandler.getCACerts());
+                    certs.put(OSUCertType.Client, estHandler.getClientCerts());
+                    clientKey = estHandler.getClientKey();
+                }
+
+                Log.d(TAG, " -- Sending provisioning cert enrollment complete:");
+                String certComplete =
+                        SOAPBuilder.buildPostDevDataResponse(RequestReason.CertEnrollmentComplete,
+                                sessionID, null,
+                                omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                                omadmAdapter.getMO(OMAConstants.DevDetailURN));
+                OSUResponse moResponse2 = httpHandler.exchangeSOAP(mURL, certComplete);
+                if (moResponse2.getMessageType() != OSUMessageType.PostDevData) {
+                    throw new IOException("Bad cert enrollment complete response: " + moResponse2);
+                }
+                PostDevDataResponse provComplete = (PostDevDataResponse) moResponse2;
+                if (provComplete.getStatus() != OSUStatus.ProvComplete ||
+                        provComplete.getOSUCommand() != OSUCommandID.AddMO) {
+                    throw new IOException("Expected addMO: " + provComplete);
+                }
+                moData = (MOData) provComplete.getCommandData();
+            }
+
+            // !!! How can an ExchangeComplete be sent w/o knowing the fate of the certs???
+            String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, null);
+            Log.d(TAG, " -- Sending updateResponse:");
+            OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse);
+            Log.d(TAG, "exComplete response: " + exComplete);
+            if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) {
+                throw new IOException("Expected ExchangeComplete: " + exComplete);
+            } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) {
+                throw new IOException("Bad ExchangeComplete status: " + exComplete);
+            }
+
+            retrieveCerts(moData.getMOTree().getRoot(), certs, network, km, mKeyStore);
+            osuManager.provisioningComplete(mOSUInfo, moData, certs, clientKey, network);
+        }
+    }
+
+    public void remediate(OSUManager osuManager, Network network, KeyManager km, HomeSP homeSP,
+                          int flowType)
+            throws IOException, GeneralSecurityException {
+        try (HTTPHandler httpHandler = createHandler(network, homeSP, km, flowType)) {
+            URL redirectURL = osuManager.prepareUserInput(homeSP.getFriendlyName());
+            OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter();
+
+            String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRemediation,
+                    null,
+                    redirectURL.toString(),
+                    omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                    omadmAdapter.getMO(OMAConstants.DevDetailURN));
+
+            OSUResponse serverResponse = httpHandler.exchangeSOAP(mURL, regRequest);
+            if (serverResponse.getMessageType() != OSUMessageType.PostDevData) {
+                throw new IOException("Expected a PostDevDataResponse");
+            }
+            String sessionID = serverResponse.getSessionID();
+
+            PostDevDataResponse pddResponse = (PostDevDataResponse) serverResponse;
+            Log.d(TAG, "Remediation response: " + pddResponse);
+
+            Map<OSUCertType, List<X509Certificate>> certs = null;
+            PrivateKey clientKey = null;
+
+            if (pddResponse.getStatus() != OSUStatus.RemediationComplete) {
+                if (pddResponse.getExecCommand() == ExecCommand.UploadMO) {
+                    String ulMessage = SOAPBuilder.buildPostDevDataResponse(RequestReason.MOUpload,
+                            null,
+                            redirectURL.toString(),
+                            omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                            omadmAdapter.getMO(OMAConstants.DevDetailURN),
+                            osuManager.getMOTree(homeSP));
+
+                    Log.d(TAG, "Upload MO: " + ulMessage);
+
+                    OSUResponse ulResponse = httpHandler.exchangeSOAP(mURL, ulMessage);
+                    if (ulResponse.getMessageType() != OSUMessageType.PostDevData) {
+                        throw new IOException("Expected a PostDevDataResponse to MOUpload");
+                    }
+                    pddResponse = (PostDevDataResponse) ulResponse;
+                }
+
+                if (pddResponse.getExecCommand() == ExecCommand.Browser) {
+                    if (flowType == OSUManager.FLOW_POLICY) {
+                        throw new IOException("Browser launch requested in policy flow");
+                    }
+                    String webURL = ((BrowserURI) pddResponse.getCommandData()).getURI();
+
+                    if (webURL == null) {
+                        throw new IOException("No web-url");
+                    } else if (!webURL.contains(sessionID)) {
+                        throw new IOException("Bad or missing session ID in webURL");
+                    }
+
+                    if (!osuManager.startUserInput(new URL(webURL), network)) {
+                        throw new IOException("User session failed");
+                    }
+
+                    Log.d(TAG, " -- Sending user input complete:");
+                    String userComplete =
+                            SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete,
+                                    sessionID, null,
+                                    omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                                    omadmAdapter.getMO(OMAConstants.DevDetailURN));
+
+                    OSUResponse udResponse = httpHandler.exchangeSOAP(mURL, userComplete);
+                    if (udResponse.getMessageType() != OSUMessageType.PostDevData) {
+                        throw new IOException("Bad user input complete response: " + udResponse);
+                    }
+                    pddResponse = (PostDevDataResponse) udResponse;
+                } else if (pddResponse.getExecCommand() == ExecCommand.GetCert) {
+                    certs = new HashMap<>();
+                    try (ESTHandler estHandler = new ESTHandler((GetCertData) pddResponse.
+                            getCommandData(), network, osuManager.getOMADMAdapter(),
+                            km, mKeyStore, homeSP, flowType)) {
+                        estHandler.execute(true);
+                        certs.put(OSUCertType.CA, estHandler.getCACerts());
+                        certs.put(OSUCertType.Client, estHandler.getClientCerts());
+                        clientKey = estHandler.getClientKey();
+                    }
+
+                    if (httpHandler.isHTTPAuthPerformed()) {        // 8.4.3.6
+                        httpHandler.renegotiate(certs, clientKey);
+                    }
+
+                    Log.d(TAG, " -- Sending remediation cert enrollment complete:");
+                    // 8.4.3.5 in the spec actually prescribes that an update URI is sent here,
+                    // but there is no remediation flow that defines user interaction after EST
+                    // so for now a null is passed.
+                    String certComplete =
+                            SOAPBuilder
+                                    .buildPostDevDataResponse(RequestReason.CertEnrollmentComplete,
+                                            sessionID, null,
+                                            omadmAdapter.getMO(OMAConstants.DevInfoURN),
+                                            omadmAdapter.getMO(OMAConstants.DevDetailURN));
+                    OSUResponse ceResponse = httpHandler.exchangeSOAP(mURL, certComplete);
+                    if (ceResponse.getMessageType() != OSUMessageType.PostDevData) {
+                        throw new IOException("Bad cert enrollment complete response: "
+                                + ceResponse);
+                    }
+                    pddResponse = (PostDevDataResponse) ceResponse;
+                } else {
+                    throw new IOException("Unexpected command: " + pddResponse.getExecCommand());
+                }
+            }
+
+            if (pddResponse.getStatus() != OSUStatus.RemediationComplete) {
+                throw new IOException("Expected a PostDevDataResponse to MOUpload");
+            }
+
+            Log.d(TAG, "Remediation response: " + pddResponse);
+
+            List<MOData> mods = new ArrayList<>();
+            for (OSUCommand command : pddResponse.getCommands()) {
+                if (command.getOSUCommand() == OSUCommandID.UpdateNode) {
+                    mods.add((MOData) command.getCommandData());
+                } else if (command.getOSUCommand() != OSUCommandID.NoMOUpdate) {
+                    throw new IOException("Unexpected OSU response: " + command);
+                }
+            }
+
+            // 1. Machine remediation: Remediation complete + replace node
+            // 2a. User remediation with upload: ExecCommand.UploadMO
+            // 2b. User remediation without upload: ExecCommand.Browser
+            // 3. User remediation only: -> sppPostDevData user input complete
+            //
+            // 4. Update node
+            // 5. -> Update response
+            // 6. Exchange complete
+
+            OSUError error = null;
+
+            String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, error);
+            Log.d(TAG, " -- Sending updateResponse:");
+            OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse);
+            Log.d(TAG, "exComplete response: " + exComplete);
+            if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) {
+                throw new IOException("Expected ExchangeComplete: " + exComplete);
+            } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) {
+                throw new IOException("Bad ExchangeComplete status: " + exComplete);
+            }
+
+            // There's a chicken and egg here: If the config is saved before sending update complete
+            // the network is lost and the remediation flow fails.
+            try {
+                osuManager.remediationComplete(homeSP, mods, certs, clientKey);
+            } catch (IOException | GeneralSecurityException e) {
+                osuManager.provisioningFailed(homeSP.getFriendlyName(), e.getMessage(), homeSP,
+                        OSUManager.FLOW_REMEDIATION);
+                error = OSUError.CommandFailed;
+            }
+        }
+    }
+
+    private HTTPHandler createHandler(Network network, HomeSP homeSP,
+                                      KeyManager km, int flowType) throws GeneralSecurityException, IOException {
+        Credential credential = homeSP.getCredential();
+
+        Log.d(TAG, "Credential method " + credential.getEAPMethod().getEAPMethodID());
+        switch (credential.getEAPMethod().getEAPMethodID()) {
+            case EAP_TTLS:
+                String user;
+                byte[] password;
+                UpdateInfo subscriptionUpdate;
+                if (flowType == OSUManager.FLOW_POLICY) {
+                    subscriptionUpdate = homeSP.getPolicy() != null ?
+                            homeSP.getPolicy().getPolicyUpdate() : null;
+                } else {
+                    subscriptionUpdate = homeSP.getSubscriptionUpdate();
+                }
+                if (subscriptionUpdate != null && subscriptionUpdate.getUsername() != null) {
+                    user = subscriptionUpdate.getUsername();
+                    password = subscriptionUpdate.getPassword() != null ?
+                            subscriptionUpdate.getPassword().getBytes(StandardCharsets.UTF_8) :
+                            new byte[0];
+                } else {
+                    user = credential.getUserName();
+                    password = credential.getPassword().getBytes(StandardCharsets.UTF_8);
+                }
+                return new HTTPHandler(StandardCharsets.UTF_8,
+                        OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network,
+                                mURL, km, true), user, password);
+            case EAP_TLS:
+                return new HTTPHandler(StandardCharsets.UTF_8,
+                        OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network,
+                                mURL, km, true));
+            default:
+                throw new IOException("Cannot remediate account with " +
+                        credential.getEAPMethod().getEAPMethodID());
+        }
+    }
+
+    private static GetCertData checkResponse(PostDevDataResponse response) throws IOException {
+        if (response.getStatus() == OSUStatus.ProvComplete &&
+                response.getOSUCommand() == OSUCommandID.AddMO) {
+            return null;
+        }
+
+        if (response.getOSUCommand() == OSUCommandID.Exec &&
+                response.getExecCommand() == ExecCommand.GetCert) {
+            return (GetCertData) response.getCommandData();
+        } else {
+            throw new IOException("Unexpected command: " + response);
+        }
+    }
+
+    private static final String[] AAACertPath =
+            {"PerProviderSubscription", "?", "AAAServerTrustRoot", "*", "CertURL"};
+    private static final String[] RemdCertPath =
+            {"PerProviderSubscription", "?", "SubscriptionUpdate", "TrustRoot", "CertURL"};
+    private static final String[] PolicyCertPath =
+            {"PerProviderSubscription", "?", "Policy", "PolicyUpdate", "TrustRoot", "CertURL"};
+
+    private static void retrieveCerts(OMANode ppsRoot,
+                                      Map<OSUCertType, List<X509Certificate>> certs,
+                                      Network network, KeyManager km, KeyStore ks)
+            throws GeneralSecurityException, IOException {
+
+        List<X509Certificate> aaaCerts = getCerts(ppsRoot, AAACertPath, network, km, ks);
+        certs.put(OSUCertType.AAA, aaaCerts);
+        certs.put(OSUCertType.Remediation, getCerts(ppsRoot, RemdCertPath, network, km, ks));
+        certs.put(OSUCertType.Policy, getCerts(ppsRoot, PolicyCertPath, network, km, ks));
+    }
+
+    private static List<X509Certificate> getCerts(OMANode ppsRoot, String[] path, Network network,
+                                                  KeyManager km, KeyStore ks)
+            throws GeneralSecurityException, IOException {
+        List<String> urls = new ArrayList<>();
+        getCertURLs(ppsRoot, Arrays.asList(path).iterator(), urls);
+        Log.d(TAG, Arrays.toString(path) + ": " + urls);
+
+        List<X509Certificate> certs = new ArrayList<>(urls.size());
+        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+        for (String urlString : urls) {
+            URL url = new URL(urlString);
+            HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8,
+                    OSUSocketFactory.getSocketFactory(ks, null, OSUManager.FLOW_PROVISIONING,
+                            network, url, km, false));
+
+            certs.add((X509Certificate) certFactory.generateCertificate(httpHandler.doGet(url)));
+        }
+        return certs;
+    }
+
+    private static void getCertURLs(OMANode root, Iterator<String> path, List<String> urls)
+            throws IOException {
+
+        String name = path.next();
+        // Log.d(TAG, "Pulling '" + name + "' out of '" + root.getName() + "'");
+        Collection<OMANode> nodes = null;
+        switch (name) {
+            case "?":
+                for (OMANode node : root.getChildren()) {
+                    if (!node.isLeaf()) {
+                        nodes = Arrays.asList(node);
+                        break;
+                    }
+                }
+                break;
+            case "*":
+                nodes = root.getChildren();
+                break;
+            default:
+                nodes = Arrays.asList(root.getChild(name));
+                break;
+        }
+
+        if (nodes == null) {
+            throw new IllegalArgumentException("No matching node in " + root.getName()
+                    + " for " + name);
+        }
+
+        for (OMANode node : nodes) {
+            if (path.hasNext()) {
+                getCertURLs(node, path, urls);
+            } else {
+                urls.add(node.getValue());
+            }
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCommand.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCommand.java
new file mode 100644
index 0000000..4730377
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCommand.java
@@ -0,0 +1,130 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+import com.android.hotspot2.osu.commands.BrowserURI;
+import com.android.hotspot2.osu.commands.ClientCertInfo;
+import com.android.hotspot2.osu.commands.GetCertData;
+import com.android.hotspot2.osu.commands.MOData;
+import com.android.hotspot2.osu.commands.MOURN;
+import com.android.hotspot2.osu.commands.OSUCommandData;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class OSUCommand {
+    private final OSUCommandID mOSUCommand;
+    private final ExecCommand mExecCommand;
+    private final OSUCommandData mCommandData;
+
+    private static final Map<String, OSUCommandID> sCommands = new HashMap<>();
+    private static final Map<String, ExecCommand> sExecs = new HashMap<>();
+
+    static {
+        sCommands.put("exec", OSUCommandID.Exec);
+        sCommands.put("addmo", OSUCommandID.AddMO);
+        sCommands.put("updatenode", OSUCommandID.UpdateNode);      // Multi
+        sCommands.put("nomoupdate", OSUCommandID.NoMOUpdate);
+
+        sExecs.put("launchbrowsertouri", ExecCommand.Browser);
+        sExecs.put("getcertificate", ExecCommand.GetCert);
+        sExecs.put("useclientcerttls", ExecCommand.UseClientCertTLS);
+        sExecs.put("uploadmo", ExecCommand.UploadMO);
+    }
+
+    public OSUCommand(XMLNode child) throws OMAException {
+        mOSUCommand = sCommands.get(child.getStrippedTag());
+
+        switch (mOSUCommand) {
+            case Exec:
+                /*
+                 * Receipt of this element by a mobile device causes the following command
+                 * to be executed.
+                 */
+                child = child.getSoleChild();
+                mExecCommand = sExecs.get(child.getStrippedTag());
+                if (mExecCommand == null) {
+                    throw new OMAException("Unrecognized exec command: " + child.getStrippedTag());
+                }
+                switch (mExecCommand) {
+                    case Browser:
+                        /*
+                         * When the mobile device receives this command, it launches its default
+                         * browser to the URI contained in this element. The URI must use HTTPS as
+                         * the protocol and must contain an FQDN.
+                         */
+                        mCommandData = new BrowserURI(child);
+                        break;
+                    case GetCert:
+                        mCommandData = new GetCertData(child);
+                        break;
+                    case UploadMO:
+                        mCommandData = new MOURN(child);
+                        break;
+                    case UseClientCertTLS:
+                        /*
+                         * Command to mobile to re-negotiate the TLS connection using a client
+                         * certificate of the accepted type or Issuer to authenticate with the
+                         * Subscription server.
+                         */
+                        mCommandData = new ClientCertInfo(child);
+                        break;
+                    default:
+                        mCommandData = null;
+                        break;
+                }
+                break;
+            case AddMO:
+                /*
+                 * This command causes an management object in the mobile devices management tree
+                 * at the specified location to be added.
+                 * If there is already a management object at that location, the object is replaced.
+                 */
+                mExecCommand = null;
+                mCommandData = new MOData(child);
+                break;
+            case UpdateNode:
+                /*
+                 * This command causes the update of an interior node and its child nodes (if any)
+                 * at the location specified in the management tree URI attribute. The content of
+                 * this element is the MO node XML.
+                 */
+                mExecCommand = null;
+                mCommandData = new MOData(child);
+                break;
+            case NoMOUpdate:
+                /*
+                 * This response is used when there is no command to be executed nor update of
+                 * any MO required.
+                 */
+                mExecCommand = null;
+                mCommandData = null;
+                break;
+            default:
+                mExecCommand = null;
+                mCommandData = null;
+                break;
+        }
+    }
+
+    public OSUCommandID getOSUCommand() {
+        return mOSUCommand;
+    }
+
+    public ExecCommand getExecCommand() {
+        return mExecCommand;
+    }
+
+    public OSUCommandData getCommandData() {
+        return mCommandData;
+    }
+
+    @Override
+    public String toString() {
+        return "OSUCommand{" +
+                "OSUCommand=" + mOSUCommand +
+                ", execCommand=" + mExecCommand +
+                ", commandData=" + mCommandData +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCommandID.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCommandID.java
new file mode 100644
index 0000000..eca953f
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCommandID.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public enum OSUCommandID {
+    Exec, AddMO, UpdateNode, NoMOUpdate
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUError.java b/packages/Osu/src/com/android/hotspot2/osu/OSUError.java
new file mode 100644
index 0000000..2fa7de0
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUError.java
@@ -0,0 +1,22 @@
+package com.android.hotspot2.osu;
+
+public enum OSUError {
+    SPPversionNotSupported,
+    MOsNotSupported,
+    CredentialsFailure,
+    RemediationFailure,
+    ProvisioningFailed,
+    ExistingCertificate,
+    CookieInvalid,
+    WebSessionID,
+    PermissionDenied,
+    CommandFailed,
+    MOaddOrUpdateFailed,
+    DeviceFull,
+    BadTreeURI,
+    TooLarge,
+    CommandNotAllowed,
+    UserAborted,
+    NotFound,
+    Other
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java b/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java
new file mode 100644
index 0000000..86c0be9
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java
@@ -0,0 +1,252 @@
+package com.android.hotspot2.osu;
+
+import android.net.wifi.ScanResult;
+import android.util.Log;
+
+import com.android.anqp.HSIconFileElement;
+import com.android.anqp.I18Name;
+import com.android.anqp.IconInfo;
+import com.android.anqp.OSUProvider;
+import com.android.hotspot2.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class OSUInfo {
+    public static final String GenericLocale = "zxx";
+
+    public enum IconStatus {
+        NotQueried,     //
+        InProgress,     // Query pending
+        NotAvailable,   // Deterministically unavailable
+        Available       // Icon data retrieved
+    }
+
+    private final long mBSSID;
+    private final long mHESSID;
+    private final int mAnqpDomID;
+    private final String mSSID;
+    private final String mAdvertisingSSID;
+    private final OSUProvider mOSUProvider;
+    private final int mOsuID;
+    private long mOSUBssid;
+    private IconStatus mIconStatus = IconStatus.NotQueried;
+    private HSIconFileElement mIconFileElement;
+    private IconInfo mIconInfo;
+
+    public OSUInfo(ScanResult scanResult, String ssid, OSUProvider osuProvider, int osuID) {
+        mOsuID = osuID;
+        mBSSID = Utils.parseMac(scanResult.BSSID);
+        mHESSID = scanResult.hessid;
+        mAnqpDomID = scanResult.anqpDomainId;
+        mAdvertisingSSID = scanResult.SSID;
+        mSSID = ssid;
+        mOSUProvider = osuProvider;
+    }
+
+    public long getOSUBssid() {
+        return mOSUBssid;
+    }
+
+    public void setOSUBssid(long OSUBssid) {
+        mOSUBssid = OSUBssid;
+    }
+
+    public long getHESSID() {
+        return mHESSID;
+    }
+
+    public int getAnqpDomID() {
+        return mAnqpDomID;
+    }
+
+    public String getAdvertisingSSID() {
+        return mAdvertisingSSID;
+    }
+
+    public Set<Locale> getNameLocales() {
+        Set<Locale> locales = new HashSet<>(mOSUProvider.getNames().size());
+        for (I18Name name : mOSUProvider.getNames()) {
+            locales.add(name.getLocale());
+        }
+        return locales;
+    }
+
+    public Set<Locale> getServiceLocales() {
+        Set<Locale> locales = new HashSet<>(mOSUProvider.getServiceDescriptions().size());
+        for (I18Name name : mOSUProvider.getServiceDescriptions()) {
+            locales.add(name.getLocale());
+        }
+        return locales;
+    }
+
+    public Set<String> getIconLanguages() {
+        Set<String> locales = new HashSet<>(mOSUProvider.getIcons().size());
+        for (IconInfo iconInfo : mOSUProvider.getIcons()) {
+            locales.add(iconInfo.getLanguage());
+        }
+        return locales;
+    }
+
+    public String getName(Locale locale) {
+        List<ScoreEntry<String>> scoreList = new ArrayList<>();
+        for (I18Name name : mOSUProvider.getNames()) {
+            if (locale == null || name.getLocale().equals(locale)) {
+                return name.getText();
+            }
+            scoreList.add(new ScoreEntry<String>(name.getText(),
+                    languageScore(name.getLanguage(), locale)));
+        }
+        Collections.sort(scoreList);
+        return scoreList.isEmpty() ? null : scoreList.iterator().next().getData();
+    }
+
+    public String getServiceDescription(Locale locale) {
+        List<ScoreEntry<String>> scoreList = new ArrayList<>();
+        for (I18Name service : mOSUProvider.getServiceDescriptions()) {
+            if (locale == null || service.getLocale().equals(locale)) {
+                return service.getText();
+            }
+            scoreList.add(new ScoreEntry<>(service.getText(),
+                    languageScore(service.getLanguage(), locale)));
+        }
+        Collections.sort(scoreList);
+        return scoreList.isEmpty() ? null : scoreList.iterator().next().getData();
+    }
+
+    public int getOsuID() {
+        return mOsuID;
+    }
+
+    public void setIconStatus(IconStatus iconStatus) {
+        synchronized (mOSUProvider) {
+            mIconStatus = iconStatus;
+        }
+    }
+
+    public IconStatus getIconStatus() {
+        synchronized (mOSUProvider) {
+            return mIconStatus;
+        }
+    }
+
+    public HSIconFileElement getIconFileElement() {
+        synchronized (mOSUProvider) {
+            return mIconFileElement;
+        }
+    }
+
+    public IconInfo getIconInfo() {
+        synchronized (mOSUProvider) {
+            return mIconInfo;
+        }
+    }
+
+    public void setIconFileElement(HSIconFileElement iconFileElement, String fileName) {
+        synchronized (mOSUProvider) {
+            mIconFileElement = iconFileElement;
+            for (IconInfo iconInfo : mOSUProvider.getIcons()) {
+                if (iconInfo.getFileName().equals(fileName)) {
+                    mIconInfo = iconInfo;
+                    break;
+                }
+            }
+            mIconStatus = IconStatus.Available;
+        }
+    }
+
+    private static class ScoreEntry<T> implements Comparable<ScoreEntry> {
+        private final T mData;
+        private final int mScore;
+
+        private ScoreEntry(T data, int score) {
+            mData = data;
+            mScore = score;
+        }
+
+        public T getData() {
+            return mData;
+        }
+
+        @Override
+        public int compareTo(ScoreEntry other) {
+            return Integer.compare(mScore, other.mScore);
+        }
+    }
+
+    public List<IconInfo> getIconInfo(Locale locale, Set<String> types, int width, int height) {
+        if (mOSUProvider.getIcons().isEmpty()) {
+            return null;
+        }
+        Log.d(OSUManager.TAG, "Matching icons against " + locale
+                + ", types " + types + ", " + width + "*" + height);
+
+        List<ScoreEntry<IconInfo>> matches = new ArrayList<>();
+        for (IconInfo iconInfo : mOSUProvider.getIcons()) {
+            Log.d(OSUManager.TAG, "Checking icon " + iconInfo.toString());
+            if (!types.contains(iconInfo.getIconType())) {
+                continue;
+            }
+
+            int score = languageScore(iconInfo.getLanguage(), locale);
+            int delta = iconInfo.getWidth() - width;
+            // Best size score is 1024 for a exact match, i.e. 2048 if both sides match
+            if (delta >= 0) {
+                score += (256 - delta) * 4;  // Prefer down-scaling
+            } else {
+                score += 256 + delta;    // Before up-scaling
+            }
+            delta = iconInfo.getHeight() - height;
+            if (delta >= 0) {
+                score += (256 - delta) * 4;
+            } else {
+                score += 256 + delta;
+            }
+            matches.add(new ScoreEntry<>(iconInfo, score));
+        }
+        if (matches.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Collections.sort(matches);
+        List<IconInfo> icons = new ArrayList<>(matches.size());
+        for (ScoreEntry<IconInfo> scoredIcon : matches) {
+            icons.add(scoredIcon.getData());
+        }
+        return icons;
+    }
+
+    private static int languageScore(String language, Locale locale) {
+        if (language.length() == 3 && language.equalsIgnoreCase(locale.getISO3Language()) ||
+                language.length() == 2 && language.equalsIgnoreCase(locale.getLanguage())) {
+            return 4096;
+        } else if (language.equalsIgnoreCase(GenericLocale)) {
+            return 3072;
+        } else if (language.equalsIgnoreCase("eng")) {
+            return 2048;
+        } else {
+            return 1024;
+        }
+    }
+
+    public long getBSSID() {
+        return mBSSID;
+    }
+
+    public String getSSID() {
+        return mSSID;
+    }
+
+    public OSUProvider getOSUProvider() {
+        return mOSUProvider;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("OSU Info '%s' %012x -> %s, icon %s",
+                mSSID, mBSSID, getServiceDescription(null), mIconStatus);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUListener.java b/packages/Osu/src/com/android/hotspot2/osu/OSUListener.java
new file mode 100644
index 0000000..9197620
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUListener.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public interface OSUListener {
+    public void osuNotification(int count);
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java b/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java
new file mode 100644
index 0000000..c90e96b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java
@@ -0,0 +1,977 @@
+package com.android.hotspot2.osu;
+
+import android.content.Context;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.util.Log;
+
+import com.android.anqp.Constants;
+import com.android.anqp.OSUProvider;
+import com.android.hotspot2.AppBridge;
+import com.android.hotspot2.OMADMAdapter;
+import com.android.hotspot2.PasspointMatch;
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.WifiNetworkAdapter;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.osu.commands.MOData;
+import com.android.hotspot2.osu.service.RedirectListener;
+import com.android.hotspot2.osu.service.SubscriptionTimer;
+import com.android.hotspot2.pps.HomeSP;
+import com.android.hotspot2.pps.UpdateInfo;
+
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.KeyManager;
+
+public class OSUManager {
+    public static final String TAG = "OSUMGR";
+    public static final boolean R2_ENABLED = true;
+    public static final boolean R2_MOCK = true;
+    private static final boolean MATCH_BSSID = false;
+
+    private static final String KEYSTORE_FILE = "passpoint.ks";
+    private static final String WFA_CA_LOC = "/etc/security/wfa";
+
+    private static final String OSU_COUNT = "osu-count";
+    private static final String SP_NAME = "sp-name";
+    private static final String PROV_SUCCESS = "prov-success";
+    private static final String DEAUTH = "deauth";
+    private static final String DEAUTH_DELAY = "deauth-delay";
+    private static final String DEAUTH_URL = "deauth-url";
+    private static final String PROV_MESSAGE = "prov-message";
+
+    private static final long REMEDIATION_TIMEOUT = 120000L;
+    // How many scan result batches to hang on to
+
+    public static final int FLOW_PROVISIONING = 1;
+    public static final int FLOW_REMEDIATION = 2;
+    public static final int FLOW_POLICY = 3;
+
+    public static final String CERT_WFA_ALIAS = "wfa-root-";
+    public static final String CERT_REM_ALIAS = "rem-";
+    public static final String CERT_POLICY_ALIAS = "pol-";
+    public static final String CERT_SHARED_ALIAS = "shr-";
+    public static final String CERT_CLT_CERT_ALIAS = "clt-";
+    public static final String CERT_CLT_KEY_ALIAS = "prv-";
+    public static final String CERT_CLT_CA_ALIAS = "aaa-";
+
+    // Preferred icon parameters
+    private static final Set<String> ICON_TYPES =
+            new HashSet<>(Arrays.asList("image/png", "image/jpeg"));
+    private static final int ICON_WIDTH = 64;
+    private static final int ICON_HEIGHT = 64;
+    public static final Locale LOCALE = java.util.Locale.getDefault();
+
+    private final WifiNetworkAdapter mWifiNetworkAdapter;
+
+    private final AppBridge mAppBridge;
+    private final Context mContext;
+    private final IconCache mIconCache;
+    private final SubscriptionTimer mSubscriptionTimer;
+    private final Set<String> mOSUSSIDs = new HashSet<>();
+    private final Map<OSUProvider, OSUInfo> mOSUMap = new HashMap<>();
+    private final KeyStore mKeyStore;
+    private RedirectListener mRedirectListener;
+    private final AtomicInteger mOSUSequence = new AtomicInteger();
+    private OSUThread mProvisioningThread;
+    private final Map<String, OSUThread> mServiceThreads = new HashMap<>();
+    private volatile OSUInfo mPendingOSU;
+    private volatile Integer mOSUNwkID;
+
+    private final OSUCache mOSUCache;
+
+    public OSUManager(Context context) {
+        mContext = context;
+        mAppBridge = new AppBridge(context);
+        mIconCache = new IconCache(this);
+        mWifiNetworkAdapter = new WifiNetworkAdapter(context, this);
+        mSubscriptionTimer = new SubscriptionTimer(this, mWifiNetworkAdapter, context);
+        mOSUCache = new OSUCache();
+        KeyStore ks = null;
+        try {
+            //ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC));
+            ks = loadKeyStore(new File(context.getFilesDir(), KEYSTORE_FILE),
+                    OSUSocketFactory.buildCertSet());
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to initialize Passpoint keystore, OSU disabled", e);
+        }
+        mKeyStore = ks;
+    }
+
+    private static KeyStore loadKeyStore(File ksFile, Set<X509Certificate> diskCerts)
+            throws IOException {
+        try {
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            if (ksFile.exists()) {
+                try (FileInputStream in = new FileInputStream(ksFile)) {
+                    keyStore.load(in, null);
+                }
+
+                // Note: comparing two sets of certs does not work.
+                boolean mismatch = false;
+                int loadCount = 0;
+                for (int n = 0; n < 1000; n++) {
+                    String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
+                    Certificate cert = keyStore.getCertificate(alias);
+                    if (cert == null) {
+                        break;
+                    }
+
+                    loadCount++;
+                    boolean matched = false;
+                    Iterator<X509Certificate> iter = diskCerts.iterator();
+                    while (iter.hasNext()) {
+                        X509Certificate diskCert = iter.next();
+                        if (cert.equals(diskCert)) {
+                            iter.remove();
+                            matched = true;
+                            break;
+                        }
+                    }
+                    if (!matched) {
+                        mismatch = true;
+                        break;
+                    }
+                }
+                if (mismatch || !diskCerts.isEmpty()) {
+                    Log.d(TAG, "Re-seeding Passpoint key store with " +
+                            diskCerts.size() + " WFA certs");
+                    for (int n = 0; n < 1000; n++) {
+                        String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
+                        Certificate cert = keyStore.getCertificate(alias);
+                        if (cert == null) {
+                            break;
+                        } else {
+                            keyStore.deleteEntry(alias);
+                        }
+                    }
+                    int index = 0;
+                    for (X509Certificate caCert : diskCerts) {
+                        keyStore.setCertificateEntry(
+                                String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
+                        index++;
+                    }
+
+                    try (FileOutputStream out = new FileOutputStream(ksFile)) {
+                        keyStore.store(out, null);
+                    }
+                } else {
+                    Log.d(TAG, "Loaded Passpoint key store with " + loadCount + " CA certs");
+                    Enumeration<String> aliases = keyStore.aliases();
+                    while (aliases.hasMoreElements()) {
+                        Log.d("ZXC", "KS Alias '" + aliases.nextElement() + "'");
+                    }
+                }
+            } else {
+                keyStore.load(null, null);
+                int index = 0;
+                for (X509Certificate caCert : diskCerts) {
+                    keyStore.setCertificateEntry(
+                            String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
+                    index++;
+                }
+
+                try (FileOutputStream out = new FileOutputStream(ksFile)) {
+                    keyStore.store(out, null);
+                }
+                Log.d(TAG, "Initialized Passpoint key store with " +
+                        diskCerts.size() + " CA certs");
+            }
+            return keyStore;
+        } catch (GeneralSecurityException gse) {
+            throw new IOException(gse);
+        }
+    }
+
+    private static Set<X509Certificate> readCertsFromDisk(String dir) throws CertificateException {
+        Set<X509Certificate> certs = new HashSet<>();
+        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+        File caDir = new File(dir);
+        File[] certFiles = caDir.listFiles();
+        if (certFiles != null) {
+            for (File certFile : certFiles) {
+                try {
+                    try (FileInputStream in = new FileInputStream(certFile)) {
+                        Certificate cert = certFactory.generateCertificate(in);
+                        if (cert instanceof X509Certificate) {
+                            certs.add((X509Certificate) cert);
+                        }
+                    }
+                } catch (CertificateException | IOException e) {
+                            /* Ignore */
+                }
+            }
+        }
+        return certs;
+    }
+
+    public KeyStore getKeyStore() {
+        return mKeyStore;
+    }
+
+    private static class OSUThread extends Thread {
+        private final OSUClient mOSUClient;
+        private final OSUManager mOSUManager;
+        private final HomeSP mHomeSP;
+        private final String mSpName;
+        private final int mFlowType;
+        private final KeyManager mKeyManager;
+        private final long mLaunchTime;
+        private final Object mLock = new Object();
+        private boolean mLocalAddressSet;
+        private Network mNetwork;
+
+        private OSUThread(OSUInfo osuInfo, OSUManager osuManager, KeyManager km)
+                throws MalformedURLException {
+            mOSUClient = new OSUClient(osuInfo, osuManager.getKeyStore());
+            mOSUManager = osuManager;
+            mHomeSP = null;
+            mSpName = osuInfo.getName(LOCALE);
+            mFlowType = FLOW_PROVISIONING;
+            mKeyManager = km;
+            mLaunchTime = System.currentTimeMillis();
+
+            setDaemon(true);
+            setName("OSU Client Thread");
+        }
+
+        private OSUThread(String osuURL, OSUManager osuManager, KeyManager km, HomeSP homeSP,
+                          int flowType) throws MalformedURLException {
+            mOSUClient = new OSUClient(osuURL, osuManager.getKeyStore());
+            mOSUManager = osuManager;
+            mHomeSP = homeSP;
+            mSpName = homeSP.getFriendlyName();
+            mFlowType = flowType;
+            mKeyManager = km;
+            mLaunchTime = System.currentTimeMillis();
+
+            setDaemon(true);
+            setName("OSU Client Thread");
+        }
+
+        public long getLaunchTime() {
+            return mLaunchTime;
+        }
+
+        private void connect(Network network) {
+            synchronized (mLock) {
+                mNetwork = network;
+                mLocalAddressSet = true;
+                mLock.notifyAll();
+            }
+            Log.d(TAG, "Client notified...");
+        }
+
+        @Override
+        public void run() {
+            Log.d(TAG, mFlowType + "-" + getName() + " running.");
+            Network network;
+            synchronized (mLock) {
+                while (!mLocalAddressSet) {
+                    try {
+                        mLock.wait();
+                    } catch (InterruptedException ie) {
+                        /**/
+                    }
+                    Log.d(TAG, "OSU Thread running...");
+                }
+                network = mNetwork;
+            }
+
+            if (network == null) {
+                Log.d(TAG, "Association failed, exiting OSU flow");
+                mOSUManager.provisioningFailed(mSpName, "Network cannot be reached",
+                        mHomeSP, mFlowType);
+                return;
+            }
+
+            Log.d(TAG, "OSU SSID Associated at " + network.toString());
+            try {
+                if (mFlowType == FLOW_PROVISIONING) {
+                    mOSUClient.provision(mOSUManager, network, mKeyManager);
+                } else {
+                    mOSUClient.remediate(mOSUManager, network, mKeyManager, mHomeSP, mFlowType);
+                }
+            } catch (Throwable t) {
+                Log.w(TAG, "OSU flow failed: " + t, t);
+                mOSUManager.provisioningFailed(mSpName, t.getMessage(), mHomeSP, mFlowType);
+            }
+        }
+    }
+
+    /*
+    public void startOSU() {
+        registerUserInputListener(new UserInputListener() {
+            @Override
+            public void requestUserInput(URL target, Network network, URL endRedirect) {
+                Log.d(TAG, "Browser to " + target + ", land at " + endRedirect);
+
+                final Intent intent = new Intent(
+                        ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
+                intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network);
+                intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
+                        new CaptivePortal(new ICaptivePortal.Stub() {
+                            @Override
+                            public void appResponse(int response) {
+                            }
+                        }));
+                //intent.setData(Uri.parse(target.toString()));     !!! Doesn't work!
+                intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString());
+                intent.setFlags(
+                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+            }
+
+            @Override
+            public String operationStatus(String spIdentity, OSUOperationStatus status,
+                                          String message) {
+                Log.d(TAG, "OSU OP Status: " + status + ", message " + message);
+                Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
+                intent.putExtra(SP_NAME, spIdentity);
+                intent.putExtra(PROV_SUCCESS, status == OSUOperationStatus.ProvisioningSuccess);
+                if (message != null) {
+                    intent.putExtra(PROV_MESSAGE, message);
+                }
+                intent.setFlags(
+                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                return null;
+            }
+
+            @Override
+            public void deAuthNotification(String spIdentity, boolean ess, int delay, URL url) {
+                Log.i(TAG, "De-authentication imminent for " + (ess ? "ess" : "bss") +
+                        ", redirect to " + url);
+                Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
+                intent.putExtra(SP_NAME, spIdentity);
+                intent.putExtra(DEAUTH, ess);
+                intent.putExtra(DEAUTH_DELAY, delay);
+                intent.putExtra(DEAUTH_URL, url.toString());
+                intent.setFlags(
+                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+            }
+        });
+        addOSUListener(new OSUListener() {
+            @Override
+            public void osuNotification(int count) {
+                Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
+                intent.putExtra(OSU_COUNT, count);
+                intent.setFlags(
+                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+            }
+        });
+        mWifiNetworkAdapter.initialize();
+        mSubscriptionTimer.checkUpdates();
+    }
+    */
+
+    public List<OSUInfo> getAvailableOSUs() {
+        synchronized (mOSUMap) {
+            List<OSUInfo> completeOSUs = new ArrayList<>();
+            for (OSUInfo osuInfo : mOSUMap.values()) {
+                if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
+                    completeOSUs.add(osuInfo);
+                }
+            }
+            return completeOSUs;
+        }
+    }
+
+    public void recheckTimers() {
+        mSubscriptionTimer.checkUpdates();
+    }
+
+    public void setOSUSelection(int osuID) {
+        OSUInfo selection = null;
+        for (OSUInfo osuInfo : mOSUMap.values()) {
+            Log.d("ZXZ", "In select: " + osuInfo + ", id " + osuInfo.getOsuID());
+            if (osuInfo.getOsuID() == osuID &&
+                    osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
+                selection = osuInfo;
+                break;
+            }
+        }
+
+        Log.d(TAG, "Selected OSU ID " + osuID + ", matches " + selection);
+
+        if (selection == null) {
+            mPendingOSU = null;
+            return;
+        }
+
+        mPendingOSU = selection;
+        WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig();
+
+        if (config != null &&
+                bssidMatch(selection) &&
+                Utils.unquote(config.SSID).equals(selection.getSSID())) {
+
+            try {
+                // Go straight to provisioning if the network is already selected.
+                // Also note that mOSUNwkID is left unset to leave the network around after
+                // flow completion since it was not added by the OSU flow.
+                initiateProvisioning(mPendingOSU, mWifiNetworkAdapter.getCurrentNetwork());
+            } catch (IOException ioe) {
+                notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
+                        mPendingOSU.getName(LOCALE));
+            } finally {
+                mPendingOSU = null;
+            }
+        } else {
+            try {
+                mOSUNwkID = mWifiNetworkAdapter.connect(selection, mPendingOSU.getName(LOCALE));
+            } catch (IOException ioe) {
+                notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
+                        selection.getName(LOCALE));
+            }
+        }
+    }
+
+    public void networkConfigChange(WifiConfiguration configuration) {
+        mWifiNetworkAdapter.networkConfigChange(configuration);
+    }
+
+    public void networkConnectEvent(WifiInfo wifiInfo) {
+        if (wifiInfo != null) {
+            setActiveNetwork(mWifiNetworkAdapter.getActiveWifiConfig(),
+                    mWifiNetworkAdapter.getCurrentNetwork());
+        }
+    }
+
+    public void wifiStateChange(boolean on) {
+        if (!on) {
+            int current = mOSUMap.size();
+            mOSUMap.clear();
+            mOSUCache.clearAll();
+            mIconCache.clear();
+            if (current > 0) {
+                notifyOSUCount(0);
+            }
+        }
+    }
+
+    private boolean bssidMatch(OSUInfo osuInfo) {
+        if (MATCH_BSSID) {
+            WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
+            return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getBSSID();
+        } else {
+            return true;
+        }
+    }
+
+    public void setActiveNetwork(WifiConfiguration wifiConfiguration, Network network) {
+        Log.d(TAG, "Network change: " + network + ", cfg " +
+                (wifiConfiguration != null ? wifiConfiguration.SSID : "-") + ", osu " + mPendingOSU);
+        if (mPendingOSU != null &&
+                wifiConfiguration != null &&
+                network != null &&
+                bssidMatch(mPendingOSU) &&
+                Utils.unquote(wifiConfiguration.SSID).equals(mPendingOSU.getSSID())) {
+
+            try {
+                Log.d(TAG, "New network " + network + ", current OSU " + mPendingOSU);
+                initiateProvisioning(mPendingOSU, network);
+            } catch (IOException ioe) {
+                notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
+                        mPendingOSU.getName(LOCALE));
+            } finally {
+                mPendingOSU = null;
+            }
+            return;
+        }
+
+        /*
+        // !!! Hack to force start remediation at connection time
+        else if (wifiConfiguration != null && wifiConfiguration.isPasspoint()) {
+            HomeSP homeSP = mWifiConfigStore.getHomeSPForConfig(wifiConfiguration);
+            if (homeSP != null && homeSP.getSubscriptionUpdate() != null) {
+                if (!mServiceThreads.containsKey(homeSP.getFQDN())) {
+                    try {
+                        remediate(homeSP);
+                    } catch (IOException ioe) {
+                        Log.w(TAG, "Failed to remediate: " + ioe);
+                    }
+                }
+            }
+        }
+        */
+        else if (wifiConfiguration == null) {
+            mServiceThreads.clear();
+        }
+    }
+
+
+    /**
+     * Called when an OSU has been selected and the associated network is fully connected.
+     *
+     * @param osuInfo The selected OSUInfo or null if the current OSU flow is cancelled externally,
+     *                e.g. WiFi is turned off or the OSU network is otherwise detected as
+     *                unreachable.
+     * @param network The currently associated network (for the OSU SSID).
+     * @throws IOException
+     * @throws GeneralSecurityException
+     */
+    private void initiateProvisioning(OSUInfo osuInfo, Network network)
+            throws IOException {
+        synchronized (mWifiNetworkAdapter) {
+            if (mProvisioningThread != null) {
+                mProvisioningThread.connect(null);
+                mProvisioningThread = null;
+            }
+            if (mRedirectListener != null) {
+                mRedirectListener.abort();
+                mRedirectListener = null;
+            }
+            if (osuInfo != null) {
+                //new ConnMonitor().start();
+                mProvisioningThread = new OSUThread(osuInfo, this, getKeyManager(null, mKeyStore));
+                mProvisioningThread.start();
+                //mWifiNetworkAdapter.associate(osuInfo.getSSID(),
+                //        osuInfo.getBSSID(), osuInfo.getOSUProvider().getOsuNai());
+                mProvisioningThread.connect(network);
+            }
+        }
+    }
+
+    /**
+     * @param homeSP The Home SP associated with the keying material in question. Passing
+     *               null returns a "system wide" KeyManager to support pre-provisioned certs based
+     *               on names retrieved from the ClientCertInfo request.
+     * @return A key manager suitable for the given configuration (or pre-provisioned keys).
+     */
+    private static KeyManager getKeyManager(HomeSP homeSP, KeyStore keyStore)
+            throws IOException {
+        return homeSP != null ? new ClientKeyManager(homeSP, keyStore) :
+                new WiFiKeyManager(keyStore);
+    }
+
+    public boolean isOSU(String ssid) {
+        synchronized (mOSUMap) {
+            return mOSUSSIDs.contains(ssid);
+        }
+    }
+
+    public void tickleIconCache(boolean all) {
+        mIconCache.tickle(all);
+
+        if (all) {
+            synchronized (mOSUMap) {
+                int current = mOSUMap.size();
+                mOSUMap.clear();
+                mOSUCache.clearAll();
+                mIconCache.clear();
+                if (current > 0) {
+                    notifyOSUCount(0);
+                }
+            }
+        }
+    }
+
+    public void pushScanResults(Collection<ScanResult> scanResults) {
+        Map<OSUProvider, ScanResult> results = mOSUCache.pushScanResults(scanResults);
+        if (results != null) {
+            updateOSUInfoCache(results);
+        }
+    }
+
+    private void updateOSUInfoCache(Map<OSUProvider, ScanResult> results) {
+        Map<OSUProvider, OSUInfo> osus = new HashMap<>();
+        for (Map.Entry<OSUProvider, ScanResult> entry : results.entrySet()) {
+            OSUInfo existing = mOSUMap.get(entry.getKey());
+            long bssid = Utils.parseMac(entry.getValue().BSSID);
+
+            if (existing == null || existing.getBSSID() != bssid) {
+                osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(),
+                        entry.getKey(), mOSUSequence.getAndIncrement()));
+            } else {
+                // Maintain existing entries.
+                osus.put(entry.getKey(), existing);
+            }
+        }
+
+        mOSUMap.clear();
+        mOSUMap.putAll(osus);
+
+        mOSUSSIDs.clear();
+        for (OSUInfo osuInfo : mOSUMap.values()) {
+            mOSUSSIDs.add(osuInfo.getSSID());
+        }
+
+        if (mOSUMap.isEmpty()) {
+            notifyOSUCount(0);
+        }
+        initiateIconQueries();
+        Log.d(TAG, "Latest (app) OSU info: " + mOSUMap);
+    }
+
+    public void iconResults(List<OSUInfo> osuInfos) {
+        int newIcons = 0;
+        for (OSUInfo osuInfo : osuInfos) {
+            if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
+                newIcons++;
+            }
+        }
+        if (newIcons > 0) {
+            int count = 0;
+            for (OSUInfo existing : mOSUMap.values()) {
+                if (existing.getIconStatus() == OSUInfo.IconStatus.Available) {
+                    count++;
+                }
+            }
+            Log.d(TAG, "Icon results for " + count + " OSUs");
+            notifyOSUCount(count);
+        }
+    }
+
+    private void notifyOSUCount(int count) {
+        mAppBridge.showOsuCount(count, getAvailableOSUs());
+    }
+
+    private void initiateIconQueries() {
+        for (OSUInfo osuInfo : mOSUMap.values()) {
+            if (osuInfo.getIconStatus() == OSUInfo.IconStatus.NotQueried) {
+                mIconCache.startIconQuery(osuInfo,
+                        osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT));
+            }
+        }
+    }
+
+    public void deauth(long bssid, boolean ess, int delay, String url) throws MalformedURLException {
+        Log.d(TAG, String.format("De-auth imminent on %s, delay %ss to '%s'",
+                ess ? "ess" : "bss",
+                delay,
+                url));
+        mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess);
+        HomeSP homeSP = mWifiNetworkAdapter.getCurrentSP();
+        String spName = homeSP != null ? homeSP.getFriendlyName() : "unknown";
+        mAppBridge.showDeauth(spName, ess, delay, url);
+    }
+
+    // !!! Consistently check passpoint match.
+    // !!! Convert to a one-thread thread-pool
+    public void wnmRemediate(long bssid, String url, PasspointMatch match)
+            throws IOException, SAXException {
+        WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig();
+        HomeSP homeSP = MOManager.buildSP(config.getMoTree());
+        if (homeSP == null) {
+            throw new IOException("Remediation request for unidentified Passpoint network " +
+                    config.networkId);
+        }
+        Network network = mWifiNetworkAdapter.getCurrentNetwork();
+        if (network == null) {
+            throw new IOException("Failed to determine current network");
+        }
+        WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
+        if (wifiInfo == null || Utils.parseMac(wifiInfo.getBSSID()) != bssid) {
+            throw new IOException("Mismatching BSSID");
+        }
+        Log.d(TAG, "WNM Remediation on " + network.netId + " FQDN " + homeSP.getFQDN());
+
+        doRemediate(url, network, homeSP, false);
+    }
+
+    public void remediate(HomeSP homeSP, boolean policy) throws IOException, SAXException {
+        UpdateInfo updateInfo;
+        if (policy) {
+            if (homeSP.getPolicy() == null) {
+                throw new IOException("No policy object");
+            }
+            updateInfo = homeSP.getPolicy().getPolicyUpdate();
+        } else {
+            updateInfo = homeSP.getSubscriptionUpdate();
+        }
+        switch (updateInfo.getUpdateRestriction()) {
+            case HomeSP: {
+                Network network = mWifiNetworkAdapter.getCurrentNetwork();
+                if (network == null) {
+                    throw new IOException("Failed to determine current network");
+                }
+
+                WifiConfiguration config = mWifiNetworkAdapter.getActivePasspointNetwork();
+                HomeSP activeSP = MOManager.buildSP(config.getMoTree());
+
+                if (activeSP == null || !activeSP.getFQDN().equals(homeSP.getFQDN())) {
+                    throw new IOException("Remediation restricted to HomeSP");
+                }
+                doRemediate(updateInfo.getURI(), network, homeSP, policy);
+                break;
+            }
+            case RoamingPartner: {
+                Network network = mWifiNetworkAdapter.getCurrentNetwork();
+                if (network == null) {
+                    throw new IOException("Failed to determine current network");
+                }
+
+                WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
+                if (wifiInfo == null) {
+                    throw new IOException("Unable to determine WiFi info");
+                }
+
+                PasspointMatch match = mWifiNetworkAdapter.
+                        matchProviderWithCurrentNetwork(homeSP.getFQDN());
+
+                if (match == PasspointMatch.HomeProvider ||
+                        match == PasspointMatch.RoamingProvider) {
+                    doRemediate(updateInfo.getURI(), network, homeSP, policy);
+                } else {
+                    throw new IOException("No roaming network match: " + match);
+                }
+                break;
+            }
+            case Unrestricted: {
+                Network network = mWifiNetworkAdapter.getCurrentNetwork();
+                doRemediate(updateInfo.getURI(), network, homeSP, policy);
+                break;
+            }
+        }
+    }
+
+    private void doRemediate(String url, Network network, HomeSP homeSP, boolean policy)
+            throws IOException {
+        synchronized (mWifiNetworkAdapter) {
+            OSUThread existing = mServiceThreads.get(homeSP.getFQDN());
+            if (existing != null) {
+                if (System.currentTimeMillis() - existing.getLaunchTime() > REMEDIATION_TIMEOUT) {
+                    throw new IOException("Ignoring recurring remediation request");
+                } else {
+                    existing.connect(null);
+                }
+            }
+
+            try {
+                OSUThread osuThread = new OSUThread(url, this,
+                        getKeyManager(homeSP, mKeyStore),
+                        homeSP, policy ? FLOW_POLICY : FLOW_REMEDIATION);
+                osuThread.start();
+                osuThread.connect(network);
+                mServiceThreads.put(homeSP.getFQDN(), osuThread);
+            } catch (MalformedURLException me) {
+                throw new IOException("Failed to start remediation: " + me);
+            }
+        }
+    }
+
+    public MOTree getMOTree(HomeSP homeSP) throws IOException {
+        return mWifiNetworkAdapter.getMOTree(homeSP);
+    }
+
+    public void notifyIconReceived(long bssid, String fileName, byte[] data) {
+        mIconCache.notifyIconReceived(bssid, fileName, data);
+    }
+
+    public void doIconQuery(long bssid, String fileName) {
+        mWifiNetworkAdapter.doIconQuery(bssid, fileName);
+    }
+
+    protected URL prepareUserInput(String spName) throws IOException {
+        mRedirectListener = new RedirectListener(this, spName);
+        return mRedirectListener.getURL();
+    }
+
+    protected boolean startUserInput(URL target, Network network) throws IOException {
+        mRedirectListener.startService();
+        mWifiNetworkAdapter.launchBrowser(target, network, mRedirectListener.getURL());
+
+        return mRedirectListener.waitForUser();
+    }
+
+    public String notifyUser(OSUOperationStatus status, String message, String spName) {
+        if (status == OSUOperationStatus.UserInputComplete) {
+            return null;
+        }
+        if (mOSUNwkID != null) {
+            // Delete the OSU network if it was added by the OSU flow
+            mWifiNetworkAdapter.deleteNetwork(mOSUNwkID);
+            mOSUNwkID = null;
+        }
+        mAppBridge.showStatus(status, spName, message, null);
+        return null;
+    }
+
+    public void provisioningFailed(String spName, String message, HomeSP homeSP,
+                                   int flowType) {
+        synchronized (mWifiNetworkAdapter) {
+            switch (flowType) {
+                case FLOW_PROVISIONING:
+                    mProvisioningThread = null;
+                    if (mRedirectListener != null) {
+                        mRedirectListener.abort();
+                        mRedirectListener = null;
+                    }
+                    break;
+                case FLOW_REMEDIATION:
+                case FLOW_POLICY:
+                    mServiceThreads.remove(homeSP.getFQDN());
+                    if (mServiceThreads.isEmpty() && mRedirectListener != null) {
+                        mRedirectListener.abort();
+                        mRedirectListener = null;
+                    }
+                    break;
+            }
+        }
+        notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName);
+    }
+
+    public void provisioningComplete(OSUInfo osuInfo,
+                                     MOData moData, Map<OSUCertType, List<X509Certificate>> certs,
+                                     PrivateKey privateKey, Network osuNetwork) {
+        synchronized (mWifiNetworkAdapter) {
+            mProvisioningThread = null;
+        }
+        try {
+            Log.d("ZXZ", "MOTree.toXML: " + moData.getMOTree().toXml());
+            HomeSP homeSP = mWifiNetworkAdapter.addSP(moData.getMOTree());
+
+            Integer spNwk = mWifiNetworkAdapter.addNetwork(homeSP, certs, privateKey, osuNetwork);
+            if (spNwk == null) {
+                notifyUser(OSUOperationStatus.ProvisioningFailure,
+                        "Failed to save network configuration", osuInfo.getName(LOCALE));
+                mWifiNetworkAdapter.removeSP(homeSP.getFQDN());
+            } else {
+                Set<X509Certificate> rootCerts = OSUSocketFactory.getRootCerts(mKeyStore);
+                X509Certificate remCert = getCert(certs, OSUCertType.Remediation);
+                X509Certificate polCert = getCert(certs, OSUCertType.Policy);
+                if (privateKey != null) {
+                    X509Certificate cltCert = getCert(certs, OSUCertType.Client);
+                    mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP,
+                            privateKey.getEncoded(),
+                            new X509Certificate[]{cltCert});
+                    mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS, cltCert);
+                }
+                boolean usingShared = false;
+                int newCerts = 0;
+                if (remCert != null) {
+                    if (!rootCerts.contains(remCert)) {
+                        if (remCert.equals(polCert)) {
+                            mKeyStore.setCertificateEntry(CERT_SHARED_ALIAS + homeSP.getFQDN(),
+                                    remCert);
+                            usingShared = true;
+                            newCerts++;
+                        } else {
+                            mKeyStore.setCertificateEntry(CERT_REM_ALIAS + homeSP.getFQDN(),
+                                    remCert);
+                            newCerts++;
+                        }
+                    }
+                }
+                if (!usingShared && polCert != null) {
+                    if (!rootCerts.contains(polCert)) {
+                        mKeyStore.setCertificateEntry(CERT_POLICY_ALIAS + homeSP.getFQDN(),
+                                remCert);
+                        newCerts++;
+                    }
+                }
+
+                if (newCerts > 0) {
+                    try (FileOutputStream out = new FileOutputStream(KEYSTORE_FILE)) {
+                        mKeyStore.store(out, null);
+                    }
+                }
+                notifyUser(OSUOperationStatus.ProvisioningSuccess, null, osuInfo.getName(LOCALE));
+                Log.d(TAG, "Provisioning complete.");
+            }
+        } catch (IOException | GeneralSecurityException | SAXException e) {
+            Log.e(TAG, "Failed to provision: " + e, e);
+            notifyUser(OSUOperationStatus.ProvisioningFailure, e.toString(),
+                    osuInfo.getName(LOCALE));
+        }
+    }
+
+    private static X509Certificate getCert(Map<OSUCertType, List<X509Certificate>> certMap,
+                                           OSUCertType certType) {
+        List<X509Certificate> certs = certMap.get(certType);
+        if (certs == null || certs.isEmpty()) {
+            return null;
+        }
+        return certs.iterator().next();
+    }
+
+    public void spDeleted(String fqdn) {
+        int count = deleteCerts(mKeyStore, fqdn,
+                CERT_REM_ALIAS, CERT_POLICY_ALIAS, CERT_SHARED_ALIAS);
+
+        if (count > 0) {
+            try (FileOutputStream out = new FileOutputStream(KEYSTORE_FILE)) {
+                mKeyStore.store(out, null);
+            } catch (IOException | GeneralSecurityException e) {
+                Log.w(TAG, "Failed to remove certs from key store: " + e);
+            }
+        }
+    }
+
+    private static int deleteCerts(KeyStore keyStore, String fqdn, String... prefixes) {
+        int count = 0;
+        for (String prefix : prefixes) {
+            try {
+                String alias = prefix + fqdn;
+                Certificate cert = keyStore.getCertificate(alias);
+                if (cert != null) {
+                    keyStore.deleteEntry(alias);
+                    count++;
+                }
+            } catch (KeyStoreException kse) {
+                /**/
+            }
+        }
+        return count;
+    }
+
+    public void remediationComplete(HomeSP homeSP, Collection<MOData> mods,
+                                    Map<OSUCertType, List<X509Certificate>> certs,
+                                    PrivateKey privateKey)
+            throws IOException, GeneralSecurityException {
+
+        HomeSP altSP = mWifiNetworkAdapter.modifySP(homeSP, mods);
+        X509Certificate caCert = null;
+        List<X509Certificate> clientCerts = null;
+        if (certs != null) {
+            List<X509Certificate> certList = certs.get(OSUCertType.AAA);
+            caCert = certList != null && !certList.isEmpty() ? certList.iterator().next() : null;
+            clientCerts = certs.get(OSUCertType.Client);
+        }
+        if (altSP != null || certs != null) {
+            if (altSP == null) {
+                altSP = homeSP;     // No MO mods, only certs and key
+            }
+            mWifiNetworkAdapter.updateNetwork(altSP, caCert, clientCerts, privateKey);
+        }
+        notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName());
+    }
+
+    protected OMADMAdapter getOMADMAdapter() {
+        return OMADMAdapter.getInstance(mContext);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUMessageType.java b/packages/Osu/src/com/android/hotspot2/osu/OSUMessageType.java
new file mode 100644
index 0000000..8c1b50a
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUMessageType.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public enum OSUMessageType {
+    PostDevData, ExchangeComplete, GetCertificate, Error
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUOperationStatus.java b/packages/Osu/src/com/android/hotspot2/osu/OSUOperationStatus.java
new file mode 100644
index 0000000..ddda89c
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUOperationStatus.java
@@ -0,0 +1,8 @@
+package com.android.hotspot2.osu;
+
+public enum OSUOperationStatus {
+    UserInputComplete,
+    UserInputAborted,
+    ProvisioningSuccess,
+    ProvisioningFailure
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUResponse.java b/packages/Osu/src/com/android/hotspot2/osu/OSUResponse.java
new file mode 100644
index 0000000..1e4398d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUResponse.java
@@ -0,0 +1,97 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class OSUResponse {
+    private static final String SPPVersionAttribute = "sppVersion";
+    private static final String SPPStatusAttribute = "sppStatus";
+    private static final String SPPSessionIDAttribute = "sessionID";
+
+    private final OSUMessageType mMessageType;
+    private final String mVersion;
+    private final String mSessionID;
+    private final OSUStatus mStatus;
+    private final OSUError mError;
+    private final Map<String, String> mAttributes;
+
+    protected OSUResponse(XMLNode root, OSUMessageType messageType, String... attributes)
+            throws OMAException {
+        mMessageType = messageType;
+        String ns = root.getNameSpace() + ":";
+        mVersion = root.getAttributeValue(ns + SPPVersionAttribute);
+        mSessionID = root.getAttributeValue(ns + SPPSessionIDAttribute);
+
+        String status = root.getAttributeValue(ns + SPPStatusAttribute);
+        if (status == null) {
+            throw new OMAException("Missing status");
+        }
+        mStatus = OMAConstants.mapStatus(status);
+
+        if (mVersion == null || mSessionID == null || mStatus == null) {
+            throw new OMAException("Incomplete request: " + root.getAttributes());
+        }
+
+        if (attributes != null) {
+            mAttributes = new HashMap<>();
+            for (String attribute : attributes) {
+                String value = root.getAttributeValue(ns + attribute);
+                if (value == null) {
+                    throw new OMAException("Missing attribute: " + attribute);
+                }
+                mAttributes.put(attribute, value);
+            }
+        } else {
+            mAttributes = null;
+        }
+
+        if (mStatus == OSUStatus.Error) {
+            OSUError error = null;
+            String errorTag = ns + "sppError";
+            for (XMLNode child : root.getChildren()) {
+                if (child.getTag().equals(errorTag)) {
+                    error = OMAConstants.mapError(child.getAttributeValue("errorCode"));
+                    break;
+                }
+            }
+            mError = error;
+        } else {
+            mError = null;
+        }
+    }
+
+    public OSUMessageType getMessageType() {
+        return mMessageType;
+    }
+
+    public String getVersion() {
+        return mVersion;
+    }
+
+    public String getSessionID() {
+        return mSessionID;
+    }
+
+    public OSUStatus getStatus() {
+        return mStatus;
+    }
+
+    public OSUError getError() {
+        return mError;
+    }
+
+    protected Map<String, String> getAttributes() {
+        return mAttributes;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s version '%s', status %s, session-id '%s'%s",
+                mMessageType, mVersion, mStatus, mSessionID, mError != null
+                        ? (" (" + mError + ")") : "");
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java b/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java
new file mode 100644
index 0000000..ef22f643
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java
@@ -0,0 +1,447 @@
+package com.android.hotspot2.osu;
+
+import android.net.Network;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.pps.HomeSP;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PrivateKey;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.PKIXCertPathChecker;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+public class OSUSocketFactory {
+    private static final long ConnectionTimeout = 10000L;
+    private static final long ReconnectWait = 2000L;
+
+    private static final String SecureHTTP = "https";
+    private static final String UnsecureHTTP = "http";
+    private static final String EKU_ID = "2.5.29.37";
+    private static final Set<String> EKU_ID_SET = new HashSet<>(Arrays.asList(EKU_ID));
+    private static final EKUChecker sEKUChecker = new EKUChecker();
+
+    private final Network mNetwork;
+    private final SocketFactory mSocketFactory;
+    private final KeyManager mKeyManager;
+    private final WFATrustManager mTrustManager;
+    private final List<InetSocketAddress> mRemotes;
+
+    public static Set<X509Certificate> buildCertSet() {
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            Set<X509Certificate> set = new HashSet<>();
+            for (String b64 : WFACerts) {
+                ByteArrayInputStream bis = new ByteArrayInputStream(
+                        Base64.decode(b64, Base64.DEFAULT));
+                X509Certificate cert = (X509Certificate) certFactory.generateCertificate(bis);
+                set.add(cert);
+            }
+            return set;
+        } catch (CertificateException ce) {
+            Log.e(OSUManager.TAG, "Cannot build CA cert set");
+            return null;
+        }
+    }
+
+    public static OSUSocketFactory getSocketFactory(KeyStore ks, HomeSP homeSP, int flowType,
+                                                    Network network, URL url, KeyManager km,
+                                                    boolean enforceSecurity)
+            throws GeneralSecurityException, IOException {
+
+        if (enforceSecurity && !url.getProtocol().equalsIgnoreCase(SecureHTTP)) {
+            throw new IOException("Protocol '" + url.getProtocol() + "' is not secure");
+        }
+        return new OSUSocketFactory(ks, homeSP, flowType, network, url, km);
+    }
+
+    private OSUSocketFactory(KeyStore ks, HomeSP homeSP, int flowType, Network network,
+                             URL url, KeyManager km) throws GeneralSecurityException, IOException {
+        mNetwork = network;
+        mKeyManager = km;
+        mTrustManager = new WFATrustManager(ks, homeSP, flowType);
+        int port;
+        switch (url.getProtocol()) {
+            case UnsecureHTTP:
+                mSocketFactory = new DefaultSocketFactory();
+                port = url.getPort() > 0 ? url.getPort() : 80;
+                break;
+            case SecureHTTP:
+                SSLContext tlsContext = SSLContext.getInstance("TLSv1");
+                tlsContext.init(km != null ? new KeyManager[]{km} : null,
+                        new TrustManager[]{mTrustManager}, null);
+                mSocketFactory = tlsContext.getSocketFactory();
+                port = url.getPort() > 0 ? url.getPort() : 443;
+                break;
+            default:
+                throw new IOException("Bad URL: " + url);
+        }
+        if (OSUManager.R2_MOCK && url.getHost().endsWith(".wi-fi.org")) {
+            // !!! Warning: Ruckus hack!
+            mRemotes = new ArrayList<>(1);
+            mRemotes.add(new InetSocketAddress(InetAddress.getByName("10.123.107.107"), port));
+        } else {
+            InetAddress[] remotes = mNetwork.getAllByName(url.getHost());
+            android.util.Log.d(OSUManager.TAG, "'" + url.getHost() + "' resolves to " +
+                    Arrays.toString(remotes));
+            if (remotes == null || remotes.length == 0) {
+                throw new IOException("Failed to look up host from " + url);
+            }
+            mRemotes = new ArrayList<>(remotes.length);
+            for (InetAddress remote : remotes) {
+                mRemotes.add(new InetSocketAddress(remote, port));
+            }
+        }
+        Collections.shuffle(mRemotes);
+    }
+
+    public void reloadKeys(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
+            throws IOException {
+        if (mKeyManager instanceof ClientKeyManager) {
+            ((ClientKeyManager) mKeyManager).reloadKeys(certs, key);
+        }
+    }
+
+    public Socket createSocket() throws IOException {
+        Socket socket = mSocketFactory.createSocket();
+        mNetwork.bindSocket(socket);
+
+        long bail = System.currentTimeMillis() + ConnectionTimeout;
+        boolean success = false;
+
+        while (System.currentTimeMillis() < bail) {
+            for (InetSocketAddress remote : mRemotes) {
+                try {
+                    socket.connect(remote);
+                    Log.d(OSUManager.TAG, "Connection " + socket.getLocalSocketAddress() +
+                            " to " + socket.getRemoteSocketAddress());
+                    success = true;
+                    break;
+                } catch (IOException ioe) {
+                    Log.d(OSUManager.TAG, "Failed to connect to " + remote + ": " + ioe);
+                    socket = mSocketFactory.createSocket();
+                    mNetwork.bindSocket(socket);
+                }
+            }
+            if (success) {
+                break;
+            }
+            Utils.delay(ReconnectWait);
+        }
+        if (!success) {
+            throw new IOException("No available network");
+        }
+        return socket;
+    }
+
+    public X509Certificate getOSUCertificate(URL url) throws GeneralSecurityException {
+        String fqdn = url.getHost();
+        for (X509Certificate certificate : mTrustManager.getTrustChain()) {
+            for (List<?> name : certificate.getSubjectAlternativeNames()) {
+                if (name.size() >= SPVerifier.DNSName &&
+                        name.get(0).getClass() == Integer.class &&
+                        name.get(1).toString().equals(fqdn)) {
+                    return certificate;
+                }
+            }
+        }
+        return null;
+    }
+
+    final class DefaultSocketFactory extends SocketFactory {
+
+        DefaultSocketFactory() {
+        }
+
+        @Override
+        public Socket createSocket() throws IOException {
+            return new Socket();
+        }
+
+        @Override
+        public Socket createSocket(String host, int port) throws IOException {
+            return new Socket(host, port);
+        }
+
+        @Override
+        public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+                throws IOException {
+            return new Socket(host, port, localHost, localPort);
+        }
+
+        @Override
+        public Socket createSocket(InetAddress host, int port) throws IOException {
+            return new Socket(host, port);
+        }
+
+        @Override
+        public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+                                   int localPort) throws IOException {
+            return new Socket(address, port, localAddress, localPort);
+        }
+    }
+
+    private static class WFATrustManager implements X509TrustManager {
+        private final KeyStore mKeyStore;
+        private final HomeSP mHomeSP;
+        private final int mFlowType;
+        private X509Certificate[] mTrustChain;
+
+        private WFATrustManager(KeyStore ks, HomeSP homeSP, int flowType)
+                throws CertificateException {
+            mKeyStore = ks;
+            mHomeSP = homeSP;
+            mFlowType = flowType;
+        }
+
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            // N/A
+        }
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            Log.d("TLSOSU", "Checking " + chain.length + " certs.");
+
+            try {
+                CertPathValidator validator =
+                        CertPathValidator.getInstance(CertPathValidator.getDefaultType());
+                CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+                CertPath path = certFactory.generateCertPath(
+                        Arrays.asList(chain));
+                Set<TrustAnchor> trustAnchors = new HashSet<>();
+                if (mHomeSP == null) {
+                    for (X509Certificate cert : getRootCerts(mKeyStore)) {
+                        trustAnchors.add(new TrustAnchor(cert, null));
+                    }
+                } else {
+                    String prefix = mFlowType == OSUManager.FLOW_REMEDIATION ?
+                            OSUManager.CERT_REM_ALIAS : OSUManager.CERT_POLICY_ALIAS;
+
+                    X509Certificate cert = getCert(mKeyStore, prefix + mHomeSP.getFQDN());
+                    if (cert == null) {
+                        cert = getCert(mKeyStore, OSUManager.CERT_SHARED_ALIAS + mHomeSP.getFQDN());
+                    }
+                    if (cert == null) {
+                        for (X509Certificate root : getRootCerts(mKeyStore)) {
+                            trustAnchors.add(new TrustAnchor(root, null));
+                        }
+                    } else {
+                        trustAnchors.add(new TrustAnchor(cert, null));
+                    }
+                }
+                PKIXParameters params = new PKIXParameters(trustAnchors);
+                params.setRevocationEnabled(false);
+                params.addCertPathChecker(sEKUChecker);
+                validator.validate(path, params);
+                mTrustChain = chain;
+            } catch (GeneralSecurityException gse) {
+                throw new SecurityException(gse);
+            }
+            mTrustChain = chain;
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return null;
+        }
+
+        public X509Certificate[] getTrustChain() {
+            return mTrustChain != null ? mTrustChain : new X509Certificate[0];
+        }
+    }
+
+    private static X509Certificate getCert(KeyStore keyStore, String alias)
+            throws KeyStoreException {
+        Certificate cert = keyStore.getCertificate(alias);
+        if (cert != null && cert instanceof X509Certificate) {
+            return (X509Certificate) cert;
+        }
+        return null;
+    }
+
+    public static Set<X509Certificate> getRootCerts(KeyStore keyStore) throws KeyStoreException {
+        Set<X509Certificate> certSet = new HashSet<>();
+        int index = 0;
+        for (int n = 0; n < 1000; n++) {
+            Certificate cert = keyStore.getCertificate(
+                    String.format("%s%d", OSUManager.CERT_WFA_ALIAS, index));
+            if (cert == null) {
+                break;
+            } else if (cert instanceof X509Certificate) {
+                certSet.add((X509Certificate) cert);
+            }
+            index++;
+        }
+        return certSet;
+    }
+
+    private static class EKUChecker extends PKIXCertPathChecker {
+        @Override
+        public void init(boolean forward) throws CertPathValidatorException {
+
+        }
+
+        @Override
+        public boolean isForwardCheckingSupported() {
+            return true;
+        }
+
+        @Override
+        public Set<String> getSupportedExtensions() {
+            return EKU_ID_SET;
+        }
+
+        @Override
+        public void check(Certificate cert, Collection<String> unresolvedCritExts)
+                throws CertPathValidatorException {
+            Log.d(OSUManager.TAG, "Checking EKU " + unresolvedCritExts);
+            unresolvedCritExts.remove(EKU_ID);
+        }
+    }
+
+    /*
+     *
+      Subject: CN=osu-server.r2-testbed-rks.wi-fi.org, O=Intel Corporation CCG DRD, C=US
+      Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
+      Validity: [From: Wed Jan 28 16:00:00 PST 2015,
+                   To: Sat Jan 28 15:59:59 PST 2017]
+      Issuer: CN="NetworkFX, Inc. Hotspot 2.0 Intermediate CA", OU=OSU CA - 01, O="NetworkFX, Inc.", C=US
+      SerialNumber: [    312af3db 138eae19 1defbce2 e2b88b55]
+    *
+    *
+      Subject: CN="NetworkFX, Inc. Hotspot 2.0 Intermediate CA", OU=OSU CA - 01, O="NetworkFX, Inc.", C=US
+      Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
+      Validity: [From: Tue Nov 19 16:00:00 PST 2013,
+                   To: Sun Nov 19 15:59:59 PST 2023]
+      Issuer: CN=Hotspot 2.0 Trust Root CA - 01, O=WFA Hotspot 2.0, C=US
+      SerialNumber: [    4152b1b0 301495f3 8fa76428 2ef41046]
+     */
+
+    public static final String[] WFACerts = {
+            "MIIFbDCCA1SgAwIBAgIQDLMPcPKGpDPguQmJ3gHttzANBgkqhkiG9w0BAQsFADBQ" +
+                    "MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhvdHNwb3QgMi4wMScwJQYDVQQD" +
+                    "Ex5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0gMDMwHhcNMTMxMjA4MTIwMDAw" +
+                    "WhcNNDMxMjA4MTIwMDAwWjBQMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhv" +
+                    "dHNwb3QgMi4wMScwJQYDVQQDEx5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0g" +
+                    "MDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsdEtReIUbMlO+hR6b" +
+                    "yQk4nGVITv3meYTaDeVwZnQVal8EjHuu4Kd89g8yRYVTv3J1kq9ukE7CDrDehrXK" +
+                    "ym+8VlR7ro0lB/lwRyNk3W7yNccg3AknQ0x5fKVwcFznwD/FYg37owGmhGFtpMTB" +
+                    "cxzreQaLXvLta8YNlJU10ZkfputBpzi9bLPWsLOkIrQw7KH1Wc+Oiy4hUMUbTlSi" +
+                    "cjqacKPR188mVIoxxUoICHyVV1KvMmYZrVdc/b5dbmd0haMHxC0VSqbydXxxS7vv" +
+                    "/lCrC2d5qbKE66PiuBPkhzyU7SI9C8GU/S7akYm1MMSTn5W7lSp2AWRDnf9LQg51" +
+                    "dLvDxJ7t2fruXtSkkqG/cwY1yQI8O+WZYPDThKPcDmNbaxVE9lOizAHXFVsfYrXA" +
+                    "PbbMOkzKehYwaIikmNgcpxtQNw+wikJiZb9N8VwwtwHK71XEFi+n5DGlPa9VDYgB" +
+                    "YkBcxvVo2rbE3i3teQgHm+pWZNP08aFNWwMk9yQkm/SOGdLq1jLbQA9yd7fyR1Ct" +
+                    "W1GLzKi1Ojr/6XiB9/noL3oxP/+gb8OSgcqVfkZp4QLvrGdlKiOI2fE7Bslmzn6l" +
+                    "B3UTpApjab7BQ99rCXzDwt3Xd7IrCtAJNkxi302J7k6hnGlW8S4oPQBElkOtoH9y" +
+                    "XEhp9rNS0lZiuwtFmWW2q50fkQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G" +
+                    "A1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUZw5JLGEXnuvt4FTnhNmbrWRgc2UwDQYJ" +
+                    "KoZIhvcNAQELBQADggIBAFPoGFDyzFg9B9+jJUPGW32omftBhChVcgjllI07RCie" +
+                    "KTMBi47+auuLgiMox3xRyP7/dX7YaUeMXEQ1BMv6nlrsXWv1lH4yu+RNuehPlqRs" +
+                    "fY351mAfPtQ654SBUi0Wg++9iyTOfgF5a9IWEDt4lnSZMvA4vlw8pUCz6zpKXHnA" +
+                    "RXKrpY3bU+2dnrFDKR0XQhmAQdo7UvdsT1elVoFIxHhLpwfzx+kpEhtrXw3nGgt+" +
+                    "M4jNp684XoWpxVGaQ4Vvv00Sm2DQ8jq2sf9F+kRWszZpQOTiMGKZr0lX2CI5cww1" +
+                    "dfmd1BkAjI9cIWLkD8YSeaggZzvYe1o9d7e7lKfdJmjDlSQ0uBiG77keUK4tF2fi" +
+                    "xFTxibtPux56p3GYQ2GdRsBaKjH3A3HMJSKXwIGR+wb1sgz/bBdlyJSylG8hYD//" +
+                    "0Hyo+UrMUszAdszoPhMY+4Ol3QE3QRWzXi+W/NtKeYD2K8xUzjZM10wMdxCfoFOa" +
+                    "8bzzWnxZQlnu880ULUSHIxDPeE+DDZYYOaN1hV2Rh/hrFKvvV+gJj2eXHF5G7y9u" +
+                    "Yg7nHYCCf7Hy8UTIXDtAAeDCQNon1ReN8G+XOqhLQ9TalmnJ5U5ARtC0MdQDht7T" +
+                    "DZpWeEVv+pQHARX9GDV/T85MV2RPJWKqfZ6kK0gvQDkunADdg8IhZAjwMMx3k6B/",
+
+            "MIIFbDCCA1SgAwIBAgIQaAV8NQv/Xdusi4IU+tpUfjANBgkqhkiG9w0BAQsFADBQ" +
+                    "MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhvdHNwb3QgMi4wMScwJQYDVQQD" +
+                    "Ex5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0gMDEwHhcNMTMxMTIwMDAwMDAw" +
+                    "WhcNNDMxMTE5MjM1OTU5WjBQMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhv" +
+                    "dHNwb3QgMi4wMScwJQYDVQQDEx5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0g" +
+                    "MDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/gf4CHxWjr2EcktAZ" +
+                    "pHT4z1yFYZILD3ZVqvzzXBK+YKjWhjsgZ28Z1VwXqu51JvVzwTGDalPf5m7zMcJW" +
+                    "CpPtPBdxxwQ/cBDPK4w+/sCuYYSddlMLzwZ/IgwFike12tKTR7Kk7Nk6ghrYaxCG" +
+                    "R+QEZDVrxITj79vGpgk2otVnMI4d3H9mWt1o6Lx+hVioyBgOvmo2OWHR2uKkbg5h" +
+                    "tktXqmBEtzK+qDqIIUY4WRRZHxlOaF2/EdIIGhXlf+Vlr13aPqOPiDiE08o+GARz" +
+                    "TIp8BrW2boo0+2kpEFUKiqc427vOYEkUdSMfwu4aGOcuOewc8sk6ztquL/JcPROL" +
+                    "VSFSSFR3HKhUto8EJcHEEG9wzcOi1OO/OOSVxjNwiaV/hB9Ed1wvoBhiJ+C+Q8/K" +
+                    "HXmoH/ankXDaB06yjt2Ojemt0nO45qlarRj8tO7zbpghJuJxztur47U7PJta7Zcg" +
+                    "z7kOPJPTAbzmOU2TXt1pXO1hVnSlV+M1rRwe7qivnSMMrTnkX15YWmyK27/tgJeu" +
+                    "muR2YzvPwPtF/m1N0bRKI7FW05NYg3smItFq0E/eyf/orgolcXTZ7zNRyRGnjWNs" +
+                    "/w9SDbdby0uVUfdN4V/5uC4HBmA1rikoBbGZ+nzCtesY4yW8eEwMfguVpNT3ueaU" +
+                    "q30nufeY2VnA3Rv1WH8TaeZU+wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G" +
+                    "A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU+RjGVZbebjpzEPfthaTLqbvXMiEwDQYJ" +
+                    "KoZIhvcNAQELBQADggIBABj3LP1UXVa16HYeXC1+GU1dX/cla1n1bwpIlxRnCZ5/" +
+                    "3I3zGw/nRnsLUTkGf8q3XCgin+jX22kyzzQNrgepn0zqBsmAj+pjUUwWzYQUzphc" +
+                    "Uzmg4PJRWaEaGG3kvD+wJEC0pWvIhe48qcq8FZCCmjbvecEVn5mM0smPzPyUjf/o" +
+                    "fjUMQvVWqug/Ff5HT6kbyDWhC3nD+8IZ5PjyO85OnoBnQkr8WYwr24XJgO2HS2rs" +
+                    "W40CzQe3Kdg7HHyef+/iyLYTBJH7EUJPCHGVQtZ3q0aNqURkutXJ/CxKJYMcNTEB" +
+                    "x+a09EhZ6DOHQDqsdTuAqGh3VyrxhFk+3suNsxoh6XaRK10VslvdNB/1YKfU8DWe" +
+                    "V6XfDH/TR0NIL04exUp3rER8sERulpJGBOnaG6OQKh4bFYDB406+QfusQnvO0aYR" +
+                    "UXJzf01B15HRJgpZsggpIuex0UDcJhTTpkRfTj8L4ayUce2ZRsGn3dBaT9ZMx4o9" +
+                    "E/YsQyOpfw28gM5u+zZt4BJz4gAaRGbp4r4sk5Vm/P1/0EXJ70Du6K9d0HAHtpEv" +
+                    "Y94Ww5W6fpMDdyAKYTXZBgTX3cqtikNkLX/kHH8l4o/XW2sXqU3X7vOYqgeVYoD9" +
+                    "NnhZXYCerH4Se5Lgj8/KhXxRWtcn3XduMdkC6UTApMooA64Vs508173Z3lJn2SeQ",
+
+            "MIIFXTCCA0WgAwIBAgIBATANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJVUzEY" +
+                    "MBYGA1UECgwPV0ZBIEhvdHNwb3QgMi4wMScwJQYDVQQDDB5Ib3RzcG90IDIuMCBU" +
+                    "cnVzdCBSb290IENBIC0gMDIwHhcNMTMxMjAyMjA1NzU3WhcNNDMxMjAyMjA1NTAz" +
+                    "WjBQMQswCQYDVQQGEwJVUzEYMBYGA1UECgwPV0ZBIEhvdHNwb3QgMi4wMScwJQYD" +
+                    "VQQDDB5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0gMDIwggIiMA0GCSqGSIb3" +
+                    "DQEBAQUAA4ICDwAwggIKAoICAQDCSoMqNhtTwbnIsINp6nUhx5UFuq9ZQoTv+KDk" +
+                    "vAajT0di6+cQG3sAVvZLySmJoiBAv3PizYYLOD4eGMrFQRqi7PmSJ83WqNv23ZYF" +
+                    "ryFFJiy/URXc/ALDuB3dgElPt24Mx7n2xDPAh9t82HTmuskpQRrsyg9QPoi5rRRS" +
+                    "Djm5mjFJjKChq99RWcweNV/KGH1sTwcmlDmNMScK16A+BBNiSvmZlsGJgAlP369k" +
+                    "lnNqt6UiDhepcktuKpHmSvNel+c/xqzR0gURfUnXcZhzjzS94Rx5O+CNWL4EGiJq" +
+                    "qKAfk99j/lbD0MWYo7Rh0UKQlXSdohWDiV93hxvvfugej8KUOIb+1wmd1Fi+lwDZ" +
+                    "bR2yg2f0qyxbC/tAV4JJNnuDLFb19leD78x+68eAnlbMi+xMH5lINs15+26s2H5d" +
+                    "lx9kwRDBJq02LuHnen6FLafWjejnnBQ/PuGD0ACvBegSsDKDaCuTAnTNS6MDmQr4" +
+                    "wza08iX360ZN+BbSAnCK1YGa/7J7fhyydwxLJ7s5Eo0b6SUMY87FMc5XmkAk4xxL" +
+                    "MLqS2HMtqsGBI5JQT0SgH0ghE6DjMWArBTZcD+swuzTi1/Cz5+Z9Es8xJ3MPvSZW" +
+                    "pJi6VVB2eVMAqfHOj4ozHoVpvJypIVGRwWBzVRWom76R47utuRK6uKzoLiB1jwE5" +
+                    "vwHpUQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBxjAd" +
+                    "BgNVHQ4EFgQU5C9c1OMsB+/MOwl9OKG2D/XSwrUwDQYJKoZIhvcNAQELBQADggIB" +
+                    "AGULYE/VrnA3K0ptgHrWlQoPfp5wGvScgsmy0wp9qE3b6n/4bLehBKb5w4Y3JVA9" +
+                    "gjxoQ5xE2ssDtULZ3nKnGWmMN3qOBoRZCA6KjKs1860p09tm1ScUsajDJ15Tp1nI" +
+                    "zfR0oP63+2bJx+JXM8fPKOJe245hj2rs1c3JXsGCe+UVrlGsotG+wR0PdrejaXJ8" +
+                    "HbhBQHcbhgjsD1Gb6Egm4YxRKAtcVY3q9EKKWAGhbC1qvCh1iLNKo3FeGgm2r3EG" +
+                    "L4cYJBb2fhSKltjISqCDhYq4tplOIeQSJJyJC8gfW/BnMU39lTjNgnSjjGPLQXGV" +
+                    "+Ulb/CgNMJ3RhRJdBoLcpIm/EeLx6JLq/2Erxy7CxjaSOcD0UKa14+dzLSHVsXft" +
+                    "HZuOy548X8m18KruSZsf5uAT3c7NqlXtr9YgOVUqSJykNAHTGi/BHB1dC2clKvxN" +
+                    "ElfLWWrG9yaAd5TFW0+3wsaDIwRZL584AsFwwAD3KMo1oU/2zRvtm0E+VghsuD/Z" +
+                    "IE1xaVGTPaL7ph/YgC9+0rGHieauT8SXz6Ryp3h0RtYMLFZOMTKM7xjmcbMZDwrO" +
+                    "c+J/XjK9dbiCqlx5/B8P0xWaYYHzvE5/fafiPYzoGyFVUXquu0dFCCQrvjF/y0tC" +
+                    "TPm4hQim3k1F+5NChcbeNggN+kq+VdlSqPhQEuOY+kNv"
+    };
+
+    //private static final Set<TrustAnchor> sTrustAnchors = buildCertSet();
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUStatus.java b/packages/Osu/src/com/android/hotspot2/osu/OSUStatus.java
new file mode 100644
index 0000000..00f0634
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUStatus.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public enum OSUStatus {
+    OK, ProvComplete, RemediationComplete, UpdateComplete, ExchangeComplete, Unknown, Error
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/PostDevDataResponse.java b/packages/Osu/src/com/android/hotspot2/osu/PostDevDataResponse.java
new file mode 100644
index 0000000..12b9997
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/PostDevDataResponse.java
@@ -0,0 +1,49 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+import com.android.hotspot2.osu.commands.OSUCommandData;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class PostDevDataResponse extends OSUResponse {
+    private final List<OSUCommand> mOSUCommands;
+
+    public PostDevDataResponse(XMLNode root) throws OMAException {
+        super(root, OSUMessageType.PostDevData);
+
+        if (getStatus() == OSUStatus.Error) {
+            mOSUCommands = null;
+            return;
+        }
+
+        mOSUCommands = new ArrayList<>();
+        for (XMLNode child : root.getChildren()) {
+            mOSUCommands.add(new OSUCommand(child));
+        }
+    }
+
+    public OSUCommandID getOSUCommand() {
+        return mOSUCommands.size() == 1 ? mOSUCommands.get(0).getOSUCommand() : null;
+    }
+
+    public ExecCommand getExecCommand() {
+        return mOSUCommands.size() == 1 ? mOSUCommands.get(0).getExecCommand() : null;
+    }
+
+    public OSUCommandData getCommandData() {
+        return mOSUCommands.size() == 1 ? mOSUCommands.get(0).getCommandData() : null;
+    }
+
+    public Collection<OSUCommand> getCommands() {
+        return Collections.unmodifiableCollection(mOSUCommands);
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + ", commands " + mOSUCommands;
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/RequestReason.java b/packages/Osu/src/com/android/hotspot2/osu/RequestReason.java
new file mode 100644
index 0000000..db222b4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/RequestReason.java
@@ -0,0 +1,16 @@
+package com.android.hotspot2.osu;
+
+public enum RequestReason {
+    SubRegistration,
+    SubProvisioning,
+    SubRemediation,
+    InputComplete,
+    NoClientCert,
+    CertEnrollmentComplete,
+    CertEnrollmentFailed,
+    SubMetaDataUpdate,
+    PolicyUpdate,
+    NextCommand,
+    MOUpload,
+    Unspecified
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ResponseFactory.java b/packages/Osu/src/com/android/hotspot2/osu/ResponseFactory.java
new file mode 100644
index 0000000..3e236a7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ResponseFactory.java
@@ -0,0 +1,8 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+public interface ResponseFactory {
+    public OSUResponse buildResponse(XMLNode root) throws OMAException;
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/SOAPBuilder.java b/packages/Osu/src/com/android/hotspot2/osu/SOAPBuilder.java
new file mode 100644
index 0000000..e2f91ea
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/SOAPBuilder.java
@@ -0,0 +1,188 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SOAPBuilder {
+    private static final String EnvelopeTag = "s12:Envelope";
+    private static final String BodyTag = "s12:Body";
+
+    private static final Map<String, String> sEnvelopeAttributes = new HashMap<>(2);
+    private static final Map<RequestReason, String> sRequestReasons =
+            new EnumMap<>(RequestReason.class);
+
+    static {
+        sEnvelopeAttributes.put("xmlns:s12", "http://www.w3.org/2003/05/soap-envelope");
+        sEnvelopeAttributes.put("xmlns:spp",
+                "http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp");
+
+        sRequestReasons.put(RequestReason.SubRegistration, "Subscription registration");
+        sRequestReasons.put(RequestReason.SubProvisioning, "Subscription provisioning");
+        sRequestReasons.put(RequestReason.SubRemediation, "Subscription remediation");
+        sRequestReasons.put(RequestReason.InputComplete, "User input completed");
+        sRequestReasons.put(RequestReason.NoClientCert, "No acceptable client certificate");
+        sRequestReasons.put(RequestReason.CertEnrollmentComplete,
+                "Certificate enrollment completed");
+        sRequestReasons.put(RequestReason.CertEnrollmentFailed, "Certificate enrollment failed");
+        sRequestReasons.put(RequestReason.SubMetaDataUpdate, "Subscription metadata update");
+        sRequestReasons.put(RequestReason.PolicyUpdate, "Policy update");
+        sRequestReasons.put(RequestReason.NextCommand, "Retrieve next command");
+        sRequestReasons.put(RequestReason.MOUpload, "MO upload");
+        sRequestReasons.put(RequestReason.Unspecified, "Unspecified");
+    }
+
+    public static String buildPostDevDataResponse(RequestReason reason, String sessionID,
+                                                  String redirURI, MOTree... mos) {
+        XMLNode envelope = buildEnvelope();
+        buildSppPostDevData(envelope.getChildren().get(0), sessionID, reason, redirURI, mos);
+        return envelope.toString();
+    }
+
+    public static String buildUpdateResponse(String sessionID, OSUError error) {
+        XMLNode envelope = buildEnvelope();
+        buildSppUpdateResponse(envelope.getChildren().get(0), sessionID, error);
+        return envelope.toString();
+    }
+
+    private static XMLNode buildEnvelope() {
+        XMLNode envelope = new XMLNode(null, EnvelopeTag, sEnvelopeAttributes);
+        envelope.addChild(new XMLNode(envelope, BodyTag, (Map<String, String>) null));
+        return envelope;
+    }
+
+    private static XMLNode buildSppPostDevData(XMLNode parent, String sessionID,
+                                               RequestReason reason, String redirURI,
+                                               MOTree... mos) {
+        Map<String, String> pddAttributes = new HashMap<>();
+        pddAttributes.put(OMAConstants.TAG_Version, OMAConstants.MOVersion);
+        pddAttributes.put("requestReason", sRequestReasons.get(reason));
+        if (sessionID != null) {
+            pddAttributes.put(OMAConstants.TAG_SessionID, sessionID);
+        }
+        if (redirURI != null) {
+            pddAttributes.put("redirectURI", redirURI);
+        }
+
+        XMLNode pddNode = new XMLNode(parent, OMAConstants.TAG_PostDevData, pddAttributes);
+
+        XMLNode vNode = new XMLNode(pddNode, OMAConstants.TAG_SupportedVersions,
+                (HashMap<String, String>) null);
+        vNode.setText("1.0");
+        pddNode.addChild(vNode);
+
+        XMLNode moNode = new XMLNode(pddNode, OMAConstants.TAG_SupportedMOs,
+                (HashMap<String, String>) null);
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for (String urn : OMAConstants.SupportedMO_URNs) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append(' ');
+            }
+            sb.append(urn);
+        }
+        moNode.setText(sb.toString());
+        pddNode.addChild(moNode);
+
+        if (mos != null) {
+            for (MOTree moTree : mos) {
+                Map<String, String> map = null;
+                if (moTree.getUrn() != null) {
+                    map = new HashMap<>(1);
+                    map.put(OMAConstants.SppMOAttribute, moTree.getUrn());
+                }
+                moNode = new XMLNode(pddNode, OMAConstants.TAG_MOContainer, map);
+                moNode.setText(moTree.toXml());
+                pddNode.addChild(moNode);
+            }
+        }
+
+        parent.addChild(pddNode);
+        return pddNode;
+    }
+
+    private static XMLNode buildSppUpdateResponse(XMLNode parent, String sessionID,
+                                                  OSUError error) {
+        Map<String, String> urAttributes = new HashMap<>();
+        urAttributes.put(OMAConstants.TAG_Version, OMAConstants.MOVersion);
+        if (sessionID != null) {
+            urAttributes.put(OMAConstants.TAG_SessionID, sessionID);
+        }
+        if (error == null) {
+            urAttributes.put(OMAConstants.TAG_Status, OMAConstants.mapStatus(OSUStatus.OK));
+        } else {
+            urAttributes.put(OMAConstants.TAG_Status, OMAConstants.mapStatus(OSUStatus.Error));
+        }
+
+        XMLNode urNode = new XMLNode(parent, OMAConstants.TAG_UpdateResponse, urAttributes);
+
+        if (error != null) {
+            Map<String, String> errorAttributes = new HashMap<>();
+            errorAttributes.put("errorCode", OMAConstants.mapError(error));
+            XMLNode errorNode = new XMLNode(urNode, OMAConstants.TAG_Error, errorAttributes);
+            urNode.addChild(errorNode);
+        }
+
+        parent.addChild(urNode);
+        return urNode;
+    }
+
+    /*
+    <xsd:element name="sppUpdateResponse">
+		<xsd:annotation>
+			<xsd:documentation>SOAP method used by SPP client to confirm installation of MO addition or update.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element ref="sppError" minOccurs="0"/>
+				<xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+			</xsd:sequence>
+			<xsd:attribute ref="sppVersion" use="required"/>
+			<xsd:attribute ref="sppStatus" use="required"/>
+			<xsd:attribute ref="sessionID" use="required"/>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+
+    <xsd:element name="sppError">
+		<xsd:annotation>
+			<xsd:documentation>Error response.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:attribute name="errorCode" use="required">
+				<xsd:simpleType>
+					<xsd:restriction base="xsd:string">
+						<xsd:enumeration value="SPP version not supported"/>
+						<xsd:enumeration value="One or more mandatory MOs not supported"/>
+						<xsd:enumeration value="Credentials cannot be provisioned at this time"/>
+						<xsd:enumeration value="Remediation cannot be completed at this time"/>
+						<xsd:enumeration value="Provisioning cannot be completed at this time"/>
+						<xsd:enumeration value="Continue to use existing certificate"/>
+						<xsd:enumeration value="Cookie invalid"/>
+						<xsd:enumeration value="No corresponding web-browser-connection Session ID"/>
+						<xsd:enumeration value="Permission denied"/>
+						<xsd:enumeration value="Command failed"/>
+						<xsd:enumeration value="MO addition or update failed"/>
+						<xsd:enumeration value="Device full"/>
+						<xsd:enumeration value="Bad management tree URI"/>
+						<xsd:enumeration value="Requested entity too large"/>
+						<xsd:enumeration value="Command not allowed"/>
+						<xsd:enumeration value="Command not executed due to user"/>
+						<xsd:enumeration value="Not found"/>
+						<xsd:enumeration value="Other"/>
+					</xsd:restriction>
+				</xsd:simpleType>
+			</xsd:attribute>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+
+
+     */
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/SOAPParser.java b/packages/Osu/src/com/android/hotspot2/osu/SOAPParser.java
new file mode 100644
index 0000000..b848ba9
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/SOAPParser.java
@@ -0,0 +1,327 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+public class SOAPParser {
+
+    private static final String EnvelopeTag = "envelope";
+    private static final String BodyTag = "body";
+
+    private static final Map<String, ResponseFactory> sResponseMap = new HashMap<>();
+
+    static {
+        sResponseMap.put("spppostdevdataresponse", new ResponseFactory() {
+            @Override
+            public OSUResponse buildResponse(XMLNode root) throws OMAException {
+                return new PostDevDataResponse(root);
+            }
+        });
+        sResponseMap.put("sppexchangecomplete", new ResponseFactory() {
+            @Override
+            public OSUResponse buildResponse(XMLNode root) throws OMAException {
+                return new ExchangeCompleteResponse(root);
+            }
+        });
+        sResponseMap.put("getcertificate", new ResponseFactory() {
+            @Override
+            public OSUResponse buildResponse(XMLNode root) {
+                return null;
+            }
+        });
+        sResponseMap.put("spperror", new ResponseFactory() {
+            @Override
+            public OSUResponse buildResponse(XMLNode root) {
+                return null;
+            }
+        });
+    }
+
+    private final XMLNode mResponseNode;
+
+    public SOAPParser(InputStream in)
+            throws ParserConfigurationException, SAXException, IOException {
+        XMLNode root;
+
+        try {
+            XMLParser parser = new XMLParser(in);
+            root = parser.getRoot();
+        } finally {
+            in.close();
+        }
+
+        String[] nsn = root.getTag().split(":");
+        if (nsn.length > 2) {
+            throw new OMAException("Bad root tag syntax: '" + root.getTag() + "'");
+        } else if (!EnvelopeTag.equalsIgnoreCase(nsn[nsn.length - 1])) {
+            throw new OMAException("Expected envelope: '" + root.getTag() + "'");
+        }
+
+        String bodyTag = nsn.length > 1 ? (nsn[0] + ":" + BodyTag) : BodyTag;
+        XMLNode body = null;
+
+        for (XMLNode child : root.getChildren()) {
+            if (bodyTag.equalsIgnoreCase(child.getTag())) {
+                body = child;
+                break;
+            }
+        }
+
+        if (body == null || body.getChildren().isEmpty()) {
+            throw new OMAException("Missing SOAP body");
+        }
+
+        mResponseNode = body.getSoleChild();
+    }
+
+    public OSUResponse getResponse() throws OMAException {
+        ResponseFactory responseFactory = sResponseMap.get(mResponseNode.getStrippedTag());
+        if (responseFactory == null) {
+            throw new OMAException("Unknown response type: '"
+                    + mResponseNode.getStrippedTag() + "'");
+        }
+        return responseFactory.buildResponse(mResponseNode);
+    }
+
+    public XMLNode getResponseNode() {
+        return mResponseNode;
+    }
+
+
+    /*
+    <xsd:element name="sppPostDevDataResponse">
+		<xsd:annotation>
+			<xsd:documentation>SOAP method response from SPP server.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:choice>
+				<xsd:element ref="sppError"/>
+				<xsd:element name="exec">
+					<xsd:annotation>
+						<xsd:documentation>Receipt of this element by a mobile device causes the following command to be executed.</xsd:documentation>
+					</xsd:annotation>
+					<xsd:complexType>
+						<xsd:choice>
+							<xsd:element name="launchBrowserToURI" type="httpsURIType">
+								<xsd:annotation>
+									<xsd:documentation>When the mobile device receives this command, it launches its default browser to the URI contained in this element.  The URI must use HTTPS as the protocol and must contain an FQDN.</xsd:documentation>
+								</xsd:annotation>
+							</xsd:element>
+							<xsd:element ref="getCertificate"/>
+							<xsd:element name="useClientCertTLS">
+								<xsd:annotation>
+									<xsd:documentation>Command to mobile to re-negotiate the TLS connection using a client certificate of the accepted type or Issuer to authenticate with the Subscription server.</xsd:documentation>
+								</xsd:annotation>
+								<xsd:complexType>
+									<xsd:sequence>
+										<xsd:element name="providerIssuerName" minOccurs="0"
+											maxOccurs="unbounded">
+											<xsd:complexType>
+												<xsd:attribute name="name" type="xsd:string">
+												<xsd:annotation>
+												<xsd:documentation>The issuer name of an acceptable provider-issued certificate.  The text of this element is formatted in accordance with the Issuer Name field in RFC-3280.  This element is present only when acceptProviderCerts is true.</xsd:documentation>
+												</xsd:annotation>
+												</xsd:attribute>
+												<xsd:anyAttribute namespace="##other"/>
+											</xsd:complexType>
+										</xsd:element>
+										<xsd:any namespace="##other" minOccurs="0"
+											maxOccurs="unbounded"/>
+									</xsd:sequence>
+									<xsd:attribute name="acceptMfgCerts" type="xsd:boolean"
+										use="optional" default="false">
+										<xsd:annotation>
+											<xsd:documentation>When this boolean is true, IEEE 802.1ar manufacturing certificates are acceptable for mobile device authentication.</xsd:documentation>
+										</xsd:annotation>
+									</xsd:attribute>
+									<xsd:attribute name="acceptProviderCerts" type="xsd:boolean"
+										use="optional" default="true">
+										<xsd:annotation>
+											<xsd:documentation>When this boolean is true, X509v3 certificates issued by providers identified in the providerIssuerName child element(s) are acceptable for mobile device authentication.</xsd:documentation>
+										</xsd:annotation>
+									</xsd:attribute>
+									<xsd:anyAttribute namespace="##other"/>
+								</xsd:complexType>
+							</xsd:element>
+							<xsd:element name="uploadMO" maxOccurs="unbounded">
+								<xsd:annotation>
+									<xsd:documentation>Command to mobile to upload the MO named in the moURN attribute to the SPP server.</xsd:documentation>
+								</xsd:annotation>
+								<xsd:complexType>
+									<xsd:attribute ref="moURN"/>
+								</xsd:complexType>
+							</xsd:element>
+							<xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0">
+								<xsd:annotation>
+									<xsd:documentation>Element to allow the addition of new commands in the future.</xsd:documentation>
+								</xsd:annotation>
+							</xsd:any>
+						</xsd:choice>
+						<xsd:anyAttribute namespace="##other"/>
+					</xsd:complexType>
+				</xsd:element>
+				<xsd:element name="addMO">
+					<xsd:annotation>
+						<xsd:documentation>This command causes an management object in the mobile devices management tree at the specified location to be added.  If there is already a management object at that location, the object is replaced.</xsd:documentation>
+					</xsd:annotation>
+					<xsd:complexType>
+						<xsd:simpleContent>
+							<xsd:extension base="xsd:string">
+								<xsd:attribute ref="managementTreeURI"/>
+								<xsd:attribute ref="moURN"/>
+							</xsd:extension>
+						</xsd:simpleContent>
+					</xsd:complexType>
+				</xsd:element>
+				<xsd:element maxOccurs="unbounded" name="updateNode">
+					<xsd:annotation>
+						<xsd:documentation>This command causes the update of an interior node and its child nodes (if any) at the location specified in the management tree URI attribute.  The content of this element is the MO node XML.</xsd:documentation>
+					</xsd:annotation>
+					<xsd:complexType>
+						<xsd:simpleContent>
+							<xsd:extension base="xsd:string">
+								<xsd:attribute ref="managementTreeURI"/>
+							</xsd:extension>
+						</xsd:simpleContent>
+					</xsd:complexType>
+				</xsd:element>
+				<xsd:element name="noMOUpdate">
+					<xsd:annotation>
+						<xsd:documentation>This response is used when there is no command to be executed nor update of any MO required.</xsd:documentation>
+					</xsd:annotation>
+				</xsd:element>
+				<xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded">
+					<xsd:annotation>
+						<xsd:documentation>For vendor-specific extensions or future needs.</xsd:documentation>
+					</xsd:annotation>
+				</xsd:any>
+			</xsd:choice>
+			<xsd:attribute ref="sppVersion" use="required"/>
+			<xsd:attribute ref="sppStatus" use="required"/>
+			<xsd:attribute ref="moreCommands" use="optional"/>
+			<xsd:attribute ref="sessionID" use="required"/>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="sppUpdateResponse">
+		<xsd:annotation>
+			<xsd:documentation>SOAP method used by SPP client to confirm installation of MO addition or update.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element ref="sppError" minOccurs="0"/>
+				<xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+			</xsd:sequence>
+			<xsd:attribute ref="sppVersion" use="required"/>
+			<xsd:attribute ref="sppStatus" use="required"/>
+			<xsd:attribute ref="sessionID" use="required"/>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="sppExchangeComplete">
+		<xsd:annotation>
+			<xsd:documentation>SOAP method used by SPP server to end session.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element ref="sppError" minOccurs="0"/>
+				<xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+			</xsd:sequence>
+			<xsd:attribute ref="sppVersion" use="required"/>
+			<xsd:attribute ref="sppStatus" use="required"/>
+			<xsd:attribute ref="sessionID" use="required"/>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="getCertificate">
+		<xsd:annotation>
+			<xsd:documentation>Command to mobile to initiate certificate enrollment or re-enrollment and is a container for metadata to enable enrollment.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element name="enrollmentServerURI" type="httpsURIType">
+					<xsd:annotation>
+						<xsd:documentation>HTTPS URI of the server to be contacted to initiate certificate enrollment.  The URI must contain an FQDN.</xsd:documentation>
+					</xsd:annotation>
+				</xsd:element>
+				<xsd:element name="estUserID" minOccurs="0">
+					<xsd:annotation>
+						<xsd:documentation>Temporary userid used by an EST client to authenticate to the EST server using HTTP Digest authentication.  This element must be used for initial certificate enrollment; its use is optional for certificate re-enrollment.</xsd:documentation>
+					</xsd:annotation>
+					<xsd:simpleType>
+						<xsd:restriction base="xsd:string">
+							<xsd:maxLength value="255"/>
+						</xsd:restriction>
+					</xsd:simpleType>
+				</xsd:element>
+				<xsd:element name="estPassword" minOccurs="0">
+					<xsd:annotation>
+						<xsd:documentation>Temporary password used by an EST client to authenticate to the EST server using HTTP Digest authentication.  This element must be used for initial certificate enrollment; its use is optional for certificate re-enrollment.</xsd:documentation>
+					</xsd:annotation>
+					<xsd:simpleType>
+						<xsd:restriction base="xsd:base64Binary">
+							<xsd:maxLength value="340"/>
+						</xsd:restriction>
+					</xsd:simpleType>
+				</xsd:element>
+				<xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded">
+					<xsd:annotation>
+						<xsd:documentation>For vendor-specific extensions or future needs.</xsd:documentation>
+					</xsd:annotation>
+				</xsd:any>
+			</xsd:sequence>
+			<xsd:attribute name="enrollmentProtocol" use="required">
+				<xsd:simpleType>
+					<xsd:restriction base="xsd:string">
+						<xsd:enumeration value="EST"/>
+						<xsd:enumeration value="Other"/>
+					</xsd:restriction>
+				</xsd:simpleType>
+			</xsd:attribute>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="sppError">
+		<xsd:annotation>
+			<xsd:documentation>Error response.</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:attribute name="errorCode" use="required">
+				<xsd:simpleType>
+					<xsd:restriction base="xsd:string">
+						<xsd:enumeration value="SPP version not supported"/>
+						<xsd:enumeration value="One or more mandatory MOs not supported"/>
+						<xsd:enumeration value="Credentials cannot be provisioned at this time"/>
+						<xsd:enumeration value="Remediation cannot be completed at this time"/>
+						<xsd:enumeration value="Provisioning cannot be completed at this time"/>
+						<xsd:enumeration value="Continue to use existing certificate"/>
+						<xsd:enumeration value="Cookie invalid"/>
+						<xsd:enumeration value="No corresponding web-browser-connection Session ID"/>
+						<xsd:enumeration value="Permission denied"/>
+						<xsd:enumeration value="Command failed"/>
+						<xsd:enumeration value="MO addition or update failed"/>
+						<xsd:enumeration value="Device full"/>
+						<xsd:enumeration value="Bad management tree URI"/>
+						<xsd:enumeration value="Requested entity too large"/>
+						<xsd:enumeration value="Command not allowed"/>
+						<xsd:enumeration value="Command not executed due to user"/>
+						<xsd:enumeration value="Not found"/>
+						<xsd:enumeration value="Other"/>
+					</xsd:restriction>
+				</xsd:simpleType>
+			</xsd:attribute>
+			<xsd:anyAttribute namespace="##other"/>
+		</xsd:complexType>
+	</xsd:element>
+
+     */
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java b/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java
new file mode 100644
index 0000000..c529053
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java
@@ -0,0 +1,329 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.anqp.HSIconFileElement;
+import com.android.anqp.I18Name;
+import com.android.anqp.IconInfo;
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.asn1.Asn1Class;
+import com.android.hotspot2.asn1.Asn1Constructed;
+import com.android.hotspot2.asn1.Asn1Decoder;
+import com.android.hotspot2.asn1.Asn1Integer;
+import com.android.hotspot2.asn1.Asn1Object;
+import com.android.hotspot2.asn1.Asn1Octets;
+import com.android.hotspot2.asn1.Asn1Oid;
+import com.android.hotspot2.asn1.Asn1String;
+import com.android.hotspot2.asn1.OidMappings;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class SPVerifier {
+    public static final int OtherName = 0;
+    public static final int DNSName = 2;
+
+    private final OSUInfo mOSUInfo;
+
+    public SPVerifier(OSUInfo osuInfo) {
+        mOSUInfo = osuInfo;
+    }
+
+    /*
+    SEQUENCE:
+      [Context 0]:
+        SEQUENCE:
+          [Context 0]:                      -- LogotypeData
+            SEQUENCE:
+              SEQUENCE:
+                SEQUENCE:
+                  IA5String='image/png'
+                  SEQUENCE:
+                    SEQUENCE:
+                      SEQUENCE:
+                        OID=2.16.840.1.101.3.4.2.1
+                        NULL
+                      OCTET_STRING= cf aa 74 a8 ad af 85 82 06 c8 f5 b5 bf ee 45 72 8a ee ea bd 47 ab 50 d3 62 0c 92 c1 53 c3 4c 6b
+                  SEQUENCE:
+                    IA5String='http://www.r2-testbed.wi-fi.org/icon_orange_zxx.png'
+                SEQUENCE:
+                  INTEGER=4184
+                  INTEGER=-128
+                  INTEGER=61
+                  [Context 4]= 7a 78 78
+          [Context 0]:                      -- LogotypeData
+            SEQUENCE:
+              SEQUENCE:                     -- LogotypeImage
+                SEQUENCE:                   -- LogoTypeDetails
+                  IA5String='image/png'
+                  SEQUENCE:
+                    SEQUENCE:               -- HashAlgAndValue
+                      SEQUENCE:
+                        OID=2.16.840.1.101.3.4.2.1
+                        NULL
+                      OCTET_STRING= cb 35 5c ba 7a 21 59 df 8e 0a e1 d8 9f a4 81 9e 41 8f af 58 0c 08 d6 28 7f 66 22 98 13 57 95 8d
+                  SEQUENCE:
+                    IA5String='http://www.r2-testbed.wi-fi.org/icon_orange_eng.png'
+                SEQUENCE:                   -- LogotypeImageInfo
+                  INTEGER=11635
+                  INTEGER=-96
+                  INTEGER=76
+                  [Context 4]= 65 6e 67
+     */
+
+    private static class LogoTypeImage {
+        private final String mMimeType;
+        private final List<HashAlgAndValue> mHashes = new ArrayList<>();
+        private final List<String> mURIs = new ArrayList<>();
+        private final int mFileSize;
+        private final int mXsize;
+        private final int mYsize;
+        private final String mLanguage;
+
+        private LogoTypeImage(Asn1Constructed sequence) throws IOException {
+            Iterator<Asn1Object> children = sequence.getChildren().iterator();
+
+            Iterator<Asn1Object> logoTypeDetails =
+                    castObject(children.next(), Asn1Constructed.class).getChildren().iterator();
+            mMimeType = castObject(logoTypeDetails.next(), Asn1String.class).getString();
+
+            Asn1Constructed hashes = castObject(logoTypeDetails.next(), Asn1Constructed.class);
+            for (Asn1Object hash : hashes.getChildren()) {
+                mHashes.add(new HashAlgAndValue(castObject(hash, Asn1Constructed.class)));
+            }
+            Asn1Constructed urls = castObject(logoTypeDetails.next(), Asn1Constructed.class);
+            for (Asn1Object url : urls.getChildren()) {
+                mURIs.add(castObject(url, Asn1String.class).getString());
+            }
+
+            boolean imageInfoSet = false;
+            int fileSize = -1;
+            int xSize = -1;
+            int ySize = -1;
+            String language = null;
+
+            if (children.hasNext()) {
+                Iterator<Asn1Object> imageInfo =
+                        castObject(children.next(), Asn1Constructed.class).getChildren().iterator();
+
+                Asn1Object first = imageInfo.next();
+                if (first.getTag() == 0) {
+                    first = imageInfo.next();   // Ignore optional LogotypeImageType
+                }
+
+                fileSize = (int) castObject(first, Asn1Integer.class).getValue();
+                xSize = (int) castObject(imageInfo.next(), Asn1Integer.class).getValue();
+                ySize = (int) castObject(imageInfo.next(), Asn1Integer.class).getValue();
+                imageInfoSet = true;
+
+                if (imageInfo.hasNext()) {
+                    Asn1Object next = imageInfo.next();
+                    if (next.getTag() != 4) {
+                        next = imageInfo.hasNext() ? imageInfo.next() : null;   // Skip resolution
+                    }
+                    if (next != null && next.getTag() == 4) {
+                        language = new String(castObject(next, Asn1Octets.class).getOctets(),
+                                StandardCharsets.US_ASCII);
+                    }
+                }
+            }
+
+            if (imageInfoSet) {
+                mFileSize = complement(fileSize);
+                mXsize = complement(xSize);
+                mYsize = complement(ySize);
+            } else {
+                mFileSize = mXsize = mYsize = -1;
+            }
+            mLanguage = language;
+        }
+
+        private boolean verify(OSUInfo osuInfo) throws GeneralSecurityException, IOException {
+            IconInfo iconInfo = osuInfo.getIconInfo();
+            HSIconFileElement iconData = osuInfo.getIconFileElement();
+            if (!iconInfo.getIconType().equals(mMimeType) ||
+                    !iconInfo.getLanguage().equals(mLanguage) ||
+                    iconData.getIconData().length != mFileSize) {
+                return false;
+            }
+            for (HashAlgAndValue hash : mHashes) {
+                if (hash.getJCEName() != null) {
+                    MessageDigest digest = MessageDigest.getInstance(hash.getJCEName());
+                    byte[] computed = digest.digest(iconData.getIconData());
+                    if (!Arrays.equals(computed, hash.getHash())) {
+                        throw new IOException("Icon hash mismatch");
+                    } else {
+                        Log.d(OSUManager.TAG, "Icon verified with " + hash.getJCEName());
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "LogoTypeImage{" +
+                    "MimeType='" + mMimeType + '\'' +
+                    ", hashes=" + mHashes +
+                    ", URIs=" + mURIs +
+                    ", fileSize=" + mFileSize +
+                    ", xSize=" + mXsize +
+                    ", ySize=" + mYsize +
+                    ", language='" + mLanguage + '\'' +
+                    '}';
+        }
+    }
+
+    private static class HashAlgAndValue {
+        private final String mJCEName;
+        private final byte[] mHash;
+
+        private HashAlgAndValue(Asn1Constructed sequence) throws IOException {
+            if (sequence.getChildren().size() != 2) {
+                throw new IOException("Bad HashAlgAndValue");
+            }
+            Iterator<Asn1Object> children = sequence.getChildren().iterator();
+            mJCEName = OidMappings.getJCEName(getFirstInner(children.next(), Asn1Oid.class));
+            mHash = castObject(children.next(), Asn1Octets.class).getOctets();
+        }
+
+        public String getJCEName() {
+            return mJCEName;
+        }
+
+        public byte[] getHash() {
+            return mHash;
+        }
+
+        @Override
+        public String toString() {
+            return "HashAlgAndValue{" +
+                    "JCEName='" + mJCEName + '\'' +
+                    ", hash=" + Utils.toHex(mHash) +
+                    '}';
+        }
+    }
+
+    private static int complement(int value) {
+        return value >= 0 ? value : (~value) + 1;
+    }
+
+    private static <T extends Asn1Object> T castObject(Asn1Object object, Class<T> klass)
+            throws IOException {
+        if (object.getClass() != klass) {
+            throw new IOException("Object is an " + object.getClass().getSimpleName() +
+                    " expected an " + klass.getSimpleName());
+        }
+        return klass.cast(object);
+    }
+
+    private static <T extends Asn1Object> T getFirstInner(Asn1Object container, Class<T> klass)
+            throws IOException {
+        if (container.getClass() != Asn1Constructed.class) {
+            throw new IOException("Not a container");
+        }
+        Iterator<Asn1Object> children = container.getChildren().iterator();
+        if (!children.hasNext()) {
+            throw new IOException("No content");
+        }
+        return castObject(children.next(), klass);
+    }
+
+    public void verify(X509Certificate osuCert) throws IOException, GeneralSecurityException {
+        if (osuCert == null) {
+            throw new IOException("No OSU cert found");
+        }
+
+        checkName(castObject(getExtension(osuCert, OidMappings.IdCeSubjectAltName),
+                Asn1Constructed.class));
+
+        List<LogoTypeImage> logos = getImageData(getExtension(osuCert, OidMappings.IdPeLogotype));
+        Log.d(OSUManager.TAG, "Logos: " + logos);
+        for (LogoTypeImage logoTypeImage : logos) {
+            if (logoTypeImage.verify(mOSUInfo)) {
+                return;
+            }
+        }
+        throw new IOException("Failed to match icon against any cert logo");
+    }
+
+    private static List<LogoTypeImage> getImageData(Asn1Object logoExtension) throws IOException {
+        Asn1Constructed logo = castObject(logoExtension, Asn1Constructed.class);
+        Asn1Constructed communityLogo = castObject(logo.getChildren().iterator().next(),
+                Asn1Constructed.class);
+        if (communityLogo.getTag() != 0) {
+            throw new IOException("Expected tag [0] for communityLogos");
+        }
+
+        List<LogoTypeImage> images = new ArrayList<>();
+        Asn1Constructed communityLogoSeq = castObject(communityLogo.getChildren().iterator().next(),
+                Asn1Constructed.class);
+        for (Asn1Object logoTypeData : communityLogoSeq.getChildren()) {
+            if (logoTypeData.getTag() != 0) {
+                throw new IOException("Expected tag [0] for LogotypeData");
+            }
+            for (Asn1Object logoTypeImage : castObject(logoTypeData.getChildren().iterator().next(),
+                    Asn1Constructed.class).getChildren()) {
+                // only read the image SEQUENCE and skip any audio [1] tags
+                if (logoTypeImage.getAsn1Class() == Asn1Class.Universal) {
+                    images.add(new LogoTypeImage(castObject(logoTypeImage, Asn1Constructed.class)));
+                }
+            }
+        }
+        return images;
+    }
+
+    private void checkName(Asn1Constructed altName) throws IOException {
+        Map<String, I18Name> friendlyNames = new HashMap<>();
+        for (Asn1Object name : altName.getChildren()) {
+            if (name.getAsn1Class() == Asn1Class.Context && name.getTag() == OtherName) {
+                Asn1Constructed otherName = (Asn1Constructed) name;
+                Iterator<Asn1Object> children = otherName.getChildren().iterator();
+                if (children.hasNext()) {
+                    Asn1Object oidObject = children.next();
+                    if (OidMappings.sIdWfaHotspotFriendlyName.equals(oidObject) &&
+                            children.hasNext()) {
+                        Asn1Constructed value = castObject(children.next(), Asn1Constructed.class);
+                        String text = castObject(value.getChildren().iterator().next(),
+                                Asn1String.class).getString();
+                        I18Name friendlyName = new I18Name(text);
+                        friendlyNames.put(friendlyName.getLanguage(), friendlyName);
+                    }
+                }
+            }
+        }
+        Log.d(OSUManager.TAG, "Friendly names: " + friendlyNames.values());
+        for (I18Name osuName : mOSUInfo.getOSUProvider().getNames()) {
+            I18Name friendlyName = friendlyNames.get(osuName.getLanguage());
+            if (!osuName.equals(friendlyName)) {
+                throw new IOException("Friendly name '" + osuName + " not in certificate");
+            }
+        }
+    }
+
+    private static Asn1Object getExtension(X509Certificate certificate, String extension)
+            throws GeneralSecurityException, IOException {
+        byte[] data = certificate.getExtensionValue(extension);
+        if (data == null) {
+            return null;
+        }
+        Asn1Octets octetString = (Asn1Octets) Asn1Decoder.decode(ByteBuffer.wrap(data)).
+                iterator().next();
+        Asn1Constructed sequence = castObject(Asn1Decoder.decode(
+                        ByteBuffer.wrap(octetString.getOctets())).iterator().next(),
+                Asn1Constructed.class);
+        Log.d(OSUManager.TAG, "Extension " + extension + ": " + sequence);
+        return sequence;
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/UserInputListener.java b/packages/Osu/src/com/android/hotspot2/osu/UserInputListener.java
new file mode 100644
index 0000000..ca30e3d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/UserInputListener.java
@@ -0,0 +1,46 @@
+package com.android.hotspot2.osu;
+
+import android.net.Network;
+
+import java.net.URL;
+
+public interface UserInputListener {
+    /**
+     * Launch an appropriate application to handle user input and HTTP exchanges to the target
+     * URL. Under normal circumstances this implies that a web-browser is started and pointed at
+     * the target URL from which it is supposed to perform an initial HTTP GET operation.
+     * This call must not block beyond the time it takes to launch the user agent, i.e. must return
+     * well before the HTTP exchange terminates.
+     * @param target A fully encoded URL to which to send an initial HTTP GET and then handle
+     *               subsequent HTTP exchanges.
+     * @param endRedirect A URL to which the user agent will be redirected upon completion of
+     *                    the HTTP exchange. This parameter is for informational purposes only
+     *                    as the redirect to the URL is the responsibility of the remote server.
+     */
+    public void requestUserInput(URL target, Network network, URL endRedirect);
+
+    /**
+     * Notification that status of the OSU operation has changed. The implementation may choose to
+     * return a string that will be passed to the user agent. Please note that the string is
+     * passed as the payload of (the redirect) HTTP connection to the agent and must be formatted
+     * appropriately (e.g. as well formed HTML).
+     * Returning a null string on the initial status update of UserInputComplete or UserInputAborted
+     * will cause the local "redirect" web-server to terminate and any further strings returned will
+     * be ignored.
+     * If programmatic termination of the user agent is desired, it should be initiated from within
+     * the implementation of this method.
+     * @param status
+     * @param message
+     * @return
+     */
+    public String operationStatus(String spIdentity, OSUOperationStatus status, String message);
+
+    /**
+     * Notify the user that a de-authentication event is imminent.
+     * @param ess set to indicate that the de-authentication is for an ESS instead of a BSS
+     * @param delay delay the number of seconds that the user will have to wait before
+     *              reassociating with the BSS or ESS.
+     * @param url a URL to which to redirect the user
+     */
+    public void deAuthNotification(String spIdentity, boolean ess, int delay, URL url);
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/WiFiKeyManager.java b/packages/Osu/src/com/android/hotspot2/osu/WiFiKeyManager.java
new file mode 100644
index 0000000..54a3c4d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/WiFiKeyManager.java
@@ -0,0 +1,172 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.X509KeyManager;
+import javax.security.auth.x500.X500Principal;
+
+public class WiFiKeyManager implements X509KeyManager {
+    private final KeyStore mKeyStore;
+    private final Map<X500Principal, String[]> mAliases = new HashMap<>();
+
+    public WiFiKeyManager(KeyStore keyStore) throws IOException {
+        mKeyStore = keyStore;
+    }
+
+    public void enableClientAuth(List<String> issuerNames) throws GeneralSecurityException,
+            IOException {
+
+        Set<X500Principal> acceptedIssuers = new HashSet<>();
+        for (String issuerName : issuerNames) {
+            acceptedIssuers.add(new X500Principal(issuerName));
+        }
+
+        Enumeration<String> aliases = mKeyStore.aliases();
+        while (aliases.hasMoreElements()) {
+            String alias = aliases.nextElement();
+            Certificate cert = mKeyStore.getCertificate(alias);
+            if ((cert instanceof X509Certificate) && mKeyStore.getKey(alias, null) != null) {
+                X509Certificate x509Certificate = (X509Certificate) cert;
+                X500Principal issuer = x509Certificate.getIssuerX500Principal();
+                if (acceptedIssuers.contains(issuer)) {
+                    mAliases.put(issuer, new String[]{alias, cert.getPublicKey().getAlgorithm()});
+                }
+            }
+        }
+
+        if (mAliases.isEmpty()) {
+            throw new IOException("No aliases match requested issuers: " + issuerNames);
+        }
+    }
+
+    private static class AliasEntry implements Comparable<AliasEntry> {
+        private final int mPreference;
+        private final String mAlias;
+
+        private AliasEntry(int preference, String alias) {
+            mPreference = preference;
+            mAlias = alias;
+        }
+
+        public int getPreference() {
+            return mPreference;
+        }
+
+        public String getAlias() {
+            return mAlias;
+        }
+
+        @Override
+        public int compareTo(AliasEntry other) {
+            return Integer.compare(getPreference(), other.getPreference());
+        }
+    }
+
+    @Override
+    public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+
+        Map<String, Integer> keyPrefs = new HashMap<>(keyTypes.length);
+        int pref = 0;
+        for (String keyType : keyTypes) {
+            keyPrefs.put(keyType, pref++);
+        }
+
+        List<AliasEntry> aliases = new ArrayList<>();
+        if (issuers != null) {
+            for (Principal issuer : issuers) {
+                if (issuer instanceof X500Principal) {
+                    String[] aliasAndKey = mAliases.get((X500Principal) issuer);
+                    if (aliasAndKey != null) {
+                        Integer preference = keyPrefs.get(aliasAndKey[1]);
+                        if (preference != null) {
+                            aliases.add(new AliasEntry(preference, aliasAndKey[0]));
+                        }
+                    }
+                }
+            }
+        } else {
+            for (String[] aliasAndKey : mAliases.values()) {
+                Integer preference = keyPrefs.get(aliasAndKey[1]);
+                if (preference != null) {
+                    aliases.add(new AliasEntry(preference, aliasAndKey[0]));
+                }
+            }
+        }
+        Collections.sort(aliases);
+        return aliases.isEmpty() ? null : aliases.get(0).getAlias();
+    }
+
+    @Override
+    public String[] getClientAliases(String keyType, Principal[] issuers) {
+        List<String> aliases = new ArrayList<>();
+        if (issuers != null) {
+            for (Principal issuer : issuers) {
+                if (issuer instanceof X500Principal) {
+                    String[] aliasAndKey = mAliases.get((X500Principal) issuer);
+                    if (aliasAndKey != null) {
+                        aliases.add(aliasAndKey[0]);
+                    }
+                }
+            }
+        } else {
+            for (String[] aliasAndKey : mAliases.values()) {
+                aliases.add(aliasAndKey[0]);
+            }
+        }
+        return aliases.isEmpty() ? null : aliases.toArray(new String[aliases.size()]);
+    }
+
+    @Override
+    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getServerAliases(String keyType, Principal[] issuers) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain(String alias) {
+        try {
+            List<X509Certificate> certs = new ArrayList<>();
+            for (Certificate certificate : mKeyStore.getCertificateChain(alias)) {
+                if (certificate instanceof X509Certificate) {
+                    certs.add((X509Certificate) certificate);
+                }
+            }
+            return certs.toArray(new X509Certificate[certs.size()]);
+        } catch (KeyStoreException kse) {
+            Log.w(OSUManager.TAG, "Failed to retrieve certificates: " + kse);
+            return null;
+        }
+    }
+
+    @Override
+    public PrivateKey getPrivateKey(String alias) {
+        try {
+            return (PrivateKey) mKeyStore.getKey(alias, null);
+        } catch (GeneralSecurityException gse) {
+            Log.w(OSUManager.TAG, "Failed to retrieve private key: " + gse);
+            return null;
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/XMLParser.java b/packages/Osu/src/com/android/hotspot2/osu/XMLParser.java
new file mode 100644
index 0000000..b23e555
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/XMLParser.java
@@ -0,0 +1,71 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.XMLNode;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+public class XMLParser extends DefaultHandler {
+    private final SAXParser mParser;
+    private final InputSource mInputSource;
+
+    private XMLNode mRoot;
+    private XMLNode mCurrent;
+
+    public XMLParser(InputStream in) throws ParserConfigurationException, SAXException {
+        mParser = SAXParserFactory.newInstance().newSAXParser();
+        mInputSource = new InputSource(new BufferedReader(
+                new InputStreamReader(in, StandardCharsets.UTF_8)));
+    }
+
+    public XMLNode getRoot() throws SAXException, IOException {
+        mParser.parse(mInputSource, this);
+        return mRoot;
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes)
+            throws SAXException {
+        XMLNode parent = mCurrent;
+
+        mCurrent = new XMLNode(mCurrent, qName, attributes);
+        //System.out.println("Added " + mCurrent.getTag() + ", atts " + mCurrent.getAttributes());
+
+        if (mRoot == null)
+            mRoot = mCurrent;
+        else
+            parent.addChild(mCurrent);
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        if (!qName.equals(mCurrent.getTag()))
+            throw new SAXException("End tag '" + qName + "' doesn't match current node: " +
+                    mCurrent);
+
+        try {
+            mCurrent.close();
+        } catch (IOException ioe) {
+            throw new SAXException("Failed to close element", ioe);
+        }
+
+        mCurrent = mCurrent.getParent();
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        mCurrent.addText(ch, start, length);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/BrowserURI.java b/packages/Osu/src/com/android/hotspot2/osu/commands/BrowserURI.java
new file mode 100644
index 0000000..137dbc9
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/BrowserURI.java
@@ -0,0 +1,32 @@
+package com.android.hotspot2.osu.commands;
+
+import com.android.hotspot2.omadm.XMLNode;
+
+/*
+    <spp:sppPostDevDataResponse xmlns:spp="http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp"
+                                spp:sessionID="D74A7B03005645DAA516191DEE77B94F" spp:sppStatus="OK"
+                                spp:sppVersion="1.0">
+        <spp:exec>
+            <spp:launchBrowserToURI>
+                https://subscription-server.r2-testbed-rks.wi-fi.org:8443/web/ruckuswireles/home/-/onlinesignup/subscriberDetails?Credentials=USERNAME_PASSWORD&amp;SessionID=D74A7B03005645DAA516191DEE77B94F&amp;RedirectURI=http://127.0.0.1:12345/index.htm&amp;UpdateMethod=SPP-ClientInitiated
+            </spp:launchBrowserToURI>
+        </spp:exec>
+    </spp:sppPostDevDataResponse>
+ */
+
+public class BrowserURI implements OSUCommandData {
+    private final String mURI;
+
+    public BrowserURI(XMLNode commandNode) {
+        mURI = commandNode.getText();
+    }
+
+    public String getURI() {
+        return mURI;
+    }
+
+    @Override
+    public String toString() {
+        return "URI: " + mURI;
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/ClientCertInfo.java b/packages/Osu/src/com/android/hotspot2/osu/commands/ClientCertInfo.java
new file mode 100644
index 0000000..f877353
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/ClientCertInfo.java
@@ -0,0 +1,94 @@
+package com.android.hotspot2.osu.commands;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+<xsd:element name="useClientCertTLS">
+    <xsd:annotation>
+        <xsd:documentation>Command to mobile to re-negotiate the TLS connection using a client certificate of the accepted type or Issuer to authenticate with the Subscription server.</xsd:documentation>
+    </xsd:annotation>
+    <xsd:complexType>
+        <xsd:sequence>
+            <xsd:element name="providerIssuerName" minOccurs="0"
+                maxOccurs="unbounded">
+                <xsd:complexType>
+                    <xsd:attribute name="name" type="xsd:string">
+                    <xsd:annotation>
+                    <xsd:documentation>The issuer name of an acceptable provider-issued certificate.  The text of this element is formatted in accordance with the Issuer Name field in RFC-3280.  This element is present only when acceptProviderCerts is true.</xsd:documentation>
+                    </xsd:annotation>
+                    </xsd:attribute>
+                    <xsd:anyAttribute namespace="##other"/>
+                </xsd:complexType>
+            </xsd:element>
+            <xsd:any namespace="##other" minOccurs="0"
+                maxOccurs="unbounded"/>
+        </xsd:sequence>
+        <xsd:attribute name="acceptMfgCerts" type="xsd:boolean"
+            use="optional" default="false">
+            <xsd:annotation>
+                <xsd:documentation>When this boolean is true, IEEE 802.1ar manufacturing certificates are acceptable for mobile device authentication.</xsd:documentation>
+            </xsd:annotation>
+        </xsd:attribute>
+        <xsd:attribute name="acceptProviderCerts" type="xsd:boolean"
+            use="optional" default="true">
+            <xsd:annotation>
+                <xsd:documentation>When this boolean is true, X509v3 certificates issued by providers identified in the providerIssuerName child element(s) are acceptable for mobile device authentication.</xsd:documentation>
+            </xsd:annotation>
+        </xsd:attribute>
+        <xsd:anyAttribute namespace="##other"/>
+    </xsd:complexType>
+</xsd:element>
+ */
+
+public class ClientCertInfo implements OSUCommandData {
+    private final boolean mAcceptMfgCerts;
+    private final boolean mAcceptProviderCerts;
+    /*
+     * The issuer name of an acceptable provider-issued certificate.
+     * The text of this element is formatted in accordance with the Issuer Name field in RFC-3280.
+     * This element is present only when acceptProviderCerts is true.
+     */
+    private final List<String> mIssuerNames;
+
+    public ClientCertInfo(XMLNode commandNode) throws OMAException {
+        mAcceptMfgCerts = Boolean.parseBoolean(commandNode.getAttributeValue("acceptMfgCerts"));
+        mAcceptProviderCerts =
+                Boolean.parseBoolean(commandNode.getAttributeValue("acceptProviderCerts"));
+
+        if (mAcceptMfgCerts) {
+            mIssuerNames = new ArrayList<>();
+            for (XMLNode node : commandNode.getChildren()) {
+                if (node.getStrippedTag().equals("providerIssuerName")) {
+                    mIssuerNames.add(node.getAttributeValue("name"));
+                }
+            }
+        } else {
+            mIssuerNames = null;
+        }
+    }
+
+    public boolean doesAcceptMfgCerts() {
+        return mAcceptMfgCerts;
+    }
+
+    public boolean doesAcceptProviderCerts() {
+        return mAcceptProviderCerts;
+    }
+
+    public List<String> getIssuerNames() {
+        return mIssuerNames;
+    }
+
+    @Override
+    public String toString() {
+        return "ClientCertInfo{" +
+                "acceptMfgCerts=" + mAcceptMfgCerts +
+                ", acceptProviderCerts=" + mAcceptProviderCerts +
+                ", issuerNames=" + mIssuerNames +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/GetCertData.java b/packages/Osu/src/com/android/hotspot2/osu/commands/GetCertData.java
new file mode 100644
index 0000000..60a73fb
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/GetCertData.java
@@ -0,0 +1,75 @@
+package com.android.hotspot2.osu.commands;
+
+import android.util.Base64;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/*
+    <env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
+        <env:Header/>
+        <env:Body>
+            <spp:sppPostDevDataResponse xmlns:spp="http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp"
+                                        spp:sessionID="A40103ACEDE94C45BA127A41239BD60F" spp:sppStatus="OK"
+                                        spp:sppVersion="1.0">
+                <spp:exec>
+                    <spp:getCertificate enrollmentProtocol="EST">
+                        <spp:enrollmentServerURI>https://osu-server.r2-testbed-rks.wi-fi.org:9446/.well-known/est
+                        </spp:enrollmentServerURI>
+                        <spp:estUserID>a88c4830-aafd-420b-b790-c08f457a0fa3</spp:estUserID>
+                        <spp:estPassword>cnVja3VzMTIzNA==</spp:estPassword>
+                    </spp:getCertificate>
+                </spp:exec>
+            </spp:sppPostDevDataResponse>
+        </env:Body>
+    </env:Envelope>
+ */
+
+public class GetCertData implements OSUCommandData {
+    private final String mProtocol;
+    private final String mServer;
+    private final String mUserName;
+    private final byte[] mPassword;
+
+    public GetCertData(XMLNode commandNode) throws OMAException {
+        mProtocol = commandNode.getAttributeValue("enrollmentProtocol");
+
+        Map<String, String> values = new HashMap<>(3);
+        for (XMLNode node : commandNode.getChildren()) {
+            values.put(node.getStrippedTag(), node.getText());
+        }
+
+        mServer = values.get("enrollmentserveruri");
+        mUserName = values.get("estuserid");
+        mPassword = Base64.decode(values.get("estpassword"), Base64.DEFAULT);
+    }
+
+    public String getProtocol() {
+        return mProtocol;
+    }
+
+    public String getServer() {
+        return mServer;
+    }
+
+    public String getUserName() {
+        return mUserName;
+    }
+
+    public byte[] getPassword() {
+        return mPassword;
+    }
+
+    @Override
+    public String toString() {
+        return "GetCertData " +
+                "protocol='" + mProtocol + '\'' +
+                ", server='" + mServer + '\'' +
+                ", userName='" + mUserName + '\'' +
+                ", password='" + new String(mPassword, StandardCharsets.ISO_8859_1) + '\'';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/MOData.java b/packages/Osu/src/com/android/hotspot2/osu/commands/MOData.java
new file mode 100644
index 0000000..758c0cb
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/MOData.java
@@ -0,0 +1,51 @@
+package com.android.hotspot2.osu.commands;
+
+import android.net.wifi.PasspointManagementObjectDefinition;
+
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMAParser;
+import com.android.hotspot2.omadm.XMLNode;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+public class MOData implements OSUCommandData {
+    private final String mBaseURI;
+    private final String mURN;
+    private final MOTree mMOTree;
+
+    public MOData(XMLNode root) {
+        mBaseURI = root.getAttributeValue("spp:managementTreeURI");
+        mURN = root.getAttributeValue("spp:moURN");
+        mMOTree = root.getMOTree();
+    }
+
+    public MOData(PasspointManagementObjectDefinition moDef) throws IOException, SAXException {
+        mBaseURI = ""; //moDef.getmBaseUri();
+        mURN = ""; // moDef.getmUrn();
+        /*
+        OMAParser omaParser = new OMAParser();
+        mMOTree = omaParser.parse(moDef.getmMoTree(), OMAConstants.PPS_URN);
+        */
+        mMOTree = null;
+    }
+
+    public String getBaseURI() {
+        return mBaseURI;
+    }
+
+    public String getURN() {
+        return mURN;
+    }
+
+    public MOTree getMOTree() {
+        return mMOTree;
+    }
+
+    @Override
+    public String toString() {
+        return "Base URI: " + mBaseURI + ", MO: " + mMOTree;
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/MOURN.java b/packages/Osu/src/com/android/hotspot2/osu/commands/MOURN.java
new file mode 100644
index 0000000..46394ef
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/MOURN.java
@@ -0,0 +1,33 @@
+package com.android.hotspot2.osu.commands;
+
+/*
+<xsd:element name="uploadMO" maxOccurs="unbounded">
+    <xsd:annotation>
+        <xsd:documentation>Command to mobile to upload the MO named in the moURN attribute to the SPP server.</xsd:documentation>
+    </xsd:annotation>
+    <xsd:complexType>
+        <xsd:attribute ref="moURN"/>
+    </xsd:complexType>
+</xsd:element>
+ */
+
+import com.android.hotspot2.omadm.XMLNode;
+
+public class MOURN implements OSUCommandData {
+    private final String mURN;
+
+    public MOURN(XMLNode root) {
+        mURN = root.getAttributeValue("spp:moURN");
+    }
+
+    public String getURN() {
+        return mURN;
+    }
+
+    @Override
+    public String toString() {
+        return "MOURN{" +
+                "URN='" + mURN + '\'' +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/OSUCommandData.java b/packages/Osu/src/com/android/hotspot2/osu/commands/OSUCommandData.java
new file mode 100644
index 0000000..06f81bf
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/OSUCommandData.java
@@ -0,0 +1,7 @@
+package com.android.hotspot2.osu.commands;
+
+/**
+ * Marker interface
+ */
+public interface OSUCommandData {
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java b/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java
new file mode 100644
index 0000000..ce26afd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java
@@ -0,0 +1,202 @@
+package com.android.hotspot2.osu.service;
+
+import android.util.Log;
+
+import com.android.hotspot2.osu.OSUManager;
+import com.android.hotspot2.osu.OSUOperationStatus;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+
+public class RedirectListener extends Thread {
+    private static final long ThreadTimeout = 3000L;
+    private static final long UserTimeout = 3600000L;
+    private static final int MaxRetry = 5;
+    private static final String TAG = "OSULSN";
+
+    private static final String HTTPResponseHeader =
+            "HTTP/1.1 304 Not Modified\r\n" +
+                    "Server: dummy\r\n" +
+                    "Keep-Alive: timeout=500, max=5\r\n\r\n";
+
+    private static final String GoodBye =
+            "<html>" +
+                    "<head><title>Goodbye</title></head>" +
+                    "<body>" +
+                    "<h3>Killing browser...</h3>" +
+                    "</body>" +
+                    "</html>\r\n";
+
+    private final OSUManager mOSUManager;
+    private final String mSpName;
+    private final ServerSocket mServerSocket;
+    private final String mPath;
+    private final URL mURL;
+    private final Object mLock = new Object();
+
+    private boolean mListening;
+    private OSUOperationStatus mUserStatus;
+    private volatile boolean mAborted;
+
+    public RedirectListener(OSUManager osuManager, String spName) throws IOException {
+        mOSUManager = osuManager;
+        mSpName = spName;
+        mServerSocket = new ServerSocket(0, 5, InetAddress.getLocalHost());
+        Random rnd = new Random(System.currentTimeMillis());
+        mPath = "rnd" + Integer.toString(Math.abs(rnd.nextInt()), Character.MAX_RADIX);
+        mURL = new URL("http", mServerSocket.getInetAddress().getHostAddress(),
+                mServerSocket.getLocalPort(), mPath);
+
+        Log.d(TAG, "Redirect URL: " + mURL);
+        setName("HS20-Redirect-Listener");
+        setDaemon(true);
+    }
+
+    public void startService() throws IOException {
+        start();
+        synchronized (mLock) {
+            long bail = System.currentTimeMillis() + ThreadTimeout;
+            long remainder = ThreadTimeout;
+            while (remainder > 0 && !mListening) {
+                try {
+                    mLock.wait(remainder);
+                } catch (InterruptedException ie) {
+                    /**/
+                }
+                if (mListening) {
+                    break;
+                }
+                remainder = bail - System.currentTimeMillis();
+            }
+            if (!mListening) {
+                throw new IOException("Failed to start listener");
+            } else {
+                Log.d(TAG, "OSU Redirect listener running");
+            }
+        }
+    }
+
+    public boolean waitForUser() {
+        boolean success;
+        synchronized (mLock) {
+            long bail = System.currentTimeMillis() + UserTimeout;
+            long remainder = UserTimeout;
+            while (remainder > 0 && mUserStatus == null) {
+                try {
+                    mLock.wait(remainder);
+                } catch (InterruptedException ie) {
+                    /**/
+                }
+                if (mUserStatus != null) {
+                    break;
+                }
+                remainder = bail - System.currentTimeMillis();
+            }
+            success = mUserStatus == OSUOperationStatus.UserInputComplete;
+        }
+        abort();
+        return success;
+    }
+
+    public void abort() {
+        try {
+            mAborted = true;
+            mServerSocket.close();
+        } catch (IOException ioe) {
+            /**/
+        }
+    }
+
+    public URL getURL() {
+        return mURL;
+    }
+
+    @Override
+    public void run() {
+        int count = 0;
+        synchronized (mLock) {
+            mListening = true;
+            mLock.notifyAll();
+        }
+
+        boolean terminate = false;
+
+        for (; ; ) {
+            count++;
+            try (Socket instance = mServerSocket.accept()) {
+                try (BufferedReader in = new BufferedReader(
+                        new InputStreamReader(instance.getInputStream(), StandardCharsets.UTF_8))) {
+                    boolean detected = false;
+                    StringBuilder sb = new StringBuilder();
+                    String s;
+                    while ((s = in.readLine()) != null) {
+                        sb.append(s).append('\n');
+                        if (!detected && s.startsWith("GET")) {
+                            String[] segments = s.split(" ");
+                            if (segments.length == 3 &&
+                                    segments[2].startsWith("HTTP/") &&
+                                    segments[1].regionMatches(1, mPath, 0, mPath.length())) {
+                                detected = true;
+                            }
+                        }
+                        if (s.length() == 0) {
+                            break;
+                        }
+                    }
+                    Log.d(TAG, "Redirect receive: " + sb);
+                    String response = null;
+                    if (detected) {
+                        response = status(OSUOperationStatus.UserInputComplete);
+                        if (response == null) {
+                            response = GoodBye;
+                            terminate = true;
+                        }
+                    }
+                    try (BufferedWriter out = new BufferedWriter(
+                            new OutputStreamWriter(instance.getOutputStream(),
+                                    StandardCharsets.UTF_8))) {
+
+                        out.write(HTTPResponseHeader);
+                        if (response != null) {
+                            out.write(response);
+                        }
+                    }
+                    if (terminate) {
+                        break;
+                    } else if (count > MaxRetry) {
+                        status(OSUOperationStatus.UserInputAborted);
+                        break;
+                    }
+                }
+            } catch (IOException ioe) {
+                if (mAborted) {
+                    return;
+                } else if (count > MaxRetry) {
+                    status(OSUOperationStatus.UserInputAborted);
+                    break;
+                }
+            }
+        }
+    }
+
+    private String status(OSUOperationStatus status) {
+        Log.d(TAG, "User input status: " + status);
+        synchronized (mLock) {
+            mUserStatus = status;
+            mLock.notifyAll();
+        }
+        String message = (status == OSUOperationStatus.UserInputAborted) ?
+                "Browser closed" : null;
+
+        return mOSUManager.notifyUser(status, message, mSpName);
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java b/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java
new file mode 100644
index 0000000..783d14b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java
@@ -0,0 +1,108 @@
+package com.android.hotspot2.osu.service;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.WifiNetworkAdapter;
+import com.android.hotspot2.osu.OSUManager;
+import com.android.hotspot2.pps.HomeSP;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SubscriptionTimer implements Runnable {
+    private final Handler mHandler;
+    private final OSUManager mOSUManager;
+    private final WifiNetworkAdapter mWifiNetworkAdapter;
+    private final Map<HomeSP, UpdateAction> mOutstanding = new HashMap<>();
+
+    private static class UpdateAction {
+        private final long mRemediation;
+        private final long mPolicy;
+
+        private UpdateAction(HomeSP homeSP, long now) {
+            mRemediation = homeSP.getSubscriptionUpdate() != null ?
+                    now + homeSP.getSubscriptionUpdate().getInterval() : -1;
+            mPolicy = homeSP.getPolicy() != null ?
+                    now + homeSP.getPolicy().getPolicyUpdate().getInterval() : -1;
+
+            Log.d(OSUManager.TAG, "Timer set for " + homeSP.getFQDN() +
+                    ", remediation: " + Utils.toUTCString(mRemediation) +
+                    ", policy: " + Utils.toUTCString(mPolicy));
+        }
+
+        private boolean remediate(long now) {
+            return mRemediation > 0 && now >= mRemediation;
+        }
+
+        private boolean policyUpdate(long now) {
+            return mPolicy > 0 && now >= mPolicy;
+        }
+
+        private long nextExpiry(long now) {
+            long min = Long.MAX_VALUE;
+            if (mRemediation > now) {
+                min = mRemediation;
+            }
+            if (mPolicy > now) {
+                min = Math.min(min, mPolicy);
+            }
+            return min;
+        }
+    }
+
+    private static final String ACTION_TIMER =
+            "com.android.hotspot2.osu.service.SubscriptionTimer.action.TICK";
+
+    public SubscriptionTimer(OSUManager osuManager,
+                             WifiNetworkAdapter wifiNetworkAdapter, Context context) {
+        mOSUManager = osuManager;
+        mWifiNetworkAdapter = wifiNetworkAdapter;
+        mHandler = new Handler();
+    }
+
+    @Override
+    public void run() {
+        checkUpdates();
+    }
+
+    public void checkUpdates() {
+        mHandler.removeCallbacks(this);
+        long now = System.currentTimeMillis();
+        long next = Long.MAX_VALUE;
+        Collection<HomeSP> homeSPs = mWifiNetworkAdapter.getLoadedSPs();
+        if (homeSPs.isEmpty()) {
+            return;
+        }
+        for (HomeSP homeSP : homeSPs) {
+            UpdateAction updateAction = mOutstanding.get(homeSP);
+            try {
+                if (updateAction == null) {
+                    updateAction = new UpdateAction(homeSP, now);
+                    mOutstanding.put(homeSP, updateAction);
+                } else if (updateAction.remediate(now)) {
+                    mOSUManager.remediate(homeSP, false);
+                    mOutstanding.put(homeSP, new UpdateAction(homeSP, now));
+                } else if (updateAction.policyUpdate(now)) {
+                    mOSUManager.remediate(homeSP, true);
+                    mOutstanding.put(homeSP, new UpdateAction(homeSP, now));
+                }
+                next = Math.min(next, updateAction.nextExpiry(now));
+            } catch (IOException | SAXException e) {
+                Log.d(OSUManager.TAG, "Failed subscription update: " + e.getMessage());
+            }
+        }
+        setAlarm(next);
+    }
+
+    private void setAlarm(long tod) {
+        long delay = tod - System.currentTimeMillis();
+        mHandler.postAtTime(this, Math.max(1, delay));
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/Credential.java b/packages/Osu/src/com/android/hotspot2/pps/Credential.java
new file mode 100644
index 0000000..15f0dcf
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/Credential.java
@@ -0,0 +1,252 @@
+package com.android.hotspot2.pps;
+
+import android.text.TextUtils;
+import android.util.Base64;
+
+import com.android.anqp.eap.EAPMethod;
+import com.android.hotspot2.IMSIParameter;
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.OMAException;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+public class Credential {
+    public enum CertType {IEEE, x509v3}
+
+    public static final String CertTypeX509 = "x509v3";
+    public static final String CertTypeIEEE = "802.1ar";
+
+    private final long mCtime;
+    private final long mExpTime;
+    private final String mRealm;
+    private final boolean mCheckAAACert;
+
+    private final String mUserName;
+    private final String mPassword;
+    private final boolean mDisregardPassword;
+    private final boolean mMachineManaged;
+    private final String mSTokenApp;
+    private final boolean mShare;
+    private final EAPMethod mEAPMethod;
+
+    private final CertType mCertType;
+    private final byte[] mFingerPrint;
+
+    private final IMSIParameter mImsi;
+
+    public Credential(long ctime, long expTime, String realm, boolean checkAAACert,
+                      EAPMethod eapMethod, String userName, String password,
+                      boolean machineManaged, String stApp, boolean share) {
+        mCtime = ctime;
+        mExpTime = expTime;
+        mRealm = realm;
+        mCheckAAACert = checkAAACert;
+        mEAPMethod = eapMethod;
+        mUserName = userName;
+
+        if (!TextUtils.isEmpty(password)) {
+            byte[] pwOctets = Base64.decode(password, Base64.DEFAULT);
+            mPassword = new String(pwOctets, StandardCharsets.UTF_8);
+        } else {
+            mPassword = null;
+        }
+        mDisregardPassword = false;
+
+        mMachineManaged = machineManaged;
+        mSTokenApp = stApp;
+        mShare = share;
+
+        mCertType = null;
+        mFingerPrint = null;
+
+        mImsi = null;
+    }
+
+    public Credential(long ctime, long expTime, String realm, boolean checkAAACert,
+                      EAPMethod eapMethod, Credential.CertType certType, byte[] fingerPrint) {
+        mCtime = ctime;
+        mExpTime = expTime;
+        mRealm = realm;
+        mCheckAAACert = checkAAACert;
+        mEAPMethod = eapMethod;
+        mCertType = certType;
+        mFingerPrint = fingerPrint;
+
+        mUserName = null;
+        mPassword = null;
+        mDisregardPassword = false;
+        mMachineManaged = false;
+        mSTokenApp = null;
+        mShare = false;
+
+        mImsi = null;
+    }
+
+    public Credential(long ctime, long expTime, String realm, boolean checkAAACert,
+                      EAPMethod eapMethod, IMSIParameter imsi) {
+        mCtime = ctime;
+        mExpTime = expTime;
+        mRealm = realm;
+        mCheckAAACert = checkAAACert;
+        mEAPMethod = eapMethod;
+        mImsi = imsi;
+
+        mCertType = null;
+        mFingerPrint = null;
+
+        mUserName = null;
+        mPassword = null;
+        mDisregardPassword = false;
+        mMachineManaged = false;
+        mSTokenApp = null;
+        mShare = false;
+    }
+
+    public Credential(Credential other, String password) {
+        mCtime = other.mCtime;
+        mExpTime = other.mExpTime;
+        mRealm = other.mRealm;
+        mCheckAAACert = other.mCheckAAACert;
+        mUserName = other.mUserName;
+        mPassword = password;
+        mDisregardPassword = other.mDisregardPassword;
+        mMachineManaged = other.mMachineManaged;
+        mSTokenApp = other.mSTokenApp;
+        mShare = other.mShare;
+        mEAPMethod = other.mEAPMethod;
+        mCertType = other.mCertType;
+        mFingerPrint = other.mFingerPrint;
+        mImsi = other.mImsi;
+    }
+
+    public static CertType mapCertType(String certType) throws OMAException {
+        if (certType.equalsIgnoreCase(CertTypeX509)) {
+            return CertType.x509v3;
+        } else if (certType.equalsIgnoreCase(CertTypeIEEE)) {
+            return CertType.IEEE;
+        } else {
+            throw new OMAException("Invalid cert type: '" + certType + "'");
+        }
+    }
+
+    public EAPMethod getEAPMethod() {
+        return mEAPMethod;
+    }
+
+    public String getRealm() {
+        return mRealm;
+    }
+
+    public IMSIParameter getImsi() {
+        return mImsi;
+    }
+
+    public String getUserName() {
+        return mUserName;
+    }
+
+    public String getPassword() {
+        return mPassword;
+    }
+
+    public boolean hasDisregardPassword() {
+        return mDisregardPassword;
+    }
+
+    public CertType getCertType() {
+        return mCertType;
+    }
+
+    public byte[] getFingerPrint() {
+        return mFingerPrint;
+    }
+
+    public long getCtime() {
+        return mCtime;
+    }
+
+    public long getExpTime() {
+        return mExpTime;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Credential that = (Credential) o;
+
+        if (mCheckAAACert != that.mCheckAAACert) return false;
+        if (mCtime != that.mCtime) return false;
+        if (mExpTime != that.mExpTime) return false;
+        if (mMachineManaged != that.mMachineManaged) return false;
+        if (mShare != that.mShare) return false;
+        if (mCertType != that.mCertType) return false;
+        if (!mEAPMethod.equals(that.mEAPMethod)) return false;
+        if (!Arrays.equals(mFingerPrint, that.mFingerPrint)) return false;
+        if (!safeEquals(mImsi, that.mImsi)) {
+            return false;
+        }
+
+        if (!mDisregardPassword && !safeEquals(mPassword, that.mPassword)) {
+            return false;
+        }
+
+        if (!mRealm.equals(that.mRealm)) return false;
+        if (!safeEquals(mSTokenApp, that.mSTokenApp)) {
+            return false;
+        }
+        if (!safeEquals(mUserName, that.mUserName)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private static boolean safeEquals(Object s1, Object s2) {
+        if (s1 == null) {
+            return s2 == null;
+        } else {
+            return s2 != null && s1.equals(s2);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (mCtime ^ (mCtime >>> 32));
+        result = 31 * result + (int) (mExpTime ^ (mExpTime >>> 32));
+        result = 31 * result + mRealm.hashCode();
+        result = 31 * result + (mCheckAAACert ? 1 : 0);
+        result = 31 * result + (mUserName != null ? mUserName.hashCode() : 0);
+        result = 31 * result + (mPassword != null ? mPassword.hashCode() : 0);
+        result = 31 * result + (mMachineManaged ? 1 : 0);
+        result = 31 * result + (mSTokenApp != null ? mSTokenApp.hashCode() : 0);
+        result = 31 * result + (mShare ? 1 : 0);
+        result = 31 * result + mEAPMethod.hashCode();
+        result = 31 * result + (mCertType != null ? mCertType.hashCode() : 0);
+        result = 31 * result + (mFingerPrint != null ? Arrays.hashCode(mFingerPrint) : 0);
+        result = 31 * result + (mImsi != null ? mImsi.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Credential{" +
+                "mCtime=" + Utils.toUTCString(mCtime) +
+                ", mExpTime=" + Utils.toUTCString(mExpTime) +
+                ", mRealm='" + mRealm + '\'' +
+                ", mCheckAAACert=" + mCheckAAACert +
+                ", mUserName='" + mUserName + '\'' +
+                ", mPassword='" + mPassword + '\'' +
+                ", mDisregardPassword=" + mDisregardPassword +
+                ", mMachineManaged=" + mMachineManaged +
+                ", mSTokenApp='" + mSTokenApp + '\'' +
+                ", mShare=" + mShare +
+                ", mEAPMethod=" + mEAPMethod +
+                ", mCertType=" + mCertType +
+                ", mFingerPrint=" + Utils.toHexString(mFingerPrint) +
+                ", mImsi='" + mImsi + '\'' +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/DomainMatcher.java b/packages/Osu/src/com/android/hotspot2/pps/DomainMatcher.java
new file mode 100644
index 0000000..10768d6
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/DomainMatcher.java
@@ -0,0 +1,149 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class DomainMatcher {
+
+    public enum Match {None, Primary, Secondary}
+
+    private final Label mRoot;
+
+    private static class Label {
+        private final Map<String, Label> mSubDomains;
+        private final Match mMatch;
+
+        private Label(Match match) {
+            mMatch = match;
+            mSubDomains = match == Match.None ? new HashMap<String, Label>() : null;
+        }
+
+        private void addDomain(Iterator<String> labels, Match match) {
+            String labelName = labels.next();
+            if (labels.hasNext()) {
+                Label subLabel = new Label(Match.None);
+                mSubDomains.put(labelName, subLabel);
+                subLabel.addDomain(labels, match);
+            } else {
+                mSubDomains.put(labelName, new Label(match));
+            }
+        }
+
+        private Label getSubLabel(String labelString) {
+            return mSubDomains.get(labelString);
+        }
+
+        public Match getMatch() {
+            return mMatch;
+        }
+
+        private void toString(StringBuilder sb) {
+            if (mSubDomains != null) {
+                sb.append(".{");
+                for (Map.Entry<String, Label> entry : mSubDomains.entrySet()) {
+                    sb.append(entry.getKey());
+                    entry.getValue().toString(sb);
+                }
+                sb.append('}');
+            } else {
+                sb.append('=').append(mMatch);
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            toString(sb);
+            return sb.toString();
+        }
+    }
+
+    public DomainMatcher(List<String> primary, List<List<String>> secondary) {
+        mRoot = new Label(Match.None);
+        for (List<String> secondaryLabel : secondary) {
+            mRoot.addDomain(secondaryLabel.iterator(), Match.Secondary);
+        }
+        // Primary overwrites secondary.
+        mRoot.addDomain(primary.iterator(), Match.Primary);
+    }
+
+    /**
+     * Check if domain is either a the same or a sub-domain of any of the domains in the domain tree
+     * in this matcher, i.e. all or or a sub-set of the labels in domain matches a path in the tree.
+     *
+     * @param domain Domain to be checked.
+     * @return None if domain is not a sub-domain, Primary if it matched one of the primary domains
+     * or Secondary if it matched on of the secondary domains.
+     */
+    public Match isSubDomain(List<String> domain) {
+
+        Label label = mRoot;
+        for (String labelString : domain) {
+            label = label.getSubLabel(labelString);
+            if (label == null) {
+                return Match.None;
+            } else if (label.getMatch() != Match.None) {
+                return label.getMatch();
+            }
+        }
+        return Match.None;  // Domain is a super domain
+    }
+
+    public static boolean arg2SubdomainOfArg1(List<String> arg1, List<String> arg2) {
+        if (arg2.size() < arg1.size()) {
+            return false;
+        }
+
+        Iterator<String> l1 = arg1.iterator();
+        Iterator<String> l2 = arg2.iterator();
+
+        while (l1.hasNext()) {
+            if (!l1.next().equals(l2.next())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "Domain matcher " + mRoot;
+    }
+
+    private static final String[] TestDomains = {
+            "garbage.apple.com",
+            "apple.com",
+            "com",
+            "jan.android.google.com.",
+            "jan.android.google.com",
+            "android.google.com",
+            "google.com",
+            "jan.android.google.net.",
+            "jan.android.google.net",
+            "android.google.net",
+            "google.net",
+            "net.",
+            "."
+    };
+
+    public static void main(String[] args) {
+        DomainMatcher dm1 = new DomainMatcher(Utils.splitDomain("android.google.com"),
+                Collections.<List<String>>emptyList());
+        for (String domain : TestDomains) {
+            System.out.println(domain + ": " + dm1.isSubDomain(Utils.splitDomain(domain)));
+        }
+        List<List<String>> secondaries = new ArrayList<List<String>>();
+        secondaries.add(Utils.splitDomain("apple.com"));
+        secondaries.add(Utils.splitDomain("net"));
+        DomainMatcher dm2 = new DomainMatcher(Utils.splitDomain("android.google.com"), secondaries);
+        for (String domain : TestDomains) {
+            System.out.println(domain + ": " + dm2.isSubDomain(Utils.splitDomain(domain)));
+        }
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/HomeSP.java b/packages/Osu/src/com/android/hotspot2/pps/HomeSP.java
new file mode 100644
index 0000000..cfbf9d1
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/HomeSP.java
@@ -0,0 +1,211 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class HomeSP {
+    private final Map<String, Long> mSSIDs;        // SSID, HESSID, [0,N]
+    private final String mFQDN;
+    private final DomainMatcher mDomainMatcher;
+    private final Set<String> mOtherHomePartners;
+    private final HashSet<Long> mRoamingConsortiums;    // [0,N]
+    private final Set<Long> mMatchAnyOIs;           // [0,N]
+    private final List<Long> mMatchAllOIs;          // [0,N]
+
+    private final Credential mCredential;
+
+    // Informational:
+    private final String mFriendlyName;             // [1]
+    private final String mIconURL;                  // [0,1]
+
+    private final Policy mPolicy;
+    private final int mCredentialPriority;
+    private final Map<String, String> mAAATrustRoots;
+    private final UpdateInfo mSubscriptionUpdate;
+    private final SubscriptionParameters mSubscriptionParameters;
+    private final int mUpdateIdentifier;
+
+    @Deprecated
+    public HomeSP(Map<String, Long> ssidMap,
+                   /*@NotNull*/ String fqdn,
+                   /*@NotNull*/ HashSet<Long> roamingConsortiums,
+                   /*@NotNull*/ Set<String> otherHomePartners,
+                   /*@NotNull*/ Set<Long> matchAnyOIs,
+                   /*@NotNull*/ List<Long> matchAllOIs,
+                   String friendlyName,
+                   String iconURL,
+                   Credential credential) {
+
+        mSSIDs = ssidMap;
+        List<List<String>> otherPartners = new ArrayList<>(otherHomePartners.size());
+        for (String otherPartner : otherHomePartners) {
+            otherPartners.add(Utils.splitDomain(otherPartner));
+        }
+        mOtherHomePartners = otherHomePartners;
+        mFQDN = fqdn;
+        mDomainMatcher = new DomainMatcher(Utils.splitDomain(fqdn), otherPartners);
+        mRoamingConsortiums = roamingConsortiums;
+        mMatchAnyOIs = matchAnyOIs;
+        mMatchAllOIs = matchAllOIs;
+        mFriendlyName = friendlyName;
+        mIconURL = iconURL;
+        mCredential = credential;
+
+        mPolicy = null;
+        mCredentialPriority = -1;
+        mAAATrustRoots = null;
+        mSubscriptionUpdate = null;
+        mSubscriptionParameters = null;
+        mUpdateIdentifier = -1;
+    }
+
+    public HomeSP(Map<String, Long> ssidMap,
+                   /*@NotNull*/ String fqdn,
+                   /*@NotNull*/ HashSet<Long> roamingConsortiums,
+                   /*@NotNull*/ Set<String> otherHomePartners,
+                   /*@NotNull*/ Set<Long> matchAnyOIs,
+                   /*@NotNull*/ List<Long> matchAllOIs,
+                   String friendlyName,
+                   String iconURL,
+                   Credential credential,
+
+                   Policy policy,
+                   int credentialPriority,
+                   Map<String, String> AAATrustRoots,
+                   UpdateInfo subscriptionUpdate,
+                   SubscriptionParameters subscriptionParameters,
+                   int updateIdentifier) {
+
+        mSSIDs = ssidMap;
+        List<List<String>> otherPartners = new ArrayList<>(otherHomePartners.size());
+        for (String otherPartner : otherHomePartners) {
+            otherPartners.add(Utils.splitDomain(otherPartner));
+        }
+        mOtherHomePartners = otherHomePartners;
+        mFQDN = fqdn;
+        mDomainMatcher = new DomainMatcher(Utils.splitDomain(fqdn), otherPartners);
+        mRoamingConsortiums = roamingConsortiums;
+        mMatchAnyOIs = matchAnyOIs;
+        mMatchAllOIs = matchAllOIs;
+        mFriendlyName = friendlyName;
+        mIconURL = iconURL;
+        mCredential = credential;
+
+        mPolicy = policy;
+        mCredentialPriority = credentialPriority;
+        mAAATrustRoots = AAATrustRoots;
+        mSubscriptionUpdate = subscriptionUpdate;
+        mSubscriptionParameters = subscriptionParameters;
+        mUpdateIdentifier = updateIdentifier;
+    }
+
+    public int getUpdateIdentifier() {
+        return mUpdateIdentifier;
+    }
+
+    public UpdateInfo getSubscriptionUpdate() {
+        return mSubscriptionUpdate;
+    }
+
+    public Policy getPolicy() {
+        return mPolicy;
+    }
+
+    private String imsiMatch(List<String> imsis, String mccMnc) {
+        if (mCredential.getImsi().matchesMccMnc(mccMnc)) {
+            for (String imsi : imsis) {
+                if (imsi.startsWith(mccMnc)) {
+                    return imsi;
+                }
+            }
+        }
+        return null;
+    }
+
+    public String getFQDN() {
+        return mFQDN;
+    }
+
+    public String getFriendlyName() {
+        return mFriendlyName;
+    }
+
+    public HashSet<Long> getRoamingConsortiums() {
+        return mRoamingConsortiums;
+    }
+
+    public Credential getCredential() {
+        return mCredential;
+    }
+
+    public Map<String, Long> getSSIDs() {
+        return mSSIDs;
+    }
+
+    public Collection<String> getOtherHomePartners() {
+        return mOtherHomePartners;
+    }
+
+    public Set<Long> getMatchAnyOIs() {
+        return mMatchAnyOIs;
+    }
+
+    public List<Long> getMatchAllOIs() {
+        return mMatchAllOIs;
+    }
+
+    public String getIconURL() {
+        return mIconURL;
+    }
+
+    public boolean deepEquals(HomeSP other) {
+        return mFQDN.equals(other.mFQDN) &&
+                mSSIDs.equals(other.mSSIDs) &&
+                mOtherHomePartners.equals(other.mOtherHomePartners) &&
+                mRoamingConsortiums.equals(other.mRoamingConsortiums) &&
+                mMatchAnyOIs.equals(other.mMatchAnyOIs) &&
+                mMatchAllOIs.equals(other.mMatchAllOIs) &&
+                mFriendlyName.equals(other.mFriendlyName) &&
+                Utils.compare(mIconURL, other.mIconURL) == 0 &&
+                mCredential.equals(other.mCredential);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        } else if (thatObject == null || getClass() != thatObject.getClass()) {
+            return false;
+        }
+
+        HomeSP that = (HomeSP) thatObject;
+        return mFQDN.equals(that.mFQDN);
+    }
+
+    @Override
+    public int hashCode() {
+        return mFQDN.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "HomeSP{" +
+                "SSIDs=" + mSSIDs +
+                ", FQDN='" + mFQDN + '\'' +
+                ", DomainMatcher=" + mDomainMatcher +
+                ", RoamingConsortiums={" + Utils.roamingConsortiumsToString(mRoamingConsortiums) +
+                '}' +
+                ", MatchAnyOIs={" + Utils.roamingConsortiumsToString(mMatchAnyOIs) + '}' +
+                ", MatchAllOIs={" + Utils.roamingConsortiumsToString(mMatchAllOIs) + '}' +
+                ", Credential=" + mCredential +
+                ", FriendlyName='" + mFriendlyName + '\'' +
+                ", IconURL='" + mIconURL + '\'' +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/Policy.java b/packages/Osu/src/com/android/hotspot2/pps/Policy.java
new file mode 100644
index 0000000..5180436
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/Policy.java
@@ -0,0 +1,195 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.OMANode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.android.hotspot2.omadm.MOManager.TAG_Country;
+import static com.android.hotspot2.omadm.MOManager.TAG_DLBandwidth;
+import static com.android.hotspot2.omadm.MOManager.TAG_FQDN_Match;
+import static com.android.hotspot2.omadm.MOManager.TAG_IPProtocol;
+import static com.android.hotspot2.omadm.MOManager.TAG_MaximumBSSLoadValue;
+import static com.android.hotspot2.omadm.MOManager.TAG_MinBackhaulThreshold;
+import static com.android.hotspot2.omadm.MOManager.TAG_NetworkType;
+import static com.android.hotspot2.omadm.MOManager.TAG_PolicyUpdate;
+import static com.android.hotspot2.omadm.MOManager.TAG_PortNumber;
+import static com.android.hotspot2.omadm.MOManager.TAG_PreferredRoamingPartnerList;
+import static com.android.hotspot2.omadm.MOManager.TAG_Priority;
+import static com.android.hotspot2.omadm.MOManager.TAG_RequiredProtoPortTuple;
+import static com.android.hotspot2.omadm.MOManager.TAG_SPExclusionList;
+import static com.android.hotspot2.omadm.MOManager.TAG_SSID;
+import static com.android.hotspot2.omadm.MOManager.TAG_ULBandwidth;
+
+public class Policy {
+    private final List<PreferredRoamingPartner> mPreferredRoamingPartners;
+    private final List<MinBackhaul> mMinBackhaulThresholds;
+    private final UpdateInfo mPolicyUpdate;
+    private final List<String> mSPExclusionList;
+    private final Map<Integer, List<Integer>> mRequiredProtos;
+    private final int mMaxBSSLoad;
+
+    public Policy(OMANode node) throws OMAException {
+
+        OMANode rpNode = node.getChild(TAG_PreferredRoamingPartnerList);
+        if (rpNode == null) {
+            mPreferredRoamingPartners = null;
+        } else {
+            mPreferredRoamingPartners = new ArrayList<>(rpNode.getChildren().size());
+            for (OMANode instance : rpNode.getChildren()) {
+                if (instance.isLeaf()) {
+                    throw new OMAException("Not expecting leaf node in " +
+                            TAG_PreferredRoamingPartnerList);
+                }
+                mPreferredRoamingPartners.add(new PreferredRoamingPartner(instance));
+            }
+        }
+
+        OMANode bhtNode = node.getChild(TAG_MinBackhaulThreshold);
+        if (bhtNode == null) {
+            mMinBackhaulThresholds = null;
+        } else {
+            mMinBackhaulThresholds = new ArrayList<>(bhtNode.getChildren().size());
+            for (OMANode instance : bhtNode.getChildren()) {
+                if (instance.isLeaf()) {
+                    throw new OMAException("Not expecting leaf node in " +
+                            TAG_MinBackhaulThreshold);
+                }
+                mMinBackhaulThresholds.add(new MinBackhaul(instance));
+            }
+        }
+
+        mPolicyUpdate = new UpdateInfo(node.getChild(TAG_PolicyUpdate));
+
+        OMANode sxNode = node.getChild(TAG_SPExclusionList);
+        if (sxNode == null) {
+            mSPExclusionList = null;
+        } else {
+            mSPExclusionList = new ArrayList<>(sxNode.getChildren().size());
+            for (OMANode instance : sxNode.getChildren()) {
+                if (instance.isLeaf()) {
+                    throw new OMAException("Not expecting leaf node in " + TAG_SPExclusionList);
+                }
+                mSPExclusionList.add(MOManager.getString(instance, TAG_SSID));
+            }
+        }
+
+        OMANode rptNode = node.getChild(TAG_RequiredProtoPortTuple);
+        if (rptNode == null) {
+            mRequiredProtos = null;
+        } else {
+            mRequiredProtos = new HashMap<>(rptNode.getChildren().size());
+            for (OMANode instance : rptNode.getChildren()) {
+                if (instance.isLeaf()) {
+                    throw new OMAException("Not expecting leaf node in " +
+                            TAG_RequiredProtoPortTuple);
+                }
+                int protocol = (int) MOManager.getLong(instance, TAG_IPProtocol, null);
+                String[] portSegments = MOManager.getString(instance, TAG_PortNumber).split(",");
+                List<Integer> ports = new ArrayList<>(portSegments.length);
+                for (String portSegment : portSegments) {
+                    try {
+                        ports.add(Integer.parseInt(portSegment));
+                    } catch (NumberFormatException nfe) {
+                        throw new OMAException("Port is not a number: " + portSegment);
+                    }
+                }
+                mRequiredProtos.put(protocol, ports);
+            }
+        }
+
+        mMaxBSSLoad = (int) MOManager.getLong(node, TAG_MaximumBSSLoadValue, Long.MAX_VALUE);
+    }
+
+    public List<PreferredRoamingPartner> getPreferredRoamingPartners() {
+        return mPreferredRoamingPartners;
+    }
+
+    public List<MinBackhaul> getMinBackhaulThresholds() {
+        return mMinBackhaulThresholds;
+    }
+
+    public UpdateInfo getPolicyUpdate() {
+        return mPolicyUpdate;
+    }
+
+    public List<String> getSPExclusionList() {
+        return mSPExclusionList;
+    }
+
+    public Map<Integer, List<Integer>> getRequiredProtos() {
+        return mRequiredProtos;
+    }
+
+    public int getMaxBSSLoad() {
+        return mMaxBSSLoad;
+    }
+
+    private static class PreferredRoamingPartner {
+        private final List<String> mDomain;
+        private final Boolean mIncludeSubDomains;
+        private final int mPriority;
+        private final String mCountry;
+
+        private PreferredRoamingPartner(OMANode node)
+                throws OMAException {
+
+            String[] segments = MOManager.getString(node, TAG_FQDN_Match).split(",");
+            if (segments.length != 2) {
+                throw new OMAException("Bad FQDN match string: " + TAG_FQDN_Match);
+            }
+            mDomain = Utils.splitDomain(segments[0]);
+            mIncludeSubDomains = MOManager.getSelection(TAG_FQDN_Match, segments[1]);
+            mPriority = (int) MOManager.getLong(node, TAG_Priority, null);
+            mCountry = MOManager.getString(node, TAG_Country);
+        }
+
+        @Override
+        public String toString() {
+            return "PreferredRoamingPartner{" +
+                    "domain=" + mDomain +
+                    ", includeSubDomains=" + mIncludeSubDomains +
+                    ", priority=" + mPriority +
+                    ", country='" + mCountry + '\'' +
+                    '}';
+        }
+    }
+
+    private static class MinBackhaul {
+        private final Boolean mHome;
+        private final long mDL;
+        private final long mUL;
+
+        private MinBackhaul(OMANode node) throws OMAException {
+            mHome = MOManager.getSelection(node, TAG_NetworkType);
+            mDL = MOManager.getLong(node, TAG_DLBandwidth, Long.MAX_VALUE);
+            mUL = MOManager.getLong(node, TAG_ULBandwidth, Long.MAX_VALUE);
+        }
+
+        @Override
+        public String toString() {
+            return "MinBackhaul{" +
+                    "home=" + mHome +
+                    ", DL=" + mDL +
+                    ", UL=" + mUL +
+                    '}';
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Policy{" +
+                "preferredRoamingPartners=" + mPreferredRoamingPartners +
+                ", minBackhaulThresholds=" + mMinBackhaulThresholds +
+                ", policyUpdate=" + mPolicyUpdate +
+                ", SPExclusionList=" + mSPExclusionList +
+                ", requiredProtos=" + mRequiredProtos +
+                ", maxBSSLoad=" + mMaxBSSLoad +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/SubscriptionParameters.java b/packages/Osu/src/com/android/hotspot2/pps/SubscriptionParameters.java
new file mode 100644
index 0000000..e073ad7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/SubscriptionParameters.java
@@ -0,0 +1,81 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.OMANode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.hotspot2.omadm.MOManager.TAG_CreationDate;
+import static com.android.hotspot2.omadm.MOManager.TAG_DataLimit;
+import static com.android.hotspot2.omadm.MOManager.TAG_ExpirationDate;
+import static com.android.hotspot2.omadm.MOManager.TAG_StartDate;
+import static com.android.hotspot2.omadm.MOManager.TAG_TimeLimit;
+import static com.android.hotspot2.omadm.MOManager.TAG_TypeOfSubscription;
+import static com.android.hotspot2.omadm.MOManager.TAG_UsageLimits;
+import static com.android.hotspot2.omadm.MOManager.TAG_UsageTimePeriod;
+
+public class SubscriptionParameters {
+    private final long mCDate;
+    private final long mXDate;
+    private final String mType;
+    private final List<Limit> mLimits;
+
+    public SubscriptionParameters(OMANode node) throws OMAException {
+        mCDate = MOManager.getTime(node.getChild(TAG_CreationDate));
+        mXDate = MOManager.getTime(node.getChild(TAG_ExpirationDate));
+        mType = MOManager.getString(node.getChild(TAG_TypeOfSubscription));
+
+        OMANode ulNode = node.getChild(TAG_UsageLimits);
+        if (ulNode == null) {
+            mLimits = null;
+        } else {
+            mLimits = new ArrayList<>(ulNode.getChildren().size());
+            for (OMANode instance : ulNode.getChildren()) {
+                if (instance.isLeaf()) {
+                    throw new OMAException("Not expecting leaf node in " +
+                            TAG_UsageLimits);
+                }
+                mLimits.add(new Limit(instance));
+            }
+        }
+
+    }
+
+    private static class Limit {
+        private final long mDataLimit;
+        private final long mStartDate;
+        private final long mTimeLimit;
+        private final long mUsageTimePeriod;
+
+        private Limit(OMANode node) throws OMAException {
+            mDataLimit = MOManager.getLong(node, TAG_DataLimit, Long.MAX_VALUE);
+            mStartDate = MOManager.getTime(node.getChild(TAG_StartDate));
+            mTimeLimit = MOManager.getLong(node, TAG_TimeLimit, Long.MAX_VALUE) *
+                    MOManager.IntervalFactor;
+            mUsageTimePeriod = MOManager.getLong(node, TAG_UsageTimePeriod, null);
+        }
+
+        @Override
+        public String toString() {
+            return "Limit{" +
+                    "dataLimit=" + mDataLimit +
+                    ", startDate=" + Utils.toUTCString(mStartDate) +
+                    ", timeLimit=" + mTimeLimit +
+                    ", usageTimePeriod=" + mUsageTimePeriod +
+                    '}';
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SubscriptionParameters{" +
+                "cDate=" + Utils.toUTCString(mCDate) +
+                ", xDate=" + Utils.toUTCString(mXDate) +
+                ", type='" + mType + '\'' +
+                ", limits=" + mLimits +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java b/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java
new file mode 100644
index 0000000..e32f6c3
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java
@@ -0,0 +1,103 @@
+package com.android.hotspot2.pps;
+
+import android.util.Base64;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.OMANode;
+
+import java.nio.charset.StandardCharsets;
+
+import static com.android.hotspot2.omadm.MOManager.TAG_CertSHA256Fingerprint;
+import static com.android.hotspot2.omadm.MOManager.TAG_CertURL;
+import static com.android.hotspot2.omadm.MOManager.TAG_Password;
+import static com.android.hotspot2.omadm.MOManager.TAG_Restriction;
+import static com.android.hotspot2.omadm.MOManager.TAG_TrustRoot;
+import static com.android.hotspot2.omadm.MOManager.TAG_URI;
+import static com.android.hotspot2.omadm.MOManager.TAG_UpdateInterval;
+import static com.android.hotspot2.omadm.MOManager.TAG_UpdateMethod;
+import static com.android.hotspot2.omadm.MOManager.TAG_Username;
+import static com.android.hotspot2.omadm.MOManager.TAG_UsernamePassword;
+
+public class UpdateInfo {
+    public enum UpdateRestriction {HomeSP, RoamingPartner, Unrestricted}
+
+    private final long mInterval;
+    private final boolean mSPPClientInitiated;
+    private final UpdateRestriction mUpdateRestriction;
+    private final String mURI;
+    private final String mUsername;
+    private final String mPassword;
+    private final String mCertURL;
+    private final String mCertFP;
+
+    public UpdateInfo(OMANode policyUpdate) throws OMAException {
+        mInterval = MOManager.getLong(policyUpdate, TAG_UpdateInterval, null) *
+                MOManager.IntervalFactor;
+        mSPPClientInitiated = MOManager.getSelection(policyUpdate, TAG_UpdateMethod);
+        mUpdateRestriction = MOManager.getSelection(policyUpdate, TAG_Restriction);
+        mURI = MOManager.getString(policyUpdate, TAG_URI);
+
+        OMANode unp = policyUpdate.getChild(TAG_UsernamePassword);
+        if (unp != null) {
+            mUsername = MOManager.getString(unp.getChild(TAG_Username));
+            String pw = MOManager.getString(unp.getChild(TAG_Password));
+            mPassword = new String(Base64.decode(pw.getBytes(StandardCharsets.US_ASCII),
+                    Base64.DEFAULT), StandardCharsets.UTF_8);
+        } else {
+            mUsername = null;
+            mPassword = null;
+        }
+
+        OMANode trustRoot = MOManager.getChild(policyUpdate, TAG_TrustRoot);
+        mCertURL = MOManager.getString(trustRoot, TAG_CertURL);
+        mCertFP = MOManager.getString(trustRoot, TAG_CertSHA256Fingerprint);
+    }
+
+    public long getInterval() {
+        return mInterval;
+    }
+
+    public boolean isSPPClientInitiated() {
+        return mSPPClientInitiated;
+    }
+
+    public UpdateRestriction getUpdateRestriction() {
+        return mUpdateRestriction;
+    }
+
+    public String getURI() {
+        return mURI;
+    }
+
+    public String getUsername() {
+        return mUsername;
+    }
+
+    public String getPassword() {
+        return mPassword;
+    }
+
+    public String getCertURL() {
+        return mCertURL;
+    }
+
+    public String getCertFP() {
+        return mCertFP;
+    }
+
+    @Override
+    public String toString() {
+        return "UpdateInfo{" +
+                "interval=" + Utils.toHMS(mInterval) +
+                ", SPPClientInitiated=" + mSPPClientInitiated +
+                ", updateRestriction=" + mUpdateRestriction +
+                ", URI='" + mURI + '\'' +
+                ", username='" + mUsername + '\'' +
+                ", password=" + mPassword +
+                ", certURL='" + mCertURL + '\'' +
+                ", certFP='" + mCertFP + '\'' +
+                '}';
+    }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/utils/HTTPMessage.java b/packages/Osu/src/com/android/hotspot2/utils/HTTPMessage.java
new file mode 100644
index 0000000..c675efd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/utils/HTTPMessage.java
@@ -0,0 +1,36 @@
+package com.android.hotspot2.utils;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+public interface HTTPMessage {
+    public static final String HTTPVersion = "HTTP/1.1";
+    public static final String AgentHeader = "User-Agent";
+    public static final String AgentName = "Android HS Client";
+    public static final String HostHeader = "Host";
+    public static final String AcceptHeader = "Accept";
+    public static final String LengthHeader = "Content-Length";
+    public static final String ContentTypeHeader = "Content-Type";
+    public static final String ContentLengthHeader = "Content-Length";
+    public static final String ContentEncodingHeader = "Content-Transfer-Encoding";
+    public static final String AuthHeader = "WWW-Authenticate";
+    public static final String AuthorizationHeader = "Authorization";
+
+    public static final String ContentTypeSOAP = "application/soap+xml";
+
+    public static final int RX_BUFFER = 32768;
+    public static final String CRLF = "\r\n";
+    public static final int BODY_SEPARATOR = 0x0d0a0d0a;
+    public static final int BODY_SEPARATOR_LENGTH = 4;
+
+    public enum Method {GET, PUT, POST}
+
+    public Map<String, String> getHeaders();
+
+    public InputStream getPayloadStream();
+
+    public ByteBuffer getPayload();
+
+    public ByteBuffer getBinaryPayload();
+}
diff --git a/packages/Osu/src/com/android/hotspot2/utils/HTTPRequest.java b/packages/Osu/src/com/android/hotspot2/utils/HTTPRequest.java
new file mode 100644
index 0000000..e97c15a
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/utils/HTTPRequest.java
@@ -0,0 +1,307 @@
+package com.android.hotspot2.utils;
+
+import android.util.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class HTTPRequest implements HTTPMessage {
+    private static final Charset HeaderCharset = StandardCharsets.US_ASCII;
+    private static final int HTTPS_PORT = 443;
+
+    private final String mMethodLine;
+    private final Map<String, String> mHeaderFields;
+    private final byte[] mBody;
+
+    public HTTPRequest(Method method, URL url) {
+        this(null, null, method, url, null, false);
+    }
+
+    public HTTPRequest(String payload, Charset charset, Method method, URL url, String contentType,
+                       boolean base64) {
+        mBody = payload != null ? payload.getBytes(charset) : null;
+
+        mHeaderFields = new LinkedHashMap<>();
+        mHeaderFields.put(AgentHeader, AgentName);
+        if (url.getPort() != HTTPS_PORT) {
+            mHeaderFields.put(HostHeader, url.getHost() + ':' + url.getPort());
+        } else {
+            mHeaderFields.put(HostHeader, url.getHost());
+        }
+        mHeaderFields.put(AcceptHeader, "*/*");
+        if (payload != null) {
+            if (base64) {
+                mHeaderFields.put(ContentTypeHeader, contentType);
+                mHeaderFields.put(ContentEncodingHeader, "base64");
+            } else {
+                mHeaderFields.put(ContentTypeHeader, contentType + "; charset=" +
+                        charset.displayName().toLowerCase());
+            }
+            mHeaderFields.put(ContentLengthHeader, Integer.toString(mBody.length));
+        }
+
+        mMethodLine = method.name() + ' ' + url.getPath() + ' ' + HTTPVersion + CRLF;
+    }
+
+    public void doAuthenticate(HTTPResponse httpResponse, String userName, byte[] password,
+                               URL url, int sequence) throws IOException, GeneralSecurityException {
+        mHeaderFields.put(HTTPMessage.AuthorizationHeader,
+                generateAuthAnswer(httpResponse, userName, password, url, sequence));
+    }
+
+    private static String generateAuthAnswer(HTTPResponse httpResponse, String userName,
+                                             byte[] password, URL url, int sequence)
+            throws IOException, GeneralSecurityException {
+
+        String authRequestLine = httpResponse.getHeader(HTTPMessage.AuthHeader);
+        if (authRequestLine == null) {
+            throw new IOException("Missing auth line");
+        }
+        String[] tokens = authRequestLine.split("[ ,]+");
+        //System.out.println("Tokens: " + Arrays.toString(tokens));
+        if (tokens.length < 3 || !tokens[0].equalsIgnoreCase("digest")) {
+            throw new IOException("Bad " + HTTPMessage.AuthHeader + ": '" + authRequestLine + "'");
+        }
+
+        Map<String, String> itemMap = new HashMap<>();
+        for (int n = 1; n < tokens.length; n++) {
+            String s = tokens[n];
+            int split = s.indexOf('=');
+            if (split < 0) {
+                continue;
+            }
+            itemMap.put(s.substring(0, split).trim().toLowerCase(),
+                    unquote(s.substring(split + 1).trim()));
+        }
+
+        Set<String> qops = splitValue(itemMap.remove("qop"));
+        if (!qops.contains("auth")) {
+            throw new IOException("Unsupported quality of protection value(s): '" + qops + "'");
+        }
+        String algorithm = itemMap.remove("algorithm");
+        if (algorithm != null && !algorithm.equalsIgnoreCase("md5")) {
+            throw new IOException("Unsupported algorithm: '" + algorithm + "'");
+        }
+        String realm = itemMap.remove("realm");
+        String nonceText = itemMap.remove("nonce");
+        if (realm == null || nonceText == null) {
+            throw new IOException("realm and/or nonce missing: '" + authRequestLine + "'");
+        }
+        //System.out.println("Remaining tokens: " + itemMap);
+
+        byte[] cnonce = new byte[16];
+        SecureRandom prng = new SecureRandom();
+        prng.nextBytes(cnonce);
+
+        /*
+         * H(data) = MD5(data)
+         * KD(secret, data) = H(concat(secret, ":", data))
+         *
+         * A1 = unq(username-value) ":" unq(realm-value) ":" passwd
+         * A2 = Method ":" digest-uri-value
+         *
+         * response = KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":"
+          * unq(qop-value) ":" H(A2) )
+         */
+
+        String nc = String.format("%08d", sequence);
+
+        /*
+         * This bears witness to the ingenuity of the emerging "web generation" and the authors of
+         * RFC-2617: Strings are treated as a sequence of octets in blind ignorance of character
+         * encoding, whereas octets strings apparently aren't "good enough" and expanded to
+         * "hex strings"...
+         * As a wild guess I apply UTF-8 below.
+         */
+        String passwordString = new String(password, StandardCharsets.UTF_8);
+        String cNonceString = bytesToHex(cnonce);
+
+        byte[] a1 = hash(userName, realm, passwordString);
+        byte[] a2 = hash("POST", url.getPath());
+        byte[] response = hash(a1, nonceText, nc, cNonceString, "auth", a2);
+
+        StringBuilder authLine = new StringBuilder();
+        authLine.append("Digest ")
+                .append("username=\"").append(userName).append("\", ")
+                .append("realm=\"").append(realm).append("\", ")
+                .append("nonce=\"").append(nonceText).append("\", ")
+                .append("uri=\"").append(url.getPath()).append("\", ")
+                .append("qop=\"auth\", ")
+                .append("nc=").append(nc).append(", ")
+                .append("cnonce=\"").append(cNonceString).append("\", ")
+                .append("response=\"").append(bytesToHex(response)).append('"');
+        String opaque = itemMap.get("opaque");
+        if (opaque != null) {
+            authLine.append(", \"").append(opaque).append('"');
+        }
+
+        return authLine.toString();
+    }
+
+    private static Set<String> splitValue(String value) {
+        Set<String> result = new HashSet<>();
+        if (value != null) {
+            for (String s : value.split(",")) {
+                result.add(s.trim());
+            }
+        }
+        return result;
+    }
+
+    private static byte[] hash(Object... objects) throws GeneralSecurityException {
+        MessageDigest hash = MessageDigest.getInstance("MD5");
+
+        //System.out.println("<Hash>");
+        boolean first = true;
+        for (Object object : objects) {
+            byte[] octets;
+            if (object.getClass() == String.class) {
+                //System.out.println("+= '" + object + "'");
+                octets = ((String) object).getBytes(StandardCharsets.UTF_8);
+            } else {
+                octets = bytesToHexBytes((byte[]) object);
+                //System.out.println("+= " + new String(octets, StandardCharsets.ISO_8859_1));
+            }
+            if (first) {
+                first = false;
+            } else {
+                hash.update((byte) ':');
+            }
+            hash.update(octets);
+        }
+        //System.out.println("</Hash>");
+        return hash.digest();
+    }
+
+    private static String unquote(String s) {
+        return s.startsWith("\"") ? s.substring(1, s.length() - 1) : s;
+    }
+
+    private static byte[] bytesToHexBytes(byte[] octets) {
+        return bytesToHex(octets).getBytes(StandardCharsets.ISO_8859_1);
+    }
+
+    private static String bytesToHex(byte[] octets) {
+        StringBuilder sb = new StringBuilder(octets.length * 2);
+        for (byte b : octets) {
+            sb.append(String.format("%02x", b & 0xff));
+        }
+        return sb.toString();
+    }
+
+    private byte[] buildHeader() {
+        StringBuilder header = new StringBuilder();
+        header.append(mMethodLine);
+        for (Map.Entry<String, String> entry : mHeaderFields.entrySet()) {
+            header.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF);
+        }
+        header.append(CRLF);
+
+        //System.out.println("HTTP Request:");
+        StringBuilder sb2 = new StringBuilder();
+        sb2.append(header);
+        if (mBody != null) {
+            sb2.append(new String(mBody, StandardCharsets.ISO_8859_1));
+        }
+        //System.out.println(sb2);
+        //System.out.println("End HTTP Request.");
+
+        return header.toString().getBytes(HeaderCharset);
+    }
+
+    public void send(OutputStream out) throws IOException {
+        out.write(buildHeader());
+        if (mBody != null) {
+            out.write(mBody);
+        }
+        out.flush();
+    }
+
+    @Override
+    public Map<String, String> getHeaders() {
+        return Collections.unmodifiableMap(mHeaderFields);
+    }
+
+    @Override
+    public InputStream getPayloadStream() {
+        return mBody != null ? new ByteArrayInputStream(mBody) : null;
+    }
+
+    @Override
+    public ByteBuffer getPayload() {
+        return mBody != null ? ByteBuffer.wrap(mBody) : null;
+    }
+
+    @Override
+    public ByteBuffer getBinaryPayload() {
+        byte[] binary = Base64.decode(mBody, Base64.DEFAULT);
+        return ByteBuffer.wrap(binary);
+    }
+
+    public static void main(String[] args) throws GeneralSecurityException {
+        test("Mufasa", "testrealm@host.com", "Circle Of Life", "GET", "/dir/index.html",
+                "dcd98b7102dd2f0e8b11d0f600bfb0c093", "0a4f113b", "00000001", "auth",
+                "6629fae49393a05397450978507c4ef1");
+
+        // WWW-Authenticate: Digest realm="wi-fi.org", qop="auth",
+        // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="
+        // Authorization: Digest
+        //  username="1c7e1582-604d-4c00-b411-bb73735cbcb0"
+        //  realm="wi-fi.org"
+        //  nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="
+        //  uri="/.well-known/est/simpleenroll"
+        //  cnonce="NzA3NDk0"
+        //  nc=00000001
+        //  qop="auth"
+        //  response="2c485d24076452e712b77f4e70776463"
+
+        String nonce = "MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw==";
+        String cnonce = "NzA3NDk0";
+        test("1c7e1582-604d-4c00-b411-bb73735cbcb0", "wi-fi.org", "ruckus1234", "POST",
+                "/.well-known/est/simpleenroll",
+                /*new String(Base64.getDecoder().decode(nonce), StandardCharsets.ISO_8859_1)*/
+                nonce,
+                /*new String(Base64.getDecoder().decode(cnonce), StandardCharsets.ISO_8859_1)*/
+                cnonce, "00000001", "auth", "2c485d24076452e712b77f4e70776463");
+    }
+
+    private static void test(String user, String realm, String password, String method, String path,
+                             String nonce, String cnonce, String nc, String qop, String expect)
+            throws GeneralSecurityException {
+        byte[] a1 = hash(user, realm, password);
+        System.out.println("HA1: " + bytesToHex(a1));
+        byte[] a2 = hash(method, path);
+        System.out.println("HA2: " + bytesToHex(a2));
+        byte[] response = hash(a1, nonce, nc, cnonce, qop, a2);
+
+        StringBuilder authLine = new StringBuilder();
+        String responseString = bytesToHex(response);
+        authLine.append("Digest ")
+                .append("username=\"").append(user).append("\", ")
+                .append("realm=\"").append(realm).append("\", ")
+                .append("nonce=\"").append(nonce).append("\", ")
+                .append("uri=\"").append(path).append("\", ")
+                .append("qop=\"").append(qop).append("\", ")
+                .append("nc=").append(nc).append(", ")
+                .append("cnonce=\"").append(cnonce).append("\", ")
+                .append("response=\"").append(responseString).append('"');
+
+        System.out.println(authLine);
+        System.out.println("Success: " + responseString.equals(expect));
+    }
+}
\ No newline at end of file
diff --git a/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java b/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java
new file mode 100644
index 0000000..ba1b1671
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java
@@ -0,0 +1,185 @@
+package com.android.hotspot2.utils;
+
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.hotspot2.osu.OSUManager;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class HTTPResponse implements HTTPMessage {
+    private final int mStatusCode;
+    private final Map<String, String> mHeaders = new LinkedHashMap<>();
+    private final ByteBuffer mBody;
+
+    private static final String csIndicator = "charset=";
+
+    public HTTPResponse(InputStream in) throws IOException {
+        int expected = Integer.MAX_VALUE;
+        int offset = 0;
+        int body = -1;
+        byte[] input = new byte[RX_BUFFER];
+
+        int statusCode = -1;
+        int bodyPattern = 0;
+
+        while (offset < expected) {
+            int amount = in.read(input, offset, input.length - offset);
+            Log.d(OSUManager.TAG, String.format("Reading into %d from %d, amount %d -> %d",
+                    input.length, offset, input.length - offset, amount));
+            if (amount < 0) {
+                throw new EOFException();
+            }
+            //Log.d("ZXZ", "HTTP response: '"
+            // + new String(input, 0, offset + amount, StandardCharsets.ISO_8859_1));
+
+            if (body < 0) {
+                for (int n = offset; n < offset + amount; n++) {
+                    bodyPattern = (bodyPattern << 8) | (input[n] & 0xff);
+                    if (bodyPattern == 0x0d0a0d0a) {
+                        body = n + 1;
+                        statusCode = parseHeader(input, body, mHeaders);
+                        expected = calculateLength(body, mHeaders);
+                        if (expected > input.length) {
+                            input = Arrays.copyOf(input, expected);
+                        }
+                        break;
+                    }
+                }
+            }
+            offset += amount;
+            if (offset < expected && offset == input.length) {
+                input = Arrays.copyOf(input, input.length * 2);
+            }
+        }
+        mStatusCode = statusCode;
+        mBody = ByteBuffer.wrap(input, body, expected - body);
+    }
+
+    private static int parseHeader(byte[] input, int body, Map<String, String> headers)
+            throws IOException {
+        String headerText = new String(input, 0, body - BODY_SEPARATOR_LENGTH,
+                StandardCharsets.ISO_8859_1);
+        //System.out.println("Received header: " + headerText);
+        Iterator<String> headerLines = Arrays.asList(headerText.split(CRLF)).iterator();
+        if (!headerLines.hasNext()) {
+            throw new IOException("Bad HTTP Request");
+        }
+
+        int statusCode;
+        String line0 = headerLines.next();
+        String[] status = line0.split(" ");
+        if (status.length != 3 || !"HTTP/1.1".equals(status[0])) {
+            throw new IOException("Bad HTTP Result: " + line0);
+        }
+        try {
+            statusCode = Integer.parseInt(status[1].trim());
+        } catch (NumberFormatException nfe) {
+            throw new IOException("Bad HTTP header line: '" + line0 + "'");
+        }
+
+        while (headerLines.hasNext()) {
+            String line = headerLines.next();
+            int keyEnd = line.indexOf(':');
+            if (keyEnd < 0) {
+                throw new IOException("Bad header line: '" + line + "'");
+            }
+            String key = line.substring(0, keyEnd).trim();
+            String value = line.substring(keyEnd + 1).trim();
+            headers.put(key, value);
+        }
+        return statusCode;
+    }
+
+    private static int calculateLength(int body, Map<String, String> headers) throws IOException {
+        String contentLength = headers.get(LengthHeader);
+        if (contentLength == null) {
+            throw new IOException("No " + LengthHeader);
+        }
+        try {
+            return body + Integer.parseInt(contentLength);
+        } catch (NumberFormatException nfe) {
+            throw new IOException("Bad " + LengthHeader + ": " + contentLength);
+        }
+    }
+
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    @Override
+    public Map<String, String> getHeaders() {
+        return Collections.unmodifiableMap(mHeaders);
+    }
+
+    public String getHeader(String key) {
+        return mHeaders.get(key);
+    }
+
+    @Override
+    public InputStream getPayloadStream() {
+        return new ByteArrayInputStream(mBody.array(), mBody.position(),
+                mBody.limit() - mBody.position());
+    }
+
+    @Override
+    public ByteBuffer getPayload() {
+        return mBody.duplicate();
+    }
+
+    @Override
+    public ByteBuffer getBinaryPayload() {
+        byte[] data = new byte[mBody.remaining()];
+        mBody.duplicate().get(data);
+        byte[] binary = Base64.decode(data, Base64.DEFAULT);
+        return ByteBuffer.wrap(binary);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Status: ").append(mStatusCode).append(CRLF);
+        for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
+            sb.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF);
+        }
+        sb.append(CRLF);
+        Charset charset;
+        try {
+            charset = Charset.forName(getCharset());
+        } catch (IllegalArgumentException iae) {
+            charset = StandardCharsets.ISO_8859_1;
+        }
+        sb.append(new String(mBody.array(), mBody.position(),
+                mBody.limit() - mBody.position(), charset));
+        return sb.toString();
+    }
+
+    public String getCharset() {
+        String contentType = mHeaders.get(ContentTypeHeader);
+        if (contentType == null) {
+            return null;
+        }
+        int csPos = contentType.indexOf(csIndicator);
+        return csPos < 0 ? null : contentType.substring(csPos + csIndicator.length()).trim();
+    }
+
+    private static boolean equals(byte[] b1, int offset, byte[] pattern) {
+        for (int n = 0; n < pattern.length; n++) {
+            if (b1[n + offset] != pattern[n]) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
index 78c530c..f885110 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
@@ -155,7 +155,7 @@
             for (UserInfo userInfo : um.getProfiles(userId)) {
                 final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userInfo.id);
                 if (admins == null) {
-                    return null;
+                    continue;
                 }
                 final boolean isSeparateProfileChallengeEnabled =
                         lockPatternUtils.isSeparateProfileChallengeEnabled(userInfo.id);
@@ -209,16 +209,7 @@
         IPackageManager ipm = AppGlobals.getPackageManager();
         try {
             if (ipm.getBlockUninstallForUser(packageName, userId)) {
-                DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
-                        Context.DEVICE_POLICY_SERVICE);
-                if (dpm == null) {
-                    return null;
-                }
-                ComponentName admin = dpm.getProfileOwner();
-                if (admin == null) {
-                    admin = dpm.getDeviceOwnerComponentOnCallingUser();
-                }
-                return new EnforcedAdmin(admin, UserHandle.myUserId());
+                return getProfileOrDeviceOwner(context, userId);
             }
         } catch (RemoteException e) {
             // Nothing to do
@@ -238,7 +229,7 @@
         try {
             ApplicationInfo ai = ipm.getApplicationInfo(packageName, 0, userId);
             if (ai != null && ((ai.flags & ApplicationInfo.FLAG_SUSPENDED) != 0)) {
-                return getProfileOrDeviceOwnerOnCallingUser(context);
+                return getProfileOrDeviceOwner(context, userId);
             }
         } catch (RemoteException e) {
             // Nothing to do
@@ -246,6 +237,80 @@
         return null;
     }
 
+    public static EnforcedAdmin checkIfInputMethodDisallowed(Context context,
+            String packageName, int userId) {
+        DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
+                Context.DEVICE_POLICY_SERVICE);
+        if (dpm == null) {
+            return null;
+        }
+        EnforcedAdmin admin = getProfileOrDeviceOwner(context, userId);
+        boolean permitted = true;
+        if (admin != null) {
+            permitted = dpm.isInputMethodPermittedByAdmin(admin.component,
+                    packageName, userId);
+        }
+        int managedProfileId = getManagedProfileId(context, userId);
+        EnforcedAdmin profileAdmin = getProfileOrDeviceOwner(context, managedProfileId);
+        boolean permittedByProfileAdmin = true;
+        if (profileAdmin != null) {
+            permittedByProfileAdmin = dpm.isInputMethodPermittedByAdmin(profileAdmin.component,
+                    packageName, managedProfileId);
+        }
+        if (!permitted && !permittedByProfileAdmin) {
+            return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
+        } else if (!permitted) {
+            return admin;
+        } else if (!permittedByProfileAdmin) {
+            return profileAdmin;
+        }
+        return null;
+    }
+
+    public static EnforcedAdmin checkIfAccessibilityServiceDisallowed(Context context,
+            String packageName, int userId) {
+        DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
+                Context.DEVICE_POLICY_SERVICE);
+        if (dpm == null) {
+            return null;
+        }
+        EnforcedAdmin admin = getProfileOrDeviceOwner(context, userId);
+        boolean permitted = true;
+        if (admin != null) {
+            permitted = dpm.isAccessibilityServicePermittedByAdmin(admin.component,
+                    packageName, userId);
+        }
+        int managedProfileId = getManagedProfileId(context, userId);
+        EnforcedAdmin profileAdmin = getProfileOrDeviceOwner(context, managedProfileId);
+        boolean permittedByProfileAdmin = true;
+        if (profileAdmin != null) {
+            permittedByProfileAdmin = dpm.isAccessibilityServicePermittedByAdmin(
+                    profileAdmin.component, packageName, managedProfileId);
+        }
+        if (!permitted && !permittedByProfileAdmin) {
+            return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
+        } else if (!permitted) {
+            return admin;
+        } else if (!permittedByProfileAdmin) {
+            return profileAdmin;
+        }
+        return null;
+    }
+
+    private static int getManagedProfileId(Context context, int userId) {
+        UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        List<UserInfo> userProfiles = um.getProfiles(userId);
+        for (UserInfo uInfo : userProfiles) {
+            if (uInfo.id == userId) {
+                continue;
+            }
+            if (uInfo.isManagedProfile()) {
+                return uInfo.id;
+            }
+        }
+        return UserHandle.USER_NULL;
+    }
+
     /**
      * Check if account management for a specific type of account is disabled by admin.
      * Only a profile or device owner can disable account management. So, we check if account
@@ -255,7 +320,7 @@
      * or {@code null} if the account management is not disabled.
      */
     public static EnforcedAdmin checkIfAccountManagementDisabled(Context context,
-            String accountType) {
+            String accountType, int userId) {
         if (accountType == null) {
             return null;
         }
@@ -265,7 +330,7 @@
             return null;
         }
         boolean isAccountTypeDisabled = false;
-        String[] disabledTypes = dpm.getAccountTypesWithManagementDisabled();
+        String[] disabledTypes = dpm.getAccountTypesWithManagementDisabledAsUser(userId);
         for (String type : disabledTypes) {
             if (accountType.equals(type)) {
                 isAccountTypeDisabled = true;
@@ -275,7 +340,7 @@
         if (!isAccountTypeDisabled) {
             return null;
         }
-        return getProfileOrDeviceOwnerOnCallingUser(context);
+        return getProfileOrDeviceOwner(context, userId);
     }
 
     /**
@@ -296,7 +361,7 @@
     }
 
     /**
-     * Checks if an admin has enforced minimum password quality requirements on the device.
+     * Checks if an admin has enforced minimum password quality requirements on the given user.
      *
      * @return EnforcedAdmin Object containing the enforced admin component and admin user details,
      * or {@code null} if no quality requirements are set. If the requirements are set by
@@ -304,35 +369,73 @@
      * {@link UserHandle#USER_NULL}.
      *
      */
-    public static EnforcedAdmin checkIfPasswordQualityIsSet(Context context) {
+    public static EnforcedAdmin checkIfPasswordQualityIsSet(Context context, int userId) {
         final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
                 Context.DEVICE_POLICY_SERVICE);
         if (dpm == null) {
             return null;
         }
-        boolean isDisabledByMultipleAdmins = false;
-        ComponentName adminComponent = null;
-        List<ComponentName> admins = dpm.getActiveAdmins();
-        int quality;
-        if (admins != null) {
+
+        LockPatternUtils lockPatternUtils = new LockPatternUtils(context);
+        EnforcedAdmin enforcedAdmin = null;
+        if (lockPatternUtils.isSeparateProfileChallengeEnabled(userId)) {
+            // userId is managed profile and has a separate challenge, only consider
+            // the admins in that user.
+            final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userId);
+            if (admins == null) {
+                return null;
+            }
             for (ComponentName admin : admins) {
-                quality = dpm.getPasswordQuality(admin);
-                if (quality >= DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
-                    if (adminComponent == null) {
-                        adminComponent = admin;
+                if (dpm.getPasswordQuality(admin, userId)
+                        > DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+                    if (enforcedAdmin == null) {
+                        enforcedAdmin = new EnforcedAdmin(admin, userId);
                     } else {
-                        isDisabledByMultipleAdmins = true;
-                        break;
+                        return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
                     }
                 }
             }
-        }
-        EnforcedAdmin enforcedAdmin = null;
-        if (adminComponent != null) {
-            if (!isDisabledByMultipleAdmins) {
-                enforcedAdmin = new EnforcedAdmin(adminComponent, UserHandle.myUserId());
-            } else {
-                enforcedAdmin = new EnforcedAdmin();
+        } else {
+            // Return all admins for this user and the profiles that are visible from this
+            // user that do not use a separate work challenge.
+            final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
+            for (UserInfo userInfo : um.getProfiles(userId)) {
+                final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userInfo.id);
+                if (admins == null) {
+                    continue;
+                }
+                final boolean isSeparateProfileChallengeEnabled =
+                        lockPatternUtils.isSeparateProfileChallengeEnabled(userInfo.id);
+                for (ComponentName admin : admins) {
+                    if (!isSeparateProfileChallengeEnabled) {
+                        if (dpm.getPasswordQuality(admin, userInfo.id)
+                                > DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+                            if (enforcedAdmin == null) {
+                                enforcedAdmin = new EnforcedAdmin(admin, userInfo.id);
+                            } else {
+                                return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
+                            }
+                            // This same admins could have set policies both on the managed profile
+                            // and on the parent. So, if the admin has set the policy on the
+                            // managed profile here, we don't need to further check if that admin
+                            // has set policy on the parent admin.
+                            continue;
+                        }
+                    }
+                    if (userInfo.isManagedProfile()) {
+                        // If userInfo.id is a managed profile, we also need to look at
+                        // the policies set on the parent.
+                        DevicePolicyManager parentDpm = dpm.getParentProfileInstance(userInfo);
+                        if (parentDpm.getPasswordQuality(admin, userInfo.id)
+                                > DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+                            if (enforcedAdmin == null) {
+                                enforcedAdmin = new EnforcedAdmin(admin, userInfo.id);
+                            } else {
+                                return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
+                            }
+                        }
+                    }
+                }
             }
         }
         return enforcedAdmin;
@@ -352,7 +455,8 @@
         EnforcedAdmin enforcedAdmin = null;
         final int userId = UserHandle.myUserId();
         if (lockPatternUtils.isSeparateProfileChallengeEnabled(userId)) {
-            // If the user has a separate challenge, only consider the admins in that user.
+            // userId is managed profile and has a separate challenge, only consider
+            // the admins in that user.
             final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userId);
             if (admins == null) {
                 return null;
@@ -373,7 +477,7 @@
             for (UserInfo userInfo : um.getProfiles(userId)) {
                 final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userInfo.id);
                 if (admins == null) {
-                    return null;
+                    continue;
                 }
                 final boolean isSeparateProfileChallengeEnabled =
                         lockPatternUtils.isSeparateProfileChallengeEnabled(userInfo.id);
@@ -410,19 +514,24 @@
         return enforcedAdmin;
     }
 
-    public static EnforcedAdmin getProfileOrDeviceOwnerOnCallingUser(Context context) {
+    public static EnforcedAdmin getProfileOrDeviceOwner(Context context, int userId) {
+        if (userId == UserHandle.USER_NULL) {
+            return null;
+        }
         final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
                 Context.DEVICE_POLICY_SERVICE);
         if (dpm == null) {
             return null;
         }
-        ComponentName adminComponent = dpm.getDeviceOwnerComponentOnCallingUser();
+        ComponentName adminComponent = dpm.getProfileOwnerAsUser(userId);
         if (adminComponent != null) {
-            return new EnforcedAdmin(adminComponent, UserHandle.myUserId());
+            return new EnforcedAdmin(adminComponent, userId);
         }
-        adminComponent = dpm.getProfileOwner();
-        if (adminComponent != null) {
-            return new EnforcedAdmin(adminComponent, UserHandle.myUserId());
+        if (dpm.getDeviceOwnerUserId() == userId) {
+            adminComponent = dpm.getDeviceOwnerComponentOnAnyUser();
+            if (adminComponent != null) {
+                return new EnforcedAdmin(adminComponent, userId);
+            }
         }
         return null;
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java b/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java
index f5a2aae..d368de9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java
+++ b/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java
@@ -15,40 +15,19 @@
  */
 package com.android.settingslib;
 
-import android.app.ActivityManager;
-import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.content.res.Resources;
-import android.net.ConnectivityManager;
 import android.net.wifi.WifiManager;
 import android.os.SystemProperties;
-import android.os.UserManager;
-import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
 
 public class TetherUtil {
 
-    // Extras used for communicating with the TetherService.
-    public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType";
-    public static final String EXTRA_REM_TETHER_TYPE = "extraRemTetherType";
-    public static final String EXTRA_SET_ALARM = "extraSetAlarm";
-    /**
-     * Tells the service to run a provision check now.
-     */
-    public static final String EXTRA_RUN_PROVISION = "extraRunProvision";
-
     public static boolean setWifiTethering(boolean enable, Context context) {
         final WifiManager wifiManager =
                 (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         return wifiManager.setWifiApEnabled(null, enable);
     }
 
-    public static boolean isWifiTetherEnabled(Context context) {
-        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
-        return wifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_ENABLED;
-    }
-
     private static boolean isEntitlementCheckRequired(Context context) {
         final CarrierConfigManager configManager = (CarrierConfigManager) context
              .getSystemService(Context.CARRIER_CONFIG_SERVICE);
@@ -71,13 +50,4 @@
         }
         return (provisionApp.length == 2);
     }
-
-    public static boolean isTetheringSupported(Context context) {
-        final ConnectivityManager cm =
-                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        final boolean isAdminUser =
-                UserManager.get(context).isUserAdmin(ActivityManager.getCurrentUser());
-        return isAdminUser && cm.isTetheringSupported();
-    }
-
 }
diff --git a/packages/SystemUI/res/anim/recents_from_app_enter.xml b/packages/SystemUI/res/anim/recents_from_app_enter.xml
deleted file mode 100644
index 10ddce6..0000000
--- a/packages/SystemUI/res/anim/recents_from_app_enter.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-<!-- Recents Activity -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:shareInterpolator="false"
-     android:zAdjustment="top">
-  <alpha android:fromAlpha="1.0" android:toAlpha="1.0"
-         android:fillEnabled="true"
-         android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/fast_out_slow_in"
-         android:duration="0"/>
-</set>
diff --git a/packages/SystemUI/res/anim/recents_from_app_exit.xml b/packages/SystemUI/res/anim/recents_from_app_exit.xml
deleted file mode 100644
index c98ecf4..0000000
--- a/packages/SystemUI/res/anim/recents_from_app_exit.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-<!-- Incoming Activity -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:shareInterpolator="false"
-     android:zAdjustment="normal">
-
-    <!-- Animate the view out only after recents is visible -->
-    <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
-           android:fillEnabled="true"
-           android:fillBefore="true" android:fillAfter="true"
-           android:interpolator="@android:interpolator/fast_out_slow_in"
-           android:duration="1"/>
-</set>
diff --git a/packages/SystemUI/res/anim/recents_from_launcher_enter.xml b/packages/SystemUI/res/anim/recents_from_launcher_enter.xml
index b191e62..00b3dfd 100644
--- a/packages/SystemUI/res/anim/recents_from_launcher_enter.xml
+++ b/packages/SystemUI/res/anim/recents_from_launcher_enter.xml
@@ -23,6 +23,6 @@
   <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
          android:fillEnabled="true"
          android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/linear"
+         android:interpolator="@android:interpolator/linear_out_slow_in"
          android:duration="150"/>
 </set>
diff --git a/packages/SystemUI/res/anim/recents_from_launcher_exit.xml b/packages/SystemUI/res/anim/recents_from_launcher_exit.xml
index fa6caf2..33831b8 100644
--- a/packages/SystemUI/res/anim/recents_from_launcher_exit.xml
+++ b/packages/SystemUI/res/anim/recents_from_launcher_exit.xml
@@ -23,6 +23,6 @@
   <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
          android:fillEnabled="true"
          android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/linear_out_slow_in"
-         android:duration="150"/>
+         android:interpolator="@interpolator/recents_from_launcher_exit_interpolator"
+         android:duration="133"/>
 </set>
diff --git a/packages/SystemUI/res/anim/recents_from_search_launcher_exit.xml b/packages/SystemUI/res/anim/recents_from_search_launcher_exit.xml
index e0e2fc8..23cedf8 100644
--- a/packages/SystemUI/res/anim/recents_from_search_launcher_exit.xml
+++ b/packages/SystemUI/res/anim/recents_from_search_launcher_exit.xml
@@ -23,6 +23,6 @@
   <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
          android:fillEnabled="true"
          android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/linear_out_slow_in"
-         android:duration="@integer/recents_enter_from_home_transition_duration"/>
+         android:interpolator="@interpolator/recents_from_launcher_exit_interpolator"
+         android:duration="133"/>
 </set>
diff --git a/packages/SystemUI/res/anim/recents_launch_from_launcher_enter.xml b/packages/SystemUI/res/anim/recents_launch_from_launcher_enter.xml
deleted file mode 100644
index 1135bc0..0000000
--- a/packages/SystemUI/res/anim/recents_launch_from_launcher_enter.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:shareInterpolator="false"
-     android:zAdjustment="normal">
-  <!--scale android:fromXScale="2.0" android:toXScale="1.0"
-         android:fromYScale="2.0" android:toYScale="1.0"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:fillEnabled="true"
-         android:fillBefore="true" android:fillAfter="true"
-         android:pivotX="50%p" android:pivotY="50%p"
-         android:duration="250" /-->
-</set>
diff --git a/packages/SystemUI/res/anim/recents_launch_from_launcher_exit.xml b/packages/SystemUI/res/anim/recents_launch_from_launcher_exit.xml
deleted file mode 100644
index fa28cf4..0000000
--- a/packages/SystemUI/res/anim/recents_launch_from_launcher_exit.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:shareInterpolator="false"
-     android:zAdjustment="top">
-  <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
-         android:fillEnabled="true"
-         android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:duration="250"/>
-</set>
diff --git a/packages/SystemUI/res/anim/recents_return_to_launcher_exit.xml b/packages/SystemUI/res/anim/recents_return_to_launcher_exit.xml
deleted file mode 100644
index e95e667..0000000
--- a/packages/SystemUI/res/anim/recents_return_to_launcher_exit.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:shareInterpolator="false"
-     android:zAdjustment="normal">
-  <!--scale android:fromXScale="1.0" android:toXScale="2.0"
-         android:fromYScale="1.0" android:toYScale="2.0"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:pivotX="50%p" android:pivotY="50%p"
-         android:duration="250" /-->
-  <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:duration="250"/>
-</set>
diff --git a/packages/SystemUI/res/anim/recents_to_launcher_enter.xml b/packages/SystemUI/res/anim/recents_to_launcher_enter.xml
index b191e62..544ec88 100644
--- a/packages/SystemUI/res/anim/recents_to_launcher_enter.xml
+++ b/packages/SystemUI/res/anim/recents_to_launcher_enter.xml
@@ -23,6 +23,6 @@
   <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
          android:fillEnabled="true"
          android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/linear"
-         android:duration="150"/>
+         android:interpolator="@interpolator/recents_to_launcher_enter_interpolator"
+         android:duration="133"/>
 </set>
diff --git a/packages/SystemUI/res/anim/recents_to_launcher_exit.xml b/packages/SystemUI/res/anim/recents_to_launcher_exit.xml
index fa6caf2..226edb8 100644
--- a/packages/SystemUI/res/anim/recents_to_launcher_exit.xml
+++ b/packages/SystemUI/res/anim/recents_to_launcher_exit.xml
@@ -24,5 +24,5 @@
          android:fillEnabled="true"
          android:fillBefore="true" android:fillAfter="true"
          android:interpolator="@android:interpolator/linear_out_slow_in"
-         android:duration="150"/>
+         android:duration="1"/>
 </set>
diff --git a/packages/SystemUI/res/anim/recents_to_search_launcher_enter.xml b/packages/SystemUI/res/anim/recents_to_search_launcher_enter.xml
index ea82835..657b216 100644
--- a/packages/SystemUI/res/anim/recents_to_search_launcher_enter.xml
+++ b/packages/SystemUI/res/anim/recents_to_search_launcher_enter.xml
@@ -23,6 +23,6 @@
   <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
          android:fillEnabled="true"
          android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/linear"
-         android:duration="100"/>
+         android:interpolator="@interpolator/recents_to_launcher_enter_interpolator"
+         android:duration="133"/>
 </set>
diff --git a/packages/SystemUI/res/anim/recents_to_search_launcher_exit.xml b/packages/SystemUI/res/anim/recents_to_search_launcher_exit.xml
index a8bdc8e..5182cab2 100644
--- a/packages/SystemUI/res/anim/recents_to_search_launcher_exit.xml
+++ b/packages/SystemUI/res/anim/recents_to_search_launcher_exit.xml
@@ -24,5 +24,5 @@
          android:fillEnabled="true"
          android:fillBefore="true" android:fillAfter="true"
          android:interpolator="@android:interpolator/linear"
-         android:duration="100"/>
+         android:duration="1"/>
 </set>
diff --git a/packages/SystemUI/res/anim/wallpaper_recents_launch_from_launcher_enter.xml b/packages/SystemUI/res/anim/wallpaper_recents_launch_from_launcher_enter.xml
deleted file mode 100644
index 73ae9f2..0000000
--- a/packages/SystemUI/res/anim/wallpaper_recents_launch_from_launcher_enter.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:detachWallpaper="true"
-     android:shareInterpolator="false"
-     android:zAdjustment="normal">
-  <!--scale android:fromXScale="2.0" android:toXScale="1.0"
-         android:fromYScale="2.0" android:toYScale="1.0"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:fillEnabled="true"
-         android:fillBefore="true" android:fillAfter="true"
-         android:pivotX="50%p" android:pivotY="50%p"
-         android:duration="250" /-->
-  <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
-         android:fillEnabled="true"
-         android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:duration="250"/>
-</set>
diff --git a/packages/SystemUI/res/anim/wallpaper_recents_launch_from_launcher_exit.xml b/packages/SystemUI/res/anim/wallpaper_recents_launch_from_launcher_exit.xml
deleted file mode 100644
index 7e257d9..0000000
--- a/packages/SystemUI/res/anim/wallpaper_recents_launch_from_launcher_exit.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:detachWallpaper="true"
-     android:shareInterpolator="false"
-     android:zAdjustment="top">
-  <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
-         android:fillEnabled="true"
-         android:fillBefore="true" android:fillAfter="true"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:duration="250"/>
-</set>
diff --git a/packages/SystemUI/res/anim/recents_return_to_launcher_enter.xml b/packages/SystemUI/res/interpolator/recents_from_launcher_exit_interpolator.xml
similarity index 62%
rename from packages/SystemUI/res/anim/recents_return_to_launcher_enter.xml
rename to packages/SystemUI/res/interpolator/recents_from_launcher_exit_interpolator.xml
index efa9019..4a7fff6 100644
--- a/packages/SystemUI/res/anim/recents_return_to_launcher_enter.xml
+++ b/packages/SystemUI/res/interpolator/recents_from_launcher_exit_interpolator.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 /*
-** Copyright 2012, The Android Open Source Project
+** Copyright 2014, 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.
@@ -16,11 +16,8 @@
 ** limitations under the License.
 */
 -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:shareInterpolator="false"
-     android:zAdjustment="normal">
-  <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:duration="250"/>
-</set>
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0"
+    android:controlY1="0"
+    android:controlX2="0.8"
+    android:controlY2="1" />
diff --git a/packages/SystemUI/res/anim/recents_return_to_launcher_enter.xml b/packages/SystemUI/res/interpolator/recents_to_launcher_enter_interpolator.xml
similarity index 62%
copy from packages/SystemUI/res/anim/recents_return_to_launcher_enter.xml
copy to packages/SystemUI/res/interpolator/recents_to_launcher_enter_interpolator.xml
index efa9019..c61dfd8 100644
--- a/packages/SystemUI/res/anim/recents_return_to_launcher_enter.xml
+++ b/packages/SystemUI/res/interpolator/recents_to_launcher_enter_interpolator.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 /*
-** Copyright 2012, The Android Open Source Project
+** Copyright 2014, 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.
@@ -16,11 +16,8 @@
 ** limitations under the License.
 */
 -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:shareInterpolator="false"
-     android:zAdjustment="normal">
-  <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
-         android:interpolator="@android:interpolator/decelerate_cubic"
-         android:duration="250"/>
-</set>
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0.4"
+    android:controlY1="0"
+    android:controlX2="1"
+    android:controlY2="1" />
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_wrapper.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_category_separator.xml
similarity index 66%
rename from packages/SystemUI/res/layout/keyboard_shortcuts_wrapper.xml
rename to packages/SystemUI/res/layout/keyboard_shortcuts_category_separator.xml
index 802acfe..778ef8f 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_wrapper.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_category_separator.xml
@@ -15,8 +15,11 @@
   ~ 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">
-</LinearLayout>
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:layout_marginStart="24dp"
+    android:layout_marginTop="8dp"
+    android:layout_marginEnd="0dp"
+    android:layout_marginBottom="20dp"
+    android:background="?android:attr/dividerHorizontal" />
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_container.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_container.xml
index fa07eb1..7ca3b95 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_container.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_container.xml
@@ -16,7 +16,7 @@
   -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:orientation="horizontal"
-              android:layout_width="match_parent"
-              android:layout_height="wrap_content">
-</LinearLayout>
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content" />
+
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_view.xml
index 77b1264..f73ee15 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_view.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_view.xml
@@ -14,25 +14,35 @@
   ~ 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:id="@+id/keyboard_shortcuts_wrapper"
-        android:layout_width="488dp"
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="488dp"
+    android:layout_height="wrap_content">
+    <LinearLayout
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:focusable="true">
-    <ScrollView
+        android:orientation="vertical">
+        <ScrollView
             android:id="@+id/keyboard_shortcuts_scroll_view"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1">
-        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+            <LinearLayout
                 android:id="@+id/keyboard_shortcuts_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical"/>
-    </ScrollView>
-    <View
+        </ScrollView>
+        <!-- Required for stretching to full available height when the items in the scroll view
+             occupy less space then the full height -->
+        <View
             android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:background="?android:attr/listDivider"/>
-</LinearLayout>
+            android:layout_height="0dp"
+            android:layout_weight="1"/>
+    </LinearLayout>
+    <View
+        android:layout_gravity="bottom"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?android:attr/dividerHorizontal"/>
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/qs_add_tile_layout.xml b/packages/SystemUI/res/layout/qs_add_tile_layout.xml
deleted file mode 100644
index 962b00e..0000000
--- a/packages/SystemUI/res/layout/qs_add_tile_layout.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?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.
--->
-
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_height="wrap_content"
-    android:layout_width="wrap_content"
-    android:paddingTop="20dp"
-    android:paddingStart="7dp"
-    android:paddingEnd="7dp">
-    <LinearLayout
-        android:layout_height="wrap_content"
-        android:layout_width="80dp"
-        android:orientation="vertical">
-        <ImageView
-            android:id="@+id/tile_icon"
-            android:layout_gravity="center"
-            android:layout_width="@dimen/qs_tile_icon_size"
-            android:layout_height="@dimen/qs_tile_icon_size" />
-        <TextView
-            android:id="@+id/tile_label"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center|bottom"
-            android:paddingTop="10dp"
-            android:gravity="center"
-            android:textSize="@dimen/qs_tile_text_size" />
-    </LinearLayout>
-</FrameLayout>
diff --git a/packages/SystemUI/res/layout/qs_customize_layout.xml b/packages/SystemUI/res/layout/qs_customize_divider.xml
similarity index 63%
rename from packages/SystemUI/res/layout/qs_customize_layout.xml
rename to packages/SystemUI/res/layout/qs_customize_divider.xml
index 0b8e02f..71ad85b 100644
--- a/packages/SystemUI/res/layout/qs_customize_layout.xml
+++ b/packages/SystemUI/res/layout/qs_customize_divider.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-     Copyright (C) 2015 The Android Open Source Project
+     Copyright (C) 2016 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.
@@ -14,18 +14,16 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.systemui.qs.customize.NonPagedTileLayout
+
+<TextView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/tiles_container"
+    android:id="@android:id/title"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:orientation="vertical">
-
-    <view
-        class="com.android.systemui.qs.PagedTileLayout$TilePage"
-        android:id="@+id/tile_page"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-</com.android.systemui.qs.customize.NonPagedTileLayout>
-
+    android:paddingTop="20dp"
+    android:paddingStart="16dp"
+    android:paddingEnd="8dp"
+    android:paddingBottom="13dp"
+    android:textAppearance="@android:style/TextAppearance.Material.Body2"
+    android:textColor="?android:attr/colorAccent"
+    android:text="@string/drag_to_add_tiles" />
diff --git a/packages/SystemUI/res/layout/qs_customize_panel.xml b/packages/SystemUI/res/layout/qs_customize_panel.xml
index e56431b..73a92d9 100644
--- a/packages/SystemUI/res/layout/qs_customize_panel.xml
+++ b/packages/SystemUI/res/layout/qs_customize_panel.xml
@@ -22,85 +22,53 @@
     android:background="@drawable/qs_customizer_background"
     android:gravity="center_horizontal">
 
-    <FrameLayout
+    <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:background="@drawable/notification_header_bg">
+        android:paddingTop="28dp"
+        android:paddingEnd="8dp">
 
-        <LinearLayout
-            android:id="@+id/drag_buttons"
-            android:layout_width="match_parent"
-            android:layout_height="fill_parent"
-            android:orientation="horizontal">
-            <FrameLayout
-                android:layout_width="0dp"
-                android:layout_height="fill_parent"
-                android:layout_weight="1">
-                <com.android.systemui.qs.customize.DropButton
-                    android:id="@+id/info_button"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center"
-                    android:gravity="center"
-                    android:drawableStart="@drawable/ic_info"
-                    android:drawablePadding="10dp"
-                    android:textAppearance="?android:attr/textAppearanceMedium"
-                    android:textColor="@android:color/white"
-                    android:text="@string/qs_customize_info" />
-            </FrameLayout>
-            <FrameLayout
-                android:layout_width="0dp"
-                android:layout_height="fill_parent"
-                android:layout_weight="1">
-                <com.android.systemui.qs.customize.DropButton
-                    android:id="@+id/remove_button"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center"
-                    android:gravity="center"
-                    android:drawableStart="@drawable/ic_close_white"
-                    android:drawablePadding="10dp"
-                    android:textAppearance="?android:attr/textAppearanceMedium"
-                    android:textColor="@android:color/white"
-                    android:text="@string/qs_customize_remove" />
-            </FrameLayout>
-        </LinearLayout>
+        <ImageView
+            android:id="@+id/close"
+            android:layout_width="56dp"
+            android:layout_height="56dp"
+            android:padding="16dp"
+            android:src="@drawable/ic_close_white" />
 
-        <Toolbar
-            android:id="@*android:id/action_bar"
-            android:layout_width="match_parent"
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="1" />
+
+        <Button
+            android:id="@+id/save"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:navigationContentDescription="@*android:string/action_bar_up_description"
-            android:background="@drawable/notification_header_bg"
-            style="?android:attr/toolbarStyle" />
-    </FrameLayout>
+            android:paddingStart="12dp"
+            android:paddingEnd="12dp"
+            android:background="?android:attr/selectableItemBackground"
+            android:textAppearance="@android:style/TextAppearance.Material.Widget.Button.Inverse"
+            android:textColor="?android:attr/colorAccent"
+            android:text="@string/save" />
 
-    <com.android.systemui.tuner.AutoScrollView
+        <Button
+            android:id="@+id/reset"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingStart="12dp"
+            android:paddingEnd="12dp"
+            android:background="?android:attr/selectableItemBackground"
+            android:textAppearance="@android:style/TextAppearance.Material.Widget.Button.Inverse"
+            android:textColor="?android:attr/colorAccent"
+            android:text="@*android:string/reset" />
+
+    </LinearLayout>
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@android:id/list"
         android:layout_width="@dimen/notification_panel_width"
         android:layout_height="0dp"
-        android:layout_weight="1"
-        android:paddingTop="12dp"
-        android:paddingBottom="8dp"
-        android:elevation="2dp">
-
-        <com.android.systemui.qs.customize.CustomQSPanel
-            android:id="@+id/quick_settings_panel"
-            android:background="#0000"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
-
-    </com.android.systemui.tuner.AutoScrollView>
-
-    <com.android.systemui.qs.customize.FloatingActionButton
-        android:id="@+id/fab"
-        android:clickable="true"
-        android:layout_width="@dimen/fab_size"
-        android:layout_height="@dimen/fab_size"
-        android:layout_gravity="bottom|end"
-        android:layout_marginEnd="@dimen/fab_margin"
-        android:layout_marginBottom="@dimen/fab_margin"
-        android:elevation="@dimen/fab_elevation"
-        android:background="@drawable/fab_background" />
+        android:layout_weight="1" />
 
     <View
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/qs_customize_tile_frame.xml b/packages/SystemUI/res/layout/qs_customize_tile_frame.xml
new file mode 100644
index 0000000..aaa84fd
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_customize_tile_frame.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2016 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.
+-->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:paddingStart="8dp"
+    android:paddingEnd="8dp"
+    android:paddingBottom="16dp" />
diff --git a/packages/SystemUI/res/layout/status_bar_no_notifications.xml b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
index dd501d4..6f87184 100644
--- a/packages/SystemUI/res/layout/status_bar_no_notifications.xml
+++ b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
@@ -25,9 +25,9 @@
             android:id="@+id/no_notifications"
             android:layout_width="match_parent"
             android:layout_height="64dp"
-            android:paddingTop="12dp"
+            android:paddingTop="28dp"
             android:gravity="top|center_horizontal"
-            android:textColor="#ffffff"
-            android:textSize="20sp"
+            android:textColor="#e6ffffff"
+            android:textSize="16sp"
             android:text="@string/empty_shade_text"/>
 </com.android.systemui.statusbar.EmptyShadeView>
diff --git a/packages/SystemUI/res/layout/status_bar_notification_dismiss_all.xml b/packages/SystemUI/res/layout/status_bar_notification_dismiss_all.xml
index dc7577a..efb273f 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_dismiss_all.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_dismiss_all.xml
@@ -19,15 +19,16 @@
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:visibility="gone"
-        android:clipChildren="false"
-        android:clipToPadding="false">
+        android:paddingEnd="8dp"
+        android:visibility="gone">
     <com.android.systemui.statusbar.DismissViewButton
+            style="@android:style/Widget.Material.Button.Borderless"
             android:id="@+id/dismiss_text"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
             android:layout_gravity="end"
             android:focusable="true"
-            android:background="@drawable/ripple_drawable"
-            android:contentDescription="@string/accessibility_clear_all"/>
+            android:contentDescription="@string/accessibility_clear_all"
+            android:text="@string/clear_all_notifications_text"
+            android:textAllCaps="true"/>
 </com.android.systemui.statusbar.DismissView>
diff --git a/packages/SystemUI/res/values-land/config.xml b/packages/SystemUI/res/values-land/config.xml
index e0affa1..43e7bac 100644
--- a/packages/SystemUI/res/values-land/config.xml
+++ b/packages/SystemUI/res/values-land/config.xml
@@ -38,6 +38,4 @@
          while the stack is not focused. -->
     <item name="recents_layout_unfocused_range_min" format="float" type="integer">-2</item>
     <item name="recents_layout_unfocused_range_max" format="float" type="integer">1.5</item>
-
-    <integer name="quick_settings_num_columns">4</integer>
 </resources>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index c75a89f..26a81c8 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -19,8 +19,7 @@
     <!-- thickness (width) of the navigation bar on phones that require it -->
     <dimen name="navigation_bar_size">@*android:dimen/navigation_bar_width</dimen>
 
-    <!-- Standard notification width + gravity -->
-    <dimen name="notification_panel_width">@dimen/standard_notification_panel_width</dimen>
+    <!-- Standard notification gravity -->
     <integer name="notification_panel_layout_gravity">@integer/standard_notification_panel_layout_gravity</integer>
 
     <dimen name="docked_divider_handle_width">2dp</dimen>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 71f92fd..c0652d8 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -93,4 +93,6 @@
 
     <dimen name="navigation_key_width">128dp</dimen>
     <dimen name="navigation_key_padding">25dp</dimen>
+
+    <dimen name="qs_expand_margin">0dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-w550dp-land/config.xml b/packages/SystemUI/res/values-w550dp-land/config.xml
new file mode 100644
index 0000000..71e54a1
--- /dev/null
+++ b/packages/SystemUI/res/values-w550dp-land/config.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, 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.
+*/
+-->
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds. -->
+<resources>
+    <integer name="quick_settings_num_columns">4</integer>
+</resources>
diff --git a/packages/SystemUI/res/values-w550dp-land/dimens.xml b/packages/SystemUI/res/values-w550dp-land/dimens.xml
new file mode 100644
index 0000000..4160c83
--- /dev/null
+++ b/packages/SystemUI/res/values-w550dp-land/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (c) 2016, 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.
+*/
+-->
+<resources>
+    <!-- Standard notification width + gravity -->
+    <dimen name="notification_panel_width">544dp</dimen>
+
+    <dimen name="qs_expand_margin">32dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 9bb6dc6..a9b8df2 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -158,5 +158,4 @@
     <!-- Keyboard shortcuts colors -->
     <color name="ksh_system_group_color">#ff00bcd4</color>
     <color name="ksh_application_group_color">#fff44336</color>
-    <color name="ksh_dialog_background_color">#ffffffff</color>
 </resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e8df01b..a6ba8b5 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -103,7 +103,7 @@
 
     <!-- The default tiles to display in QuickSettings -->
     <string name="quick_settings_tiles_default" translatable="false">
-        wifi,bt,flashlight,dnd,cell,battery,rotation,airplane,location,cast,work
+        wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location
     </string>
 
     <!-- The tiles to display in QuickSettings -->
@@ -138,12 +138,6 @@
     <!-- The duration in seconds to wait before the dismiss buttons are shown. -->
     <integer name="recents_task_bar_dismiss_delay_seconds">1000</integer>
 
-    <!-- The duration of the window transition when coming to Recents from an app.
-         In order to defer the in-app animations until after the transition is complete,
-         we also need to use this value as the starting delay when animating the first
-         task decorations in. -->
-    <integer name="recents_enter_from_app_transition_duration">325</integer>
-
     <!-- The duration for animating the task decorations in after transitioning from an app. -->
     <integer name="recents_task_enter_from_app_duration">200</integer>
 
@@ -153,12 +147,6 @@
     <!-- The duration for animating the task decorations out before transitioning to an app. -->
     <integer name="recents_task_exit_to_app_duration">125</integer>
 
-    <!-- The duration of the window transition when coming to Recents from the Launcher.
-         In order to defer the in-app animations until after the transition is complete,
-         we also need to use this value as the starting delay when animating the task views
-         in from the bottom of the screen. -->
-    <integer name="recents_enter_from_home_transition_duration">100</integer>
-
     <!-- The min animation duration for animating the nav bar scrim in. -->
     <integer name="recents_nav_bar_scrim_enter_duration">400</integer>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e79a82a..e5e5710 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -186,6 +186,7 @@
     <dimen name="qs_detail_empty_text_size">14sp</dimen>
     <dimen name="qs_data_usage_text_size">14sp</dimen>
     <dimen name="qs_data_usage_usage_text_size">36sp</dimen>
+    <dimen name="qs_expand_margin">0dp</dimen>
 
     <dimen name="segmented_button_spacing">0dp</dimen>
     <dimen name="borderless_button_radius">2dp</dimen>
@@ -606,4 +607,7 @@
 
     <dimen name="docked_divider_handle_width">16dp</dimen>
     <dimen name="docked_divider_handle_height">2dp</dimen>
+
+    <dimen name="battery_height">14.5dp</dimen>
+    <dimen name="battery_width">9.5dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c6c448d0..6ff9be1 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1044,9 +1044,6 @@
     <!-- VolumeUI restoration notification: text -->
     <string name="volumeui_notification_text">Touch to restore the original.</string>
 
-    <!-- Describes the way 2 names are concatenated. An example would be ", " to produce "Peter Muller, Paul Curry". Please also include a space here if it's appropriate in the language and if it's a RTL language include it on the left. The translation should start and end with " to keep the white space if desired [CHAR LIMIT=5] -->
-    <string name="group_summary_concadenation">", "</string>
-
     <!-- Toast shown when user unlocks screen and managed profile activity is in the foreground -->
     <string name="managed_profile_foreground_toast">You\'re using your work profile</string>
 
@@ -1187,6 +1184,11 @@
     <!-- Description for the toggle to set the initial scroll state to be paging or stack. DO NOT TRANSLATE -->
     <string name="overview_initial_state_paging_desc">Determines whether Overview will initially be in a stacked or paged state</string>
 
+    <!-- Toggle to enable the gesture to enter split-screen by swiping up from the Overview button. [CHAR LIMIT=60]-->
+    <string name="overview_nav_bar_gesture">Enable split-screen swipe-up accelerator</string>
+    <!-- Description for the toggle to enable the gesture to enter split-screen by swiping up from the Overview button. [CHAR LIMIT=NONE]-->
+    <string name="overview_nav_bar_gesture_desc">Enable gesture to enter split-screen by swiping up from the Overview button</string>
+
     <!-- Category in the System UI Tuner settings, where new/experimental
          settings are -->
     <string name="experimental">Experimental</string>
@@ -1400,4 +1402,7 @@
     <!-- SysUI Tuner: Label for preview area in navigation bar tuner [CHAR LIMIT=NONE] -->
     <string name="preview">Preview</string>
 
+    <!-- Label for area where tiles can be dragged out of [CHAR LIMIT=60] -->
+    <string name="drag_to_add_tiles">Drag to add tiles</string>
+
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 9931ab9..60a9fc2 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -16,10 +16,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="RecentsStyle" parent="@android:style/Theme.DeviceDefault.Wallpaper.NoTitleBar">
-        <item name="android:windowAnimationStyle">@style/Animation.RecentsActivity</item>
-    </style>
-
     <style name="RecentsTheme" parent="@android:style/Theme.Material">
         <!-- NoTitle -->
         <item name="android:windowNoTitle">true</item>
@@ -27,38 +23,23 @@
         <item name="android:statusBarColor">@android:color/transparent</item>
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:windowDrawsSystemBarBackgrounds">true</item>
-        <item name="android:windowAnimationStyle">@style/Animation.RecentsActivity</item>
+        <item name="android:windowAnimationStyle">@null</item>
         <item name="android:ambientShadowAlpha">0.35</item>
     </style>
 
 
-    <!-- Alternate Recents theme -->
+    <!-- Recents theme -->
     <style name="RecentsTheme.Wallpaper">
-        <!-- Wallpaper -->
         <item name="android:windowBackground">@color/transparent</item>
         <item name="android:colorBackgroundCacheHint">@null</item>
         <item name="android:windowShowWallpaper">true</item>
     </style>
 
-    <!-- Performance optimized alternate Recents theme (no wallpaper) -->
+    <!-- Performance optimized Recents theme (no wallpaper) -->
     <style name="RecentsTheme.NoWallpaper">
         <item name="android:windowBackground">@android:color/black</item>
     </style>
 
-    <!-- Animations for a non-full-screen window or activity. -->
-    <style name="Animation.RecentsActivity" parent="@android:style/Animation.Activity">
-        <item name="android:activityOpenEnterAnimation">@anim/recents_launch_from_launcher_enter</item>
-        <item name="android:activityOpenExitAnimation">@anim/recents_launch_from_launcher_exit</item>
-        <item name="android:taskOpenEnterAnimation">@anim/recents_launch_from_launcher_enter</item>
-        <item name="android:taskOpenExitAnimation">@anim/recents_launch_from_launcher_exit</item>
-        <item name="android:taskToFrontEnterAnimation">@anim/recents_launch_from_launcher_enter</item>
-        <item name="android:taskToFrontExitAnimation">@anim/recents_launch_from_launcher_exit</item>
-        <item name="android:wallpaperOpenEnterAnimation">@anim/recents_launch_from_launcher_enter</item>
-        <item name="android:wallpaperOpenExitAnimation">@anim/recents_launch_from_launcher_exit</item>
-        <item name="android:wallpaperIntraOpenEnterAnimation">@anim/wallpaper_recents_launch_from_launcher_enter</item>
-        <item name="android:wallpaperIntraOpenExitAnimation">@anim/wallpaper_recents_launch_from_launcher_exit</item>
-    </style>
-
     <style name="TextAppearance.StatusBar.HeadsUp"
         parent="@*android:style/TextAppearance.StatusBar">
     </style>
@@ -241,6 +222,10 @@
         parent="@*android:style/TextAppearance.Material.Notification.Info">
     </style>
 
+    <style name="TextAppearance.Material.Notification.HybridNotificationDivider"
+        parent="@*android:style/TextAppearance.Material.Notification">
+    </style>
+
     <style name="SearchPanelCircle">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml
index febe518..4de4ced 100644
--- a/packages/SystemUI/res/xml/tuner_prefs.xml
+++ b/packages/SystemUI/res/xml/tuner_prefs.xml
@@ -122,6 +122,11 @@
             android:title="@string/overview_fast_toggle_via_button"
             android:summary="@string/overview_fast_toggle_via_button_desc" />
 
+        <com.android.systemui.tuner.TunerSwitch
+            android:key="overview_nav_bar_gesture"
+            android:title="@string/overview_nav_bar_gesture"
+            android:summary="@string/overview_nav_bar_gesture_desc" />
+
     </PreferenceScreen>
 
     <SwitchPreference
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
index a0dbad4..454d1ce 100755
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
@@ -51,6 +51,8 @@
     private static final float BOLT_LEVEL_THRESHOLD = 0.3f;  // opaque bolt below this fraction
 
     private final int[] mColors;
+    private final int mIntrinsicWidth;
+    private final int mIntrinsicHeight;
 
     private boolean mShowPercent;
     private float mButtonHeightFraction;
@@ -161,12 +163,26 @@
         mLightModeBackgroundColor =
                 context.getColor(R.color.light_mode_icon_color_dual_tone_background);
         mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill);
+
+        mIntrinsicWidth = context.getResources().getDimensionPixelSize(R.dimen.battery_width);
+        mIntrinsicHeight = context.getResources().getDimensionPixelSize(R.dimen.battery_height);
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mIntrinsicHeight;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mIntrinsicWidth;
     }
 
     public void startListening() {
         mListening = true;
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
+        updateShowPercent();
         if (mDemoMode) return;
         mBatteryController.addStateChangedCallback(this);
     }
@@ -178,6 +194,11 @@
         mBatteryController.removeStateChangedCallback(this);
     }
 
+    public void disableShowPercent() {
+        mShowPercent = false;
+        postInvalidate();
+    }
+
     private void postInvalidate() {
         mHandler.post(new Runnable() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/Interpolators.java b/packages/SystemUI/src/com/android/systemui/Interpolators.java
index cd6dce0..5e33a9f 100644
--- a/packages/SystemUI/src/com/android/systemui/Interpolators.java
+++ b/packages/SystemUI/src/com/android/systemui/Interpolators.java
@@ -34,4 +34,10 @@
     public static final Interpolator LINEAR = new LinearInterpolator();
     public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator();
     public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
+
+    /**
+     * Interpolator to be used when animating a move based on a click. Pair with enough duration.
+     */
+    public static final Interpolator TOUCH_RESPONSE =
+            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSIconView.java b/packages/SystemUI/src/com/android/systemui/qs/QSIconView.java
index ed90904..1df372b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSIconView.java
@@ -24,6 +24,7 @@
 import android.view.ViewGroup;
 import android.widget.ImageView;
 
+import android.widget.ImageView.ScaleType;
 import com.android.systemui.R;
 
 import java.util.Objects;
@@ -96,7 +97,7 @@
     protected View createIcon() {
         final ImageView icon = new ImageView(mContext);
         icon.setId(android.R.id.icon);
-        icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+        icon.setScaleType(ScaleType.FIT_CENTER);
         return icon;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index 35000d3..d79f4d4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -76,7 +76,7 @@
 
     private String mTileSpec;
 
-    abstract protected TState newTileState();
+    public abstract TState newTileState();
     abstract protected void handleClick();
     abstract protected void handleUpdateState(TState state, Object arg);
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index e4b8a6c..f208470 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Configuration;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
@@ -102,6 +103,8 @@
 
     private static class HeaderTileLayout extends LinearLayout implements QSTileLayout {
 
+        private final ImageView mDownArrow;
+
         public HeaderTileLayout(Context context) {
             super(context);
             setClipChildren(false);
@@ -111,17 +114,31 @@
 
             int padding =
                     mContext.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding);
-            ImageView downArrow = new ImageView(context);
-            downArrow.setImageResource(R.drawable.ic_expand_more);
-            downArrow.setImageTintList(ColorStateList.valueOf(context.getResources().getColor(
+            mDownArrow = new ImageView(context);
+            mDownArrow.setImageResource(R.drawable.ic_expand_more);
+            mDownArrow.setImageTintList(ColorStateList.valueOf(context.getResources().getColor(
                     android.R.color.white, null)));
-            downArrow.setLayoutParams(generateLayoutParams());
-            downArrow.setPadding(padding, padding, padding, padding);
-            addView(downArrow);
+            mDownArrow.setLayoutParams(generateLayoutParams());
+            mDownArrow.setPadding(padding, padding, padding, padding);
+            updateDownArrowMargin();
+            addView(mDownArrow);
             setOrientation(LinearLayout.HORIZONTAL);
         }
 
         @Override
+        protected void onConfigurationChanged(Configuration newConfig) {
+            super.onConfigurationChanged(newConfig);
+            updateDownArrowMargin();
+        }
+
+        private void updateDownArrowMargin() {
+            LayoutParams params = (LayoutParams) mDownArrow.getLayoutParams();
+            params.setMarginStart(mContext.getResources().getDimensionPixelSize(
+                    R.dimen.qs_expand_margin));
+            mDownArrow.setLayoutParams(params);
+        }
+
+        @Override
         public void addTile(TileRecord tile) {
             addView(tile.tileView, getChildCount() - 1 /* Leave icon at end */,
                     generateLayoutParams());
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/BlankCustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/customize/BlankCustomTile.java
deleted file mode 100644
index 36bed0d..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/BlankCustomTile.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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.systemui.qs.customize;
-
-import android.content.ComponentName;
-import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
-import android.graphics.drawable.Drawable;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.MetricsProto.MetricsEvent;
-import com.android.systemui.R;
-import com.android.systemui.qs.QSTile;
-
-public class BlankCustomTile extends QSTile<QSTile.State> {
-    public static final String PREFIX = "custom(";
-
-    private final ComponentName mComponent;
-
-    private BlankCustomTile(Host host, String action) {
-        super(host);
-        mComponent = ComponentName.unflattenFromString(action);
-    }
-
-    public static QSTile<?> create(Host host, String spec) {
-        if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
-            throw new IllegalArgumentException("Bad custom tile spec: " + spec);
-        }
-        final String action = spec.substring(PREFIX.length(), spec.length() - 1);
-        if (action.isEmpty()) {
-            throw new IllegalArgumentException("Empty custom tile spec action");
-        }
-        return new BlankCustomTile(host, action);
-    }
-
-    @Override
-    public void setListening(boolean listening) {
-    }
-
-    @Override
-    protected State newTileState() {
-        return new State();
-    }
-
-    @Override
-    protected void handleUserSwitch(int newUserId) {
-        super.handleUserSwitch(newUserId);
-    }
-
-    @Override
-    protected void handleClick() {
-        MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
-    }
-
-    @Override
-    protected void handleLongClick() {
-    }
-
-    @Override
-    protected void handleUpdateState(State state, Object arg) {
-        try {
-            PackageManager pm = mContext.getPackageManager();
-            ServiceInfo info = pm.getServiceInfo(mComponent, 0);
-            Drawable drawable = info.loadIcon(pm);
-            drawable.setTint(mContext.getColor(R.color.qs_tile_tint_active));
-            state.icon = new DrawableIcon(drawable);
-            state.label = info.loadLabel(pm).toString();
-            state.contentDescription = state.label;
-        } catch (Exception e) {
-        }
-    }
-
-    @Override
-    public int getMetricsCategory() {
-        return MetricsEvent.QS_INTENT;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java
deleted file mode 100644
index 286748b..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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.systemui.qs.customize;
-
-import android.app.ActivityManager;
-import android.content.ClipData;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.provider.Settings.Secure;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import com.android.systemui.R;
-import com.android.systemui.qs.QSPanel;
-import com.android.systemui.qs.QSTile;
-import com.android.systemui.qs.external.CustomTile;
-import com.android.systemui.qs.external.TileLifecycleManager;
-import com.android.systemui.statusbar.phone.QSTileHost;
-import com.android.systemui.tuner.TunerService;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * A version of QSPanel that allows tiles to be dragged around rather than
- * clicked on.  Dragging starting and receiving is handled in the NonPagedTileLayout,
- * and the saving/ordering is handled by the CustomQSTileHost.
- */
-public class CustomQSPanel extends QSPanel {
-    
-    private static final String TAG = "CustomQSPanel";
-    private static final boolean DEBUG = false;
-
-    private List<String> mSavedTiles = Collections.emptyList();
-    private ArrayList<String> mStash;
-    private List<String> mTiles = new ArrayList<>();
-
-    private ArrayList<QSTile<?>> mCurrentTiles = new ArrayList<>();
-
-    public CustomQSPanel(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mTileLayout = (QSTileLayout) LayoutInflater.from(mContext)
-                .inflate(R.layout.qs_customize_layout, mQsContainer, false);
-        mQsContainer.addView((View) mTileLayout, 1 /* Between brightness and footer */);
-        ((NonPagedTileLayout) mTileLayout).setCustomQsPanel(this);
-        removeView(mFooter.getView());
-
-        if (DEBUG) Log.d(TAG, "new CustomQSPanel", new Throwable());
-        TunerService.get(mContext).addTunable(this, QSTileHost.TILES_SETTING);
-    }
-
-    @Override
-    protected void showDetail(boolean show, Record r) {
-        // No detail here.
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        // Don't allow the super to unregister the tunable.
-    }
-
-    @Override
-    public void onTuningChanged(String key, String newValue) {
-        if (key.equals(QS_SHOW_BRIGHTNESS)) {
-            // No Brightness for you.
-            super.onTuningChanged(key, "0");
-        }
-        if (QSTileHost.TILES_SETTING.equals(key)) {
-            mSavedTiles = Collections.unmodifiableList(
-                    QSTileHost.loadTileSpecs(mContext, newValue));
-            if (DEBUG) Log.d(TAG, "New saved tiles " + TextUtils.join(",", mSavedTiles));
-        }
-    }
-
-    @Override
-    protected void createCustomizePanel() {
-        // Already in CustomizePanel.
-    }
-
-    public void tileSelected(QSTile<?> tile, ClipData currentClip) {
-        String sourceSpec = getSpec(currentClip);
-        String destSpec = tile.getTileSpec();
-        if (!sourceSpec.equals(destSpec)) {
-            moveTo(sourceSpec, destSpec);
-        }
-    }
-
-    public ClipData getClip(QSTile<?> tile) {
-        String tileSpec = tile.getTileSpec();
-        // TODO: Something better than plain text.
-        // TODO: Once using something better than plain text, stop listening to non-QS drag events.
-        return ClipData.newPlainText(tileSpec, tileSpec);
-    }
-
-    public String getSpec(ClipData data) {
-        return data.getItemAt(0).getText().toString();
-    }
-
-    public void setSavedTiles() {
-        if (DEBUG) Log.d(TAG, "setSavedTiles " + TextUtils.join(",", mSavedTiles));
-        setTiles(mSavedTiles);
-    }
-
-    public void saveCurrentTiles() {
-        mHost.changeTiles(mSavedTiles, mTiles);
-    }
-
-    public void stashCurrentTiles() {
-        mStash = new ArrayList<>(mTiles);
-    }
-
-    public void unstashTiles() {
-        setTiles(mStash);
-    }
-
-    @Override
-    public void setTiles(Collection<QSTile<?>> tiles) {
-        setTilesInternal();
-    }
-
-    private void setTilesInternal() {
-        if (DEBUG) Log.d(TAG, "Set tiles internal");
-        for (int i = 0; i < mCurrentTiles.size(); i++) {
-            mCurrentTiles.get(i).destroy();
-        }
-        mCurrentTiles.clear();
-        for (int i = 0; i < mTiles.size(); i++) {
-            if (mTiles.get(i).startsWith(CustomTile.PREFIX)) {
-                QSTile<?> tile = BlankCustomTile.create(mHost, mTiles.get(i));
-                tile.setTileSpec(mTiles.get(i));
-                mCurrentTiles.add(tile);
-            } else {
-                QSTile<?> tile = mHost.createTile(mTiles.get(i));
-                if (tile != null) {
-                    tile.setTileSpec(mTiles.get(i));
-                    mCurrentTiles.add(tile);
-                } else {
-                    if (DEBUG) Log.d(TAG, "Skipping " + mTiles.get(i));
-                }
-            }
-        }
-        super.setTiles(mCurrentTiles);
-    }
-
-    public void addTile(String spec) {
-        if (DEBUG) Log.d(TAG, "addTile " + spec);
-        mTiles.add(spec);
-        setTilesInternal();
-    }
-
-    public void moveTo(String from, String to) {
-        if (DEBUG) Log.d(TAG, "moveTo " + from + " " + to);
-        int fromIndex = mTiles.indexOf(from);
-        if (fromIndex < 0) {
-            Log.e(TAG, "Unknown from tile " + from);
-            return;
-        }
-        int index = mTiles.indexOf(to);
-        if (index < 0) {
-            Log.e(TAG, "Unknown to tile " + to);
-            return;
-        }
-        mTiles.remove(fromIndex);
-        mTiles.add(index, from);
-        setTilesInternal();
-    }
-
-    public void remove(String spec) {
-        if (!mTiles.remove(spec)) {
-            Log.e(TAG, "Unknown remove spec " + spec);
-        }
-        setTilesInternal();
-    }
-
-    public void setTiles(List<String> tiles) {
-        if (DEBUG) Log.d(TAG, "Set tiles " + TextUtils.join(",", tiles));
-        mTiles = new ArrayList<>(tiles);
-        setTilesInternal();
-    }
-
-    public Collection<QSTile<?>> getTiles() {
-        return mCurrentTiles;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/DropButton.java b/packages/SystemUI/src/com/android/systemui/qs/customize/DropButton.java
deleted file mode 100644
index 3135408..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/DropButton.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.systemui.qs.customize;
-
-import android.content.ClipData;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.DragEvent;
-import android.view.View;
-import android.view.View.OnDragListener;
-import android.widget.TextView;
-
-public class DropButton extends TextView implements OnDragListener {
-
-    private OnDropListener mListener;
-
-    public DropButton(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        // TODO: Don't do this, instead make this view the right size...
-        ((View) getParent()).setOnDragListener(this);
-    }
-
-    public void setOnDropListener(OnDropListener listener) {
-        mListener = listener;
-    }
-
-    private void setHovering(boolean hovering) {
-        setAlpha(hovering ? .3f : 1);
-    }
-
-    @Override
-    public boolean onDrag(View v, DragEvent event) {
-        switch (event.getAction()) {
-            case DragEvent.ACTION_DRAG_ENTERED:
-                setHovering(true);
-                break;
-            case DragEvent.ACTION_DROP:
-                if (mListener != null) {
-                    mListener.onDrop(this, event.getClipData());
-                }
-            case DragEvent.ACTION_DRAG_EXITED:
-            case DragEvent.ACTION_DRAG_ENDED:
-                setHovering(false);
-                break;
-        }
-        return true;
-    }
-
-    public interface OnDropListener {
-        void onDrop(View v, ClipData data);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/FloatingActionButton.java b/packages/SystemUI/src/com/android/systemui/qs/customize/FloatingActionButton.java
deleted file mode 100644
index 8791a10..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/FloatingActionButton.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.systemui.qs.customize;
-
-import android.animation.AnimatorInflater;
-import android.content.Context;
-import android.graphics.Outline;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-import android.widget.ImageView;
-
-import com.android.systemui.R;
-
-public class FloatingActionButton extends ImageView {
-
-    public FloatingActionButton(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setScaleType(ScaleType.CENTER);
-        setStateListAnimator(AnimatorInflater.loadStateListAnimator(context, R.anim.fab_elevation));
-        setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                outline.setOval(0, 0, getWidth(), getHeight());
-            }
-        });
-        setClipToOutline(true);
-    }
-
-    @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-        invalidateOutline();
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/NonPagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/customize/NonPagedTileLayout.java
deleted file mode 100644
index 98c7be4..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/NonPagedTileLayout.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * 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.systemui.qs.customize;
-
-import android.content.ClipData;
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.DragEvent;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.OnTouchListener;
-import android.widget.LinearLayout;
-
-import com.android.systemui.R;
-import com.android.systemui.qs.PagedTileLayout;
-import com.android.systemui.qs.PagedTileLayout.TilePage;
-import com.android.systemui.qs.QSPanel.QSTileLayout;
-import com.android.systemui.qs.QSPanel.TileRecord;
-import com.android.systemui.qs.QSTile;
-
-import java.util.ArrayList;
-
-/**
- * Similar to PagedTileLayout, except that instead of pages it lays them out
- * vertically and expects to be inside a ScrollView.
- * @see CustomQSPanel
- */
-public class NonPagedTileLayout extends LinearLayout implements QSTileLayout, OnTouchListener {
-
-    private final ArrayList<TilePage> mPages = new ArrayList<>();
-    private final ArrayList<TileRecord> mTiles = new ArrayList<TileRecord>();
-    private CustomQSPanel mPanel;
-    private final Rect mHitRect = new Rect();
-
-    private ClipData mCurrentClip;
-    private View mCurrentView;
-
-    public NonPagedTileLayout(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        TilePage page = (PagedTileLayout.TilePage) findViewById(R.id.tile_page);
-        page.setMaxRows(3 /* First page only gets 3 */);
-        mPages.add(page);
-    }
-
-    public void setCustomQsPanel(CustomQSPanel qsPanel) {
-        mPanel = qsPanel;
-    }
-
-    @Override
-    public void addTile(TileRecord record) {
-        mTiles.add(record);
-        distributeTiles();
-        if (record.tileView.getTag() == record.tile) {
-            return;
-        }
-        record.tileView.setTag(record.tile);
-        record.tileView.setVisibility(View.VISIBLE);
-        record.tileView.init(null, null);
-        record.tileView.setOnTouchListener(this);
-        if (mCurrentClip != null && mCurrentClip.getItemAt(0)
-                .getText().toString().equals(record.tile.getTileSpec())) {
-            record.tileView.setAlpha(.3f);
-            mCurrentView = record.tileView;
-        }
-    }
-
-    @Override
-    public void removeTile(TileRecord tile) {
-        if (mTiles.remove(tile)) {
-            distributeTiles();
-        }
-    }
-
-    private void distributeTiles() {
-        final int NP = mPages.size();
-        for (int i = 0; i < NP; i++) {
-            mPages.get(i).removeAllViews();
-        }
-        int index = 0;
-        final int NT = mTiles.size();
-        for (int i = 0; i < NT; i++) {
-            TileRecord tile = mTiles.get(i);
-            mPages.get(index).addTile(tile);
-            // Keep everything in one layout for now.
-            if (false && mPages.get(index).isFull()) {
-                if (++index == mPages.size()) {
-                    LayoutInflater inflater = LayoutInflater.from(mContext);
-                    inflater.inflate(R.layout.horizontal_divider, this);
-                    mPages.add((TilePage) inflater.inflate(R.layout.qs_paged_page, this, false));
-                    addView(mPages.get(mPages.size() - 1));
-                }
-            }
-        }
-    }
-
-    @Override
-    public int getOffsetTop(TileRecord tile) {
-        // No touch feedback, so this isn't required.
-        return 0;
-    }
-
-    @Override
-    public boolean updateResources() {
-        return false;
-    }
-
-    @Override
-    public boolean onDragEvent(DragEvent event) {
-        switch (event.getAction()) {
-            case DragEvent.ACTION_DRAG_LOCATION:
-                float x = event.getX();
-                float y = event.getY();
-                final int NP = mPages.size();
-                for (int i = 0; i < NP; i++) {
-                    TilePage page = mPages.get(i);
-                    if (contains(page, x, y)) {
-                        x -= page.getLeft();
-                        y -= page.getTop();
-                        final int NC = page.getChildCount();
-                        for (int j = 0; j < NC; j++) {
-                            View child = page.getChildAt(j);
-                            if (contains(child, x, y)) {
-                                mPanel.tileSelected((QSTile<?>) child.getTag(), mCurrentClip);
-                            }
-                        }
-                        break;
-                    }
-                }
-                break;
-            case DragEvent.ACTION_DRAG_ENDED:
-                onDragEnded();
-                break;
-        }
-        return true;
-    }
-
-    @Override
-    public boolean onTouch(View v, MotionEvent event) {
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                // Stash the current tiles, in case the drop is on info, that we can restore
-                // the previous state.
-                mPanel.stashCurrentTiles();
-                mCurrentView = v;
-                mCurrentClip = mPanel.getClip((QSTile<?>) v.getTag());
-                View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);
-                ((View) getParent().getParent()).startDrag(mCurrentClip, shadow, null, 0);
-                v.setAlpha(.3f);
-                return true;
-        }
-        return false;
-    }
-
-    public void onDragEnded() {
-        mCurrentView.setAlpha(1f);
-        mCurrentView = null;
-        mCurrentClip = null;
-    }
-
-    private boolean contains(View v, float x, float y) {
-        v.getHitRect(mHitRect);
-        return mHitRect.contains((int) x, (int) y);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index a6c7fe4..edcccac 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -16,38 +16,25 @@
 package com.android.systemui.qs.customize;
 
 import android.animation.Animator;
-import android.content.ClipData;
+import android.animation.Animator.AnimatorListener;
 import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
-import android.content.DialogInterface.OnDismissListener;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
 import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
-import android.view.DragEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.view.WindowManager;
 import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.Toolbar;
-import android.widget.Toolbar.OnMenuItemClickListener;
-
 import com.android.systemui.R;
 import com.android.systemui.qs.QSDetailClipper;
-import com.android.systemui.qs.QSTile.Host.Callback;
-import com.android.systemui.qs.customize.DropButton.OnDropListener;
-import com.android.systemui.qs.customize.TileAdapter.TileSelectedListener;
+import com.android.systemui.qs.QSTile;
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
 import com.android.systemui.statusbar.phone.QSTileHost;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Allows full-screen customization of QS, through show() and hide().
@@ -55,26 +42,19 @@
  * This adds itself to the status bar window, so it can appear on top of quick settings and
  * *someday* do fancy animations to get into/out of it.
  */
-public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener, Callback,
-        OnDropListener, OnClickListener, Animator.AnimatorListener, TileSelectedListener,
-        OnCancelListener, OnDismissListener {
+public class QSCustomizer extends LinearLayout implements AnimatorListener, OnClickListener {
 
-    private static final int MENU_SAVE = Menu.FIRST;
-    private static final int MENU_RESET = Menu.FIRST + 1;
     private final QSDetailClipper mClipper;
 
     private PhoneStatusBar mPhoneStatusBar;
 
-    private Toolbar mToolbar;
-    private ViewGroup mDragButtons;
-    private CustomQSPanel mQsPanel;
-
     private boolean isShown;
-    private DropButton mInfoButton;
-    private DropButton mRemoveButton;
-    private FloatingActionButton mFab;
-    private SystemUIDialog mDialog;
     private QSTileHost mHost;
+    private RecyclerView mRecyclerView;
+    private TileAdapter mTileAdapter;
+    private View mClose;
+    private View mSave;
+    private View mReset;
 
     public QSCustomizer(Context context, AttributeSet attrs) {
         super(new ContextThemeWrapper(context, android.R.style.Theme_Material), attrs);
@@ -83,59 +63,42 @@
 
     public void setHost(QSTileHost host) {
         mHost = host;
-        mHost.addCallback(this);
         mPhoneStatusBar = host.getPhoneStatusBar();
-        mQsPanel.setTiles(mHost.getTiles());
-        mQsPanel.setHost(mHost);
-        mQsPanel.setSavedTiles();
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mToolbar = (Toolbar) findViewById(com.android.internal.R.id.action_bar);
-        TypedValue value = new TypedValue();
-        mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
-        mToolbar.setNavigationIcon(
-                getResources().getDrawable(R.drawable.ic_close_white, mContext.getTheme()));
-        mToolbar.setNavigationOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                hide(0, 0);
-            }
-        });
-        mToolbar.setOnMenuItemClickListener(this);
-        mToolbar.getMenu().add(Menu.NONE, MENU_SAVE, 0, mContext.getString(R.string.save))
-                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-        mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0,
-                mContext.getString(com.android.internal.R.string.reset));
+        mClose = findViewById(R.id.close);
+        mSave = findViewById(R.id.save);
+        mReset = findViewById(R.id.reset);
+        mClose.setOnClickListener(this);
+        mSave.setOnClickListener(this);
+        mReset.setOnClickListener(this);
 
-        mQsPanel = (CustomQSPanel) findViewById(R.id.quick_settings_panel);
-
-        mDragButtons = (ViewGroup) findViewById(R.id.drag_buttons);
-        setDragging(false);
-
-        mInfoButton = (DropButton) findViewById(R.id.info_button);
-        mInfoButton.setOnDropListener(this);
-        mRemoveButton = (DropButton) findViewById(R.id.remove_button);
-        mRemoveButton.setOnDropListener(this);
-
-        mFab = (FloatingActionButton) findViewById(R.id.fab);
-        mFab.setImageResource(R.drawable.ic_add);
-        mFab.setOnClickListener(this);
+        mRecyclerView = (RecyclerView) findViewById(android.R.id.list);
+        mTileAdapter = new TileAdapter(getContext());
+        mRecyclerView.setAdapter(mTileAdapter);
+        new ItemTouchHelper(mTileAdapter.getCallback()).attachToRecyclerView(mRecyclerView);
+        GridLayoutManager layout = new GridLayoutManager(getContext(), 3);
+        layout.setSpanSizeLookup(mTileAdapter.getSizeLookup());
+        mRecyclerView.setLayoutManager(layout);
+        mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration());
+        DefaultItemAnimator animator = new DefaultItemAnimator();
+        animator.setMoveDuration(TileAdapter.MOVE_DURATION);
+        mRecyclerView.setItemAnimator(animator);
     }
 
     public void show(int x, int y) {
         isShown = true;
-        mQsPanel.setSavedTiles();
         mPhoneStatusBar.getStatusBarWindow().addView(this);
-        mQsPanel.setListening(true);
+        setTileSpecs();
         mClipper.animateCircularClip(x, y, true, this);
+        new TileQueryHelper(mContext, mHost).setListener(mTileAdapter);
     }
 
     public void hide(int x, int y) {
         isShown = false;
-        mQsPanel.setListening(false);
         mClipper.animateCircularClip(x, y, false, this);
     }
 
@@ -149,109 +112,35 @@
         for (String tile : defTiles.split(",")) {
             tiles.add(tile);
         }
-        mQsPanel.setTiles(tiles);
+        mTileAdapter.setTileSpecs(tiles);
     }
 
-    private void setDragging(boolean dragging) {
-        mToolbar.setVisibility(!dragging ? View.VISIBLE : View.INVISIBLE);
+    private void setTileSpecs() {
+        List<String> specs = new ArrayList<>();
+        for (QSTile tile : mHost.getTiles()) {
+            specs.add(tile.getTileSpec());
+        }
+        mTileAdapter.setTileSpecs(specs);
     }
 
     private void save() {
-        Log.d("CustomQSPanel", "Save!");
-        mQsPanel.saveCurrentTiles();
-        // TODO: At save button.
-        hide(0, 0);
-    }
-
-    @Override
-    public boolean onMenuItemClick(MenuItem item) {
-        switch (item.getItemId()) {
-            case MENU_SAVE:
-                Log.d("CustomQSPanel", "Save...");
-                save();
-                break;
-            case MENU_RESET:
-                reset();
-                break;
-        }
-        return true;
-    }
-
-    @Override
-    public void onTileSelected(String spec) {
-        if (mDialog != null) {
-            mQsPanel.addTile(spec);
-            mDialog.dismiss();
-        }
-    }
-
-    @Override
-    public void onTilesChanged() {
-        mQsPanel.setTiles(mHost.getTiles());
-    }
-
-    public boolean onDragEvent(DragEvent event) {
-        switch (event.getAction()) {
-            case DragEvent.ACTION_DRAG_STARTED:
-                setDragging(true);
-                break;
-            case DragEvent.ACTION_DRAG_ENDED:
-                setDragging(false);
-                break;
-        }
-        return true;
-    }
-
-    public void onDrop(View v, ClipData data) {
-        if (v == mRemoveButton) {
-            mQsPanel.remove(mQsPanel.getSpec(data));
-        } else if (v == mInfoButton) {
-            mQsPanel.unstashTiles();
-            SystemUIDialog dialog = new SystemUIDialog(mContext);
-            dialog.setTitle(mQsPanel.getSpec(data));
-            dialog.setPositiveButton(R.string.ok, null);
-            dialog.show();
-        }
+        mTileAdapter.saveSpecs(mHost);
+        hide((int) mSave.getX() + mSave.getWidth() / 2, (int) mSave.getY() + mSave.getHeight() / 2);
     }
 
     @Override
     public void onClick(View v) {
-        if (mFab == v) {
-            mDialog = new SystemUIDialog(mContext,
-                    android.R.style.Theme_Material_Dialog);
-            View view = LayoutInflater.from(mContext).inflate(R.layout.qs_add_tiles_list, null);
-            ListView listView = (ListView) view.findViewById(android.R.id.list);
-            TileAdapter adapter = new TileAdapter(mContext, mQsPanel.getTiles(), mHost);
-            adapter.setListener(this);
-            listView.setDivider(null);
-            listView.setDividerHeight(0);
-            listView.setAdapter(adapter);
-            listView.setEmptyView(view.findViewById(R.id.empty_text));
-            mDialog.setView(view);
-            mDialog.setOnDismissListener(this);
-            mDialog.setOnCancelListener(this);
-            mDialog.show();
-            // Too lazy to figure out what this will be now, but it should probably be something
-            // besides just a dialog.
-            // For now, just make it big.
-            WindowManager.LayoutParams params = mDialog.getWindow().getAttributes();
-            params.width = WindowManager.LayoutParams.MATCH_PARENT;
-            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
-            mDialog.getWindow().setAttributes(params);
+        if (v == mClose) {
+            hide((int) mClose.getX() + mClose.getWidth() / 2,
+                    (int) mClose.getY() + mClose.getHeight() / 2);
+        } else if (v == mSave) {
+            save();
+        } else if (v == mReset) {
+            reset();
         }
     }
 
     @Override
-    public void onDismiss(DialogInterface dialog) {
-        mDialog = null;
-    }
-
-    @Override
-    public void onCancel(DialogInterface dialog) {
-        mDialog = null;
-    }
-
-    @Override
     public void onAnimationEnd(Animator animation) {
         if (!isShown) {
             mPhoneStatusBar.getStatusBarWindow().removeView(this);
@@ -274,4 +163,4 @@
     public void onAnimationRepeat(Animator animation) {
         // Don't care.
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index b72789e..fb3818c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -1,262 +1,293 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 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
+ * 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.
+ * 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.systemui.qs.customize;
 
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
-import android.os.Handler;
-import android.os.Looper;
-import android.service.quicksettings.TileService;
-import android.util.Log;
+import android.graphics.Canvas;
+import android.graphics.drawable.ColorDrawable;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ItemDecoration;
+import android.support.v7.widget.RecyclerView.State;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.support.v7.widget.helper.ItemTouchHelper;
+import android.support.v7.widget.helper.ItemTouchHelper.Callback;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.View.OnClickListener;
 import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.GridLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
+import android.widget.FrameLayout;
 import com.android.systemui.R;
-import com.android.systemui.qs.QSTile;
-import com.android.systemui.qs.QSTile.Icon;
-import com.android.systemui.qs.external.CustomTile;
+import com.android.systemui.qs.QSIconView;
+import com.android.systemui.qs.QSTileView;
+import com.android.systemui.qs.customize.TileAdapter.Holder;
+import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
+import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
 import com.android.systemui.statusbar.phone.QSTileHost;
 
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
 
-public class TileAdapter extends BaseAdapter {
+public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener {
 
-    private static final String TAG = "TileAdapter";
+    private static final long DRAG_LENGTH = 100;
+    private static final float DRAG_SCALE = 1.2f;
+    public static final long MOVE_DURATION = 150;
 
-    private final ArrayList<TileGroup> mGroups = new ArrayList<>();
+    private static final int TYPE_TILE = 0;
+    private static final int TYPE_EDIT = 1;
+
     private final Context mContext;
 
-    private TileSelectedListener mListener;
-    private ArrayList<String> mCurrentTiles;
+    private final List<TileInfo> mTiles = new ArrayList<>();
+    private int mDividerIndex;
+    private List<String> mCurrentSpecs;
+    private List<TileInfo> mOtherTiles;
+    private List<TileInfo> mAllTiles;
 
-    public TileAdapter(Context context, Collection<QSTile<?>> currentTiles, QSTileHost host) {
+    private Holder mCurrentDrag;
+
+    public TileAdapter(Context context) {
         mContext = context;
-        addSystemTiles(currentTiles, host);
-        // TODO: Live?
-    }
-
-    private void addSystemTiles(Collection<QSTile<?>> currentTiles, QSTileHost host) {
-        try {
-            ArrayList<String> tileSpecs = new ArrayList<>();
-            for (QSTile<?> tile : currentTiles) {
-                tileSpecs.add(tile.getTileSpec());
-            }
-            mCurrentTiles = tileSpecs;
-            final TileGroup group = new TileGroup("com.android.settings", mContext);
-            boolean hasColorMod = host.getDisplayController().isEnabled();
-            String possible = mContext.getString(R.string.quick_settings_tiles_default)
-                    + ",hotspot,inversion,saver" + (hasColorMod ? ",colors" : "");
-            String[] possibleTiles = possible.split(",");
-            for (int i = 0; i < possibleTiles.length; i++) {
-                final String spec = possibleTiles[i];
-                if (spec.startsWith("q")) {
-                    // Quick tiles can't be customized.
-                    continue;
-                }
-                if (tileSpecs.contains(spec)) {
-                    Log.d(TAG, "Skipping " + spec);
-                    continue;
-                }
-                Log.d(TAG, "Trying " + spec);
-                final QSTile<?> tile = host.createTile(spec);
-                if (tile == null) {
-                    continue;
-                }
-                // Bad, bad, very bad.
-                tile.setListening(true);
-                tile.clearState();
-                tile.refreshState();
-                tile.setListening(false);
-                new Handler(host.getLooper()).post(new Runnable() {
-                    @Override
-                    public void run() {
-                        group.addTile(spec, tile.getState().icon, tile.getState().label, mContext);
-                    }
-                });
-            }
-            // Error: Badness (10000).
-            // Serialize this work after the host's looper's queue is empty.
-            new Handler(host.getLooper()).post(new Runnable() {
-                @Override
-                public void run() {
-                    new Handler(Looper.getMainLooper()).post(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (group.mTiles.size() > 0) {
-                                mGroups.add(group);
-                                notifyDataSetChanged();
-                            }
-                            new QueryTilesTask().execute();
-                        }
-                    });
-                }
-            });
-        } catch (NameNotFoundException e) {
-            Log.e(TAG, "Couldn't load system tiles", e);
-        }
-    }
-
-    public void setListener(TileSelectedListener listener) {
-        mListener = listener;
-    }
-
-    @Override
-    public int getCount() {
-        return mGroups.size();
-    }
-
-    @Override
-    public Object getItem(int position) {
-        return mGroups.get(position);
+        setHasStableIds(true);
     }
 
     @Override
     public long getItemId(int position) {
-        return position;
+        return mTiles.get(position) != null ? mAllTiles.indexOf(mTiles.get(position)) : -1;
+    }
+
+    public Callback getCallback() {
+        return mCallbacks;
+    }
+
+    public ItemDecoration getItemDecoration() {
+        return mDecoration;
+    }
+
+    public void saveSpecs(QSTileHost host) {
+        List<String> newSpecs = new ArrayList<>();
+        for (int i = 0; mTiles.get(i) != null; i++) {
+            newSpecs.add(mTiles.get(i).spec);
+        }
+        host.changeTiles(mCurrentSpecs, newSpecs);
+        setTileSpecs(newSpecs);
+    }
+
+    public void setTileSpecs(List<String> currentSpecs) {
+        mCurrentSpecs = currentSpecs;
+        recalcSpecs();
     }
 
     @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        return mGroups.get(position).getView(mContext, convertView, parent, mListener);
+    public void onTilesChanged(List<TileInfo> tiles) {
+        mAllTiles = tiles;
+        recalcSpecs();
     }
 
-    private static class TileGroup {
-        private final ArrayList<TileInfo> mTiles = new ArrayList<>();
-        private CharSequence mLabel;
-        private Drawable mIcon;
-
-        public TileGroup(String pkg, Context context) throws NameNotFoundException {
-            PackageManager pm = context.getPackageManager();
-            ApplicationInfo info = pm.getApplicationInfo(pkg, 0);
-            mLabel = info.loadLabel(pm);
-            mIcon = info.loadIcon(pm);
-            Log.d(TAG, "Added " + mLabel);
+    private void recalcSpecs() {
+        if (mCurrentSpecs == null || mAllTiles == null) {
+            return;
         }
-
-        private void addTile(String spec, Drawable icon, CharSequence label) {
-            TileInfo info = new TileInfo();
-            info.label = label;
-            info.drawable = icon;
-            info.spec = spec;
-            mTiles.add(info);
+        mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
+        mTiles.clear();
+        for (int i = 0; i < mCurrentSpecs.size(); i++) {
+            mTiles.add(getAndRemoveOther(mCurrentSpecs.get(i)));
         }
+        mTiles.add(null);
+        mTiles.addAll(mOtherTiles);
+        mDividerIndex = mTiles.indexOf(null);
+        notifyDataSetChanged();
+    }
 
-        private void addTile(String spec, Icon icon, CharSequence label, Context context) {
-            addTile(spec, icon != null ? icon.getDrawable(context) : null, label);
-        }
-
-        private View getView(Context context, View convertView, ViewGroup parent,
-                final TileSelectedListener listener) {
-            if (convertView == null) {
-                convertView = LayoutInflater.from(context).inflate(R.layout.tile_listing, parent,
-                        false);
+    private TileInfo getAndRemoveOther(String s) {
+        for (int i = 0; i < mOtherTiles.size(); i++) {
+            if (mOtherTiles.get(i).spec.equals(s)) {
+                return mOtherTiles.remove(i);
             }
-            ((TextView) convertView.findViewById(android.R.id.title)).setText(mLabel);
-            ((ImageView) convertView.findViewById(android.R.id.icon)).setImageDrawable(mIcon);
-            GridLayout grid = (GridLayout) convertView.findViewById(R.id.tile_grid);
-            final int N = mTiles.size();
-            if (grid.getChildCount() != N) {
-                grid.removeAllViews();
+        }
+        return null;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (mTiles.get(position) == null) {
+            return TYPE_EDIT;
+        }
+        return TYPE_TILE;
+    }
+
+    @Override
+    public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
+        final Context context = parent.getContext();
+        LayoutInflater inflater = LayoutInflater.from(context);
+        if (viewType == 1) {
+            return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
+        }
+        FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
+                false);
+        frame.addView(new QSTileView(context, new QSIconView(context)));
+        return new Holder(frame);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mTiles.size();
+    }
+
+    @Override
+    public void onBindViewHolder(Holder holder, int position) {
+        if (holder.getItemViewType() == TYPE_EDIT) return;
+
+        TileInfo info = mTiles.get(position);
+        holder.mTileView.onStateChanged(info.state);
+    }
+
+    public SpanSizeLookup getSizeLookup() {
+        return mSizeLookup;
+    }
+
+    public class Holder extends ViewHolder {
+        private QSTileView mTileView;
+
+        public Holder(View itemView) {
+            super(itemView);
+            if (itemView instanceof FrameLayout) {
+                mTileView = (QSTileView) ((FrameLayout) itemView).getChildAt(0);
             }
-            for (int i = 0; i < N; i++) {
-                if (grid.getChildCount() <= i) {
-                    grid.addView(createTile(context));
-                }
-                View view = grid.getChildAt(i);
-                final TileInfo tileInfo = mTiles.get(i);
-                ((ImageView) view.findViewById(R.id.tile_icon)).setImageDrawable(tileInfo.drawable);
-                ((TextView) view.findViewById(R.id.tile_label)).setText(tileInfo.label);
-                view.setClickable(true);
-                view.setOnClickListener(new OnClickListener() {
-                    @Override
-                    public void onClick(View v) {
-                        listener.onTileSelected(tileInfo.spec);
-                    }
-                });
-            }
-            return convertView;
         }
 
-        private View createTile(Context context) {
-            return LayoutInflater.from(context).inflate(R.layout.qs_add_tile_layout, null);
+        public void startDrag() {
+            itemView.animate()
+                    .setDuration(DRAG_LENGTH)
+                    .scaleX(DRAG_SCALE)
+                    .scaleY(DRAG_SCALE);
+            mTileView.findViewById(R.id.tile_label).animate()
+                    .setDuration(DRAG_LENGTH)
+                    .alpha(0);
+        }
+
+        public void stopDrag() {
+            itemView.animate()
+                    .setDuration(DRAG_LENGTH)
+                    .scaleX(1)
+                    .scaleY(1);
+            mTileView.findViewById(R.id.tile_label).animate()
+                    .setDuration(DRAG_LENGTH)
+                    .alpha(1);
         }
     }
 
-    private static class TileInfo {
-        private String spec;
-        private Drawable drawable;
-        private CharSequence label;
-    }
-
-    private class QueryTilesTask extends AsyncTask<Void, Void, Collection<TileGroup>> {
+    private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
         @Override
-        protected Collection<TileGroup> doInBackground(Void... params) {
-            HashMap<String, TileGroup> pkgMap = new HashMap<>();
-            PackageManager pm = mContext.getPackageManager();
-            // TODO: Handle userness.
-            List<ResolveInfo> services = pm.queryIntentServices(
-                    new Intent(TileService.ACTION_QS_TILE), 0);
-            for (ResolveInfo info : services) {
-                String packageName = info.serviceInfo.packageName;
-                ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
-                String spec = CustomTile.toSpec(componentName);
-                if (mCurrentTiles.contains(spec)) {
+        public int getSpanSize(int position) {
+            return getItemViewType(position) == TYPE_EDIT ? 3 : 1;
+        }
+    };
+
+    private final ItemDecoration mDecoration = new ItemDecoration() {
+        // TODO: Move this to resource.
+        private final ColorDrawable mDrawable = new ColorDrawable(0xff384248);
+
+        @Override
+        public void onDraw(Canvas c, RecyclerView parent, State state) {
+            super.onDraw(c, parent, state);
+
+            final int childCount = parent.getChildCount();
+            final int width = parent.getWidth();
+            final int bottom = parent.getBottom();
+            for (int i = 0; i < childCount; i++) {
+                final View child = parent.getChildAt(i);
+                final ViewHolder holder = parent.getChildViewHolder(child);
+                if (holder.getAdapterPosition() < mDividerIndex) {
                     continue;
                 }
-                try {
-                    TileGroup group = pkgMap.get(packageName);
-                    if (group == null) {
-                        group = new TileGroup(packageName, mContext);
-                        pkgMap.put(packageName, group);
-                    }
-                    Drawable icon = info.serviceInfo.loadIcon(pm);
-                    CharSequence label = info.serviceInfo.loadLabel(pm);
-                    group.addTile(spec, icon, label != null ? label.toString() : "null");
-                } catch (NameNotFoundException e) {
-                    Log.w(TAG, "Couldn't find resolved package... " + packageName, e);
-                }
+
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+                        .getLayoutParams();
+                final int top = child.getTop() + params.topMargin +
+                        Math.round(ViewCompat.getTranslationY(child));
+                // Draw full width, in case there aren't tiles all the way across.
+                mDrawable.setBounds(0, top, width, bottom);
+                mDrawable.draw(c);
+                break;
             }
-            return pkgMap.values();
+        }
+    };
+
+    private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
+
+        @Override
+        public boolean isLongPressDragEnabled() {
+            return true;
         }
 
         @Override
-        protected void onPostExecute(Collection<TileGroup> result) {
-            mGroups.addAll(result);
-            notifyDataSetChanged();
+        public boolean isItemViewSwipeEnabled() {
+            return false;
         }
-    }
 
-    public interface TileSelectedListener {
-        void onTileSelected(String spec);
-    }
+        @Override
+        public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
+            super.onSelectedChanged(viewHolder, actionState);
+            if (mCurrentDrag != null) {
+                mCurrentDrag.stopDrag();
+            }
+            if (viewHolder != null) {
+                mCurrentDrag = (Holder) viewHolder;
+                mCurrentDrag.startDrag();
+            }
+        }
+
+        @Override
+        public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
+            if (viewHolder.getItemViewType() == TYPE_EDIT) {
+                return makeMovementFlags(0, 0);
+            }
+            int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
+                    | ItemTouchHelper.LEFT;
+            return makeMovementFlags(dragFlags, 0);
+        }
+
+        @Override
+        public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
+            int from = viewHolder.getAdapterPosition();
+            int to = target.getAdapterPosition();
+            if (to > mDividerIndex) {
+                if (from < mDividerIndex) {
+                    to = mDividerIndex;
+                } else {
+                    return false;
+                }
+            }
+            if (target.getItemViewType() == TYPE_EDIT && from < mDividerIndex) {
+                to++;
+            }
+            move(from, to, mTiles);
+            mDividerIndex = mTiles.indexOf(null);
+            notifyItemMoved(from, to);
+            return true;
+        }
+
+        private <T> void move(int from, int to, List<T> list) {
+            list.add(from > to ? to : to + 1, list.get(from));
+            list.remove(from > to ? from + 1 : from);
+        }
+
+        @Override
+        public void onSwiped(ViewHolder viewHolder, int direction) {
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
new file mode 100644
index 0000000..29f8af2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2016 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.systemui.qs.customize;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.service.quicksettings.TileService;
+import com.android.systemui.R;
+import com.android.systemui.qs.QSTile;
+import com.android.systemui.qs.QSTile.DrawableIcon;
+import com.android.systemui.qs.external.CustomTile;
+import com.android.systemui.statusbar.phone.QSTileHost;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class TileQueryHelper {
+
+    private static final String TAG = "TileQueryHelper";
+
+    private final ArrayList<TileInfo> mTiles = new ArrayList<>();
+    private final ArrayList<String> mSpecs = new ArrayList<>();
+    private final Context mContext;
+    private TileStateListener mListener;
+
+    public TileQueryHelper(Context context, QSTileHost host) {
+        mContext = context;
+        addSystemTiles(host);
+        // TODO: Live?
+    }
+
+    private void addSystemTiles(QSTileHost host) {
+        boolean hasColorMod = host.getDisplayController().isEnabled();
+        String possible = mContext.getString(R.string.quick_settings_tiles_default)
+                + ",hotspot,inversion,saver" + (hasColorMod ? ",colors" : "");
+        String[] possibleTiles = possible.split(",");
+        final Handler qsHandler = new Handler(host.getLooper());
+        final Handler mainHandler = new Handler(Looper.getMainLooper());
+        for (int i = 0; i < possibleTiles.length; i++) {
+            final String spec = possibleTiles[i];
+            final QSTile<?> tile = host.createTile(spec);
+            if (tile == null) {
+                continue;
+            }
+            tile.setListening(true);
+            tile.clearState();
+            tile.refreshState();
+            tile.setListening(false);
+            qsHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    final QSTile.State state = tile.newTileState();
+                    tile.getState().copyTo(state);
+                    mainHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            addTile(spec, state);
+                            mListener.onTilesChanged(mTiles);
+                        }
+                    });
+                }
+            });
+        }
+        qsHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                new QueryTilesTask().execute();
+            }
+        });
+    }
+
+    public void setListener(TileStateListener listener) {
+        mListener = listener;
+    }
+
+    private void addTile(String spec, QSTile.State state) {
+        if (mSpecs.contains(spec)) {
+            return;
+        }
+        TileInfo info = new TileInfo();
+        info.state = state;
+        info.spec = spec;
+        mTiles.add(info);
+        mSpecs.add(spec);
+    }
+
+    private void addTile(String spec, Drawable drawable, CharSequence label, Context context) {
+        QSTile.State state = new QSTile.State();
+        state.label = label;
+        state.contentDescription = label;
+        state.icon = new DrawableIcon(drawable);
+        addTile(spec, state);
+    }
+
+    public static class TileInfo {
+        public String spec;
+        public QSTile.State state;
+    }
+
+    private class QueryTilesTask extends AsyncTask<Void, Void, Collection<TileInfo>> {
+        @Override
+        protected Collection<TileInfo> doInBackground(Void... params) {
+            List<TileInfo> tiles = new ArrayList<>();
+            PackageManager pm = mContext.getPackageManager();
+            List<ResolveInfo> services = pm.queryIntentServicesAsUser(
+                    new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
+            for (ResolveInfo info : services) {
+                String packageName = info.serviceInfo.packageName;
+                ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
+                String spec = CustomTile.toSpec(componentName);
+                Drawable icon = info.serviceInfo.loadIcon(pm);
+                if (icon != null) {
+                    icon.mutate();
+                    icon.setTint(mContext.getColor(android.R.color.white));
+                }
+                CharSequence label = info.serviceInfo.loadLabel(pm);
+                addTile(spec, icon, label != null ? label.toString() : "null", mContext);
+            }
+            return tiles;
+        }
+
+        @Override
+        protected void onPostExecute(Collection<TileInfo> result) {
+            mTiles.addAll(result);
+            mListener.onTilesChanged(mTiles);
+        }
+    }
+
+    public interface TileStateListener {
+        void onTilesChanged(List<TileInfo> tiles);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index df3b5de..3cd9e67 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -151,7 +151,7 @@
     }
 
     @Override
-    protected State newTileState() {
+    public State newTileState() {
         return new State();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
index d78d6ff..5222e61 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
@@ -51,7 +51,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
index 64b3a6c..72cdf18 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
@@ -54,7 +54,7 @@
     }
 
     @Override
-    protected State newTileState() {
+    public State newTileState() {
         return new QSTile.State();
     }
 
@@ -153,6 +153,7 @@
         private void bindView() {
             mDrawable.onBatteryLevelChanged(100, false, false);
             mDrawable.onPowerSaveChanged(true);
+            mDrawable.disableShowPercent();
             ((ImageView) mCurrentView.findViewById(android.R.id.icon)).setImageDrawable(mDrawable);
             Checkable checkbox = (Checkable) mCurrentView.findViewById(android.R.id.toggle);
             checkbox.setChecked(mPowerSave);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 874fc3e..1dce053 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -56,7 +56,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index 18eb7a1..15e082a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -60,7 +60,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
index aacdbc9..c3a2ebe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
@@ -54,7 +54,7 @@
     }
 
     @Override
-    protected SignalState newTileState() {
+    public SignalState newTileState() {
         return new SignalState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
index 6e843e9..e98734c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
@@ -54,7 +54,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index 1aeb0fe..c6a98b4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -30,7 +30,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index f99a3e4..58872ec 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -94,7 +94,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
index 1d9f15b..f06634e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
@@ -47,7 +47,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
index 2f37943..943b502 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
@@ -44,7 +44,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java
index e1dc9f2..bdf95d8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java
@@ -75,7 +75,7 @@
     }
 
     @Override
-    protected State newTileState() {
+    public State newTileState() {
         return new State();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
index 8328897..9f41f9a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
@@ -45,7 +45,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
index f920d48..c94cf5a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -46,7 +46,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java
index 1565b6f..ba7ea4d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java
@@ -37,7 +37,7 @@
     }
 
     @Override
-    protected State newTileState() {
+    public State newTileState() {
         return new QSTile.State();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
index 42296f2..ac4dfd5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
@@ -60,7 +60,7 @@
     }
 
     @Override
-    protected SignalState newTileState() {
+    public SignalState newTileState() {
         return new SignalState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index 508490f..a94973c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -53,7 +53,7 @@
     }
 
     @Override
-    protected BooleanState newTileState() {
+    public BooleanState newTileState() {
         return new BooleanState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index d01a288..3f482c8 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -45,7 +45,6 @@
 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
 import com.android.systemui.recents.events.activity.DebugFlagsChangedEvent;
 import com.android.systemui.recents.events.activity.DismissRecentsToHomeAnimationStarted;
-import com.android.systemui.recents.events.activity.EnterRecentsTaskStackAnimationCompletedEvent;
 import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationCompletedEvent;
 import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
 import com.android.systemui.recents.events.activity.ExitRecentsWindowFirstAnimationFrameEvent;
@@ -130,6 +129,7 @@
          */
         public FinishRecentsRunnable(Intent launchIntent, ActivityOptions opts) {
             mLaunchIntent = launchIntent;
+            mOpts = opts;
         }
 
         @Override
@@ -437,11 +437,8 @@
     protected void onPause() {
         super.onPause();
 
-        RecentsDebugFlags flags = Recents.getDebugFlags();
-        if (flags.isFastToggleRecentsEnabled()) {
-            // Stop the fast-toggle dozer
-            mIterateTrigger.stopDozing();
-        }
+        // Stop the fast-toggle dozer
+        mIterateTrigger.stopDozing();
     }
 
     @Override
@@ -648,6 +645,7 @@
     }
 
     public final void onBusEvent(UserInteractionEvent event) {
+        // Stop the fast-toggle dozer
         mIterateTrigger.stopDozing();
     }
 
@@ -694,21 +692,6 @@
         }
     }
 
-    public final void onBusEvent(EnterRecentsTaskStackAnimationCompletedEvent event) {
-        RecentsDebugFlags debugFlags = Recents.getDebugFlags();
-        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
-        if (!launchState.launchedWithAltTab && debugFlags.isFastToggleRecentsEnabled() &&
-                RecentsDebugFlags.Static.EnableFastToggleTimeoutOnEnter) {
-            mIterateTrigger.setDozeDuration(
-                    getResources().getInteger(R.integer.recents_auto_advance_duration));
-            if (!mIterateTrigger.isDozing()) {
-                mIterateTrigger.startDozing();
-            } else {
-                mIterateTrigger.poke();
-            }
-        }
-    }
-
     public final void onBusEvent(EnterRecentsWindowLastAnimationFrameEvent event) {
         EventBus.getDefault().send(new UpdateFreeformTaskViewVisibilityEvent(true));
         mRecentsView.getViewTreeObserver().addOnPreDrawListener(this);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java
index 0afa1f6..177e841 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java
@@ -52,10 +52,23 @@
      * Returns the task to focus given the current launch state.
      */
     public int getInitialFocusTaskIndex(int numTasks) {
+        RecentsDebugFlags debugFlags = Recents.getDebugFlags();
         if (launchedFromAppWithThumbnail) {
+            if (debugFlags.isFastToggleRecentsEnabled()) {
+                // If fast toggling, focus the front most task so that the next tap will focus the
+                // N-1 task
+                return numTasks - 1;
+            }
+
             // If coming from another app, focus the next task
             return numTasks - 2;
         } else {
+            if (debugFlags.isFastToggleRecentsEnabled()) {
+                // If fast toggling, defer focusing until the next tap (which will automatically
+                // focus the front most task)
+                return -1;
+            }
+
             // If coming from home, focus the first task
             return numTasks - 1;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
index 6b8968f..fc14758 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
@@ -39,8 +39,6 @@
         public static final boolean EnableAffiliatedTaskGroups = true;
         // Overrides the Tuner flags and enables the fast toggle and timeout
         public static final boolean EnableFastToggleTimeoutOverride = true;
-        // Enables toggling the fast-toggle timeout immediately after entering Recents
-        public static final boolean EnableFastToggleTimeoutOnEnter = true;
 
         // Enables us to create mock recents tasks
         public static final boolean EnableMockTasks = false;
@@ -90,9 +88,6 @@
      * @return whether the initial stack state is paging.
      */
     public boolean isInitialStatePaging() {
-        if (Static.EnableFastToggleTimeoutOnEnter) {
-            return true;
-        }
         return mInitialStatePaging;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 1cceef4..dd7b7c1 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -38,6 +38,7 @@
 import android.view.AppTransitionAnimationSpec;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewConfiguration;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.Prefs;
@@ -48,6 +49,7 @@
 import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.activity.IterateRecentsEvent;
+import com.android.systemui.recents.events.activity.LaunchNextTaskRequestEvent;
 import com.android.systemui.recents.events.activity.RecentsActivityStartingEvent;
 import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
 import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
@@ -81,8 +83,10 @@
 public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener {
 
     private final static String TAG = "RecentsImpl";
+
     // The minimum amount of time between each recents button press that we will handle
     private final static int MIN_TOGGLE_DELAY_MS = 350;
+
     // The duration within which the user releasing the alt tab (from when they pressed alt tab)
     // that the fast alt-tab animation will run.  If the user's alt-tab takes longer than this
     // duration, then we will toggle recents after this duration.
@@ -337,21 +341,31 @@
         mTriggeredFromAltTab = false;
 
         try {
+            ViewConfiguration viewConfig = ViewConfiguration.get(mContext);
             SystemServicesProxy ssp = Recents.getSystemServices();
             ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
             MutableBoolean isTopTaskHome = new MutableBoolean(true);
+            long elapsedTime = SystemClock.elapsedRealtime() - mLastToggleTime;
+
             if (topTask != null && ssp.isRecentsTopMost(topTask, isTopTaskHome)) {
                 RecentsConfiguration config = Recents.getConfiguration();
                 RecentsActivityLaunchState launchState = config.getLaunchState();
                 if (!launchState.launchedWithAltTab) {
-                    // Notify recents to move onto the next task
-                    EventBus.getDefault().post(new IterateRecentsEvent());
+                    // If the user taps quickly
+                    if (ViewConfiguration.getDoubleTapMinTime() < elapsedTime &&
+                            elapsedTime < ViewConfiguration.getDoubleTapTimeout()) {
+                        // Launch the next focused task
+                        EventBus.getDefault().post(new LaunchNextTaskRequestEvent());
+                    } else {
+                        // Notify recents to move onto the next task
+                        EventBus.getDefault().post(new IterateRecentsEvent());
+                    }
                 } else {
                     // If the user has toggled it too quickly, then just eat up the event here (it's
                     // better than showing a janky screenshot).
                     // NOTE: Ideally, the screenshot mechanism would take the window transform into
                     // account
-                    if ((SystemClock.elapsedRealtime() - mLastToggleTime) < MIN_TOGGLE_DELAY_MS) {
+                    if (elapsedTime < MIN_TOGGLE_DELAY_MS) {
                         return;
                     }
 
@@ -364,7 +378,7 @@
                 // better than showing a janky screenshot).
                 // NOTE: Ideally, the screenshot mechanism would take the window transform into
                 // account
-                if ((SystemClock.elapsedRealtime() - mLastToggleTime) < MIN_TOGGLE_DELAY_MS) {
+                if (elapsedTime < MIN_TOGGLE_DELAY_MS) {
                     return;
                 }
 
@@ -551,11 +565,14 @@
     public void dockTopTask(int topTaskId, int dragMode,
             int stackCreateMode, Rect initialBounds) {
         SystemServicesProxy ssp = Recents.getSystemServices();
+
+        // Make sure we inform DividerView before we actually start the activity so we can change
+        // the resize mode already.
+        EventBus.getDefault().send(new DockingTopTaskEvent(dragMode));
         ssp.moveTaskToDockedStack(topTaskId, stackCreateMode, initialBounds);
         showRecents(false /* triggeredFromAltTab */,
                 dragMode == NavigationBarGestureHelper.DRAG_MODE_RECENTS, false /* animate */,
                 true /* reloadTasks*/);
-        EventBus.getDefault().send(new DockingTopTaskEvent(dragMode));
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchNextTaskRequestEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchNextTaskRequestEvent.java
new file mode 100644
index 0000000..11604b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchNextTaskRequestEvent.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.systemui.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+
+/**
+ * This event is sent to request that the next task is launched after a double-tap on the Recents
+ * button.
+ */
+public class LaunchNextTaskRequestEvent extends EventBus.Event {
+    // Simple event
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/UndockingTaskEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/UndockingTaskEvent.java
new file mode 100644
index 0000000..d5083a8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/UndockingTaskEvent.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 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.systemui.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+
+/**
+ * Fires when the user invoked the gesture to undock the task in the docked stack.
+ */
+public class UndockingTaskEvent extends EventBus.Event {
+
+    public UndockingTaskEvent() {
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
index ee3eb02..5eeda72 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
@@ -67,9 +67,30 @@
     public static class ViewHolder extends RecyclerView.ViewHolder implements Task.TaskCallbacks {
         public final View content;
 
-        public ViewHolder(View v) {
-            super(v);
-            content = v;
+        private Task mTask;
+
+        public ViewHolder(View content) {
+            super(content);
+            this.content = content;
+        }
+
+        /**
+         * Binds this view holder to the given task.
+         */
+        public void bindToTask(Task newTask) {
+            unbindFromTask();
+            mTask = newTask;
+            mTask.addCallback(this);
+        }
+
+        /**
+         * Unbinds this view holder from the
+         */
+        public void unbindFromTask() {
+            if (mTask != null) {
+                mTask.removeCallback(this);
+                mTask = null;
+            }
         }
 
         @Override
@@ -267,12 +288,13 @@
             }
             case TASK_ROW_VIEW_TYPE: {
                 TaskRow taskRow = (TaskRow) row;
-                taskRow.task.addCallback(holder);
                 TextView tv = (TextView) holder.content.findViewById(R.id.description);
                 tv.setText(taskRow.task.title);
                 ImageView iv = (ImageView) holder.content.findViewById(R.id.icon);
                 iv.setAlpha(0f);
                 holder.content.setOnClickListener(taskRow);
+
+                holder.bindToTask(taskRow.task);
                 loader.loadTaskData(taskRow.task, false /* fetchAndInvalidateThumbnails */);
                 break;
             }
@@ -289,7 +311,7 @@
             if (viewType == TASK_ROW_VIEW_TYPE) {
                 TaskRow taskRow = (TaskRow) row;
                 loader.unloadTaskData(taskRow.task);
-                taskRow.task.removeCallback(holder);
+                holder.unbindFromTask();
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java b/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java
index e8fa398..1cd0850 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/SystemBarScrimViews.java
@@ -83,13 +83,11 @@
      * going home).
      */
     public final void onBusEvent(DismissRecentsToHomeAnimationStarted event) {
-        int taskViewExitToAppDuration = mContext.getResources().getInteger(
-                R.integer.recents_task_exit_to_app_duration);
         if (mHasNavBarScrim && mShouldAnimateNavBarScrim) {
             mNavBarScrimView.animate()
                     .translationY(mNavBarScrimView.getMeasuredHeight())
                     .setStartDelay(0)
-                    .setDuration(taskViewExitToAppDuration)
+                    .setDuration(TaskStackAnimationHelper.EXIT_TO_HOME_TRANSLATION_DURATION)
                     .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                     .start();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
index 0eae183..7eaa193 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
@@ -108,7 +108,7 @@
             return;
         }
 
-        int offscreenY = stackLayout.mStackRect.bottom;
+        int offscreenYOffset = stackLayout.mStackRect.height();
         int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
                 R.dimen.recents_task_view_affiliate_group_enter_offset);
 
@@ -145,7 +145,7 @@
             } else if (launchState.launchedFromHome) {
                 // Move the task view off screen (below) so we can animate it in
                 RectF bounds = new RectF(mTmpTransform.rect);
-                bounds.offsetTo(bounds.left, offscreenY);
+                bounds.offset(0, offscreenYOffset);
                 tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top, (int) bounds.right,
                         (int) bounds.bottom);
             }
@@ -247,7 +247,7 @@
             return;
         }
 
-        int offscreenY = stackLayout.mStackRect.bottom;
+        int offscreenYOffset = stackLayout.mStackRect.height();
 
         // Create the animations for each of the tasks
         List<TaskView> taskViews = mStackView.getTaskViews();
@@ -277,7 +277,7 @@
 
             stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
                     null);
-            mTmpTransform.rect.offsetTo(mTmpTransform.rect.left, offscreenY);
+            mTmpTransform.rect.offset(0, offscreenYOffset);
             mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index 46fdb2a..bd37c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -383,6 +383,7 @@
      */
     void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet) {
         SystemServicesProxy ssp = Recents.getSystemServices();
+        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
 
         // Clear the progress map
         mTaskIndexMap.clear();
@@ -449,7 +450,6 @@
             if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
                 mInitialScrollP = mMinScrollP;
             } else if (getDefaultFocusState() > 0f) {
-                RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
                 if (launchState.launchedFromHome) {
                     mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, launchTaskIndex));
                 } else {
@@ -568,7 +568,7 @@
             boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task);
             if (isFrontMostTaskInGroup) {
                 getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null,
-                        false /* ignoreSingleTaskCase */);
+                        false /* ignoreSingleTaskCase */, false /* forceUpdate */);
                 float screenY = tmpTransform.rect.top;
                 boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight;
                 if (hasVisibleThumbnail) {
@@ -601,6 +601,12 @@
      */
     public TaskViewTransform getStackTransform(Task task, float stackScroll,
             TaskViewTransform transformOut, TaskViewTransform frontTransform) {
+        return getStackTransform(task, stackScroll, transformOut, frontTransform,
+                false /* forceUpdate */);
+    }
+
+    public TaskViewTransform getStackTransform(Task task, float stackScroll,
+        TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate) {
         if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
             mFreeformLayoutAlgorithm.getTransform(task, transformOut, this);
             return transformOut;
@@ -610,8 +616,9 @@
                 transformOut.reset();
                 return transformOut;
             }
-            return getStackTransform(mTaskIndexMap.get(task.key), stackScroll, transformOut,
-                    frontTransform, false /* ignoreSingleTaskCase */);
+            getStackTransform(mTaskIndexMap.get(task.key), stackScroll, transformOut,
+                    frontTransform, false /* ignoreSingleTaskCase */, forceUpdate);
+            return transformOut;
         }
     }
 
@@ -635,9 +642,9 @@
      *                             internally to ensure that we can calculate the transform for any
      *                             position in the stack.
      */
-    public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
+    public void getStackTransform(float taskProgress, float stackScroll,
             TaskViewTransform transformOut, TaskViewTransform frontTransform,
-            boolean ignoreSingleTaskCase) {
+            boolean ignoreSingleTaskCase, boolean forceUpdate) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Compute the focused and unfocused offset
@@ -658,9 +665,9 @@
         }
 
         // Skip if the task is not visible
-        if (!unfocusedVisible && !focusedVisible) {
+        if (!forceUpdate && !unfocusedVisible && !focusedVisible) {
             transformOut.reset();
-            return transformOut;
+            return;
         }
 
         int x = (mStackRect.width() - mTaskRect.width()) / 2;
@@ -700,7 +707,6 @@
         transformOut.visible = (transformOut.rect.top < mStackRect.bottom) &&
                 (frontTransform == null || transformOut.rect.top != frontTransform.rect.top);
         transformOut.p = relP;
-        return transformOut;
     }
 
     /**
@@ -797,8 +803,10 @@
                 mFocusState * (mFocusedRange.relativeMin - mUnfocusedRange.relativeMin);
         float max = mUnfocusedRange.relativeMax +
                 mFocusState * (mFocusedRange.relativeMax - mUnfocusedRange.relativeMax);
-        getStackTransform(min, 0f, mBackOfStackTransform, null, true /* ignoreSingleTaskCase */);
-        getStackTransform(max, 0f, mFrontOfStackTransform, null, true /* ignoreSingleTaskCase */);
+        getStackTransform(min, 0f, mBackOfStackTransform, null, true /* ignoreSingleTaskCase */,
+                true /* forceUpdate */);
+        getStackTransform(max, 0f, mFrontOfStackTransform, null, true /* ignoreSingleTaskCase */,
+                true /* forceUpdate */);
         mBackOfStackTransform.visible = true;
         mFrontOfStackTransform.visible = true;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 1c97b5a..bb74de4 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -58,6 +58,7 @@
 import com.android.systemui.recents.events.activity.HideHistoryButtonEvent;
 import com.android.systemui.recents.events.activity.HideHistoryEvent;
 import com.android.systemui.recents.events.activity.IterateRecentsEvent;
+import com.android.systemui.recents.events.activity.LaunchNextTaskRequestEvent;
 import com.android.systemui.recents.events.activity.LaunchTaskEvent;
 import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent;
 import com.android.systemui.recents.events.activity.PackagesChangedEvent;
@@ -654,7 +655,7 @@
                 transform.fillIn(tv);
             } else {
                 mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(),
-                        transform, null);
+                        transform, null, true /* forceUpdate */);
             }
             transform.visible = true;
         }
@@ -1544,6 +1545,24 @@
         mUIDozeTrigger.stopDozing();
     }
 
+    public final void onBusEvent(LaunchNextTaskRequestEvent event) {
+        int launchTaskIndex = mStack.indexOfStackTask(mStack.getLaunchTarget());
+        if (launchTaskIndex != -1) {
+            launchTaskIndex = Math.max(0, launchTaskIndex - 1);
+        } else {
+            launchTaskIndex = mStack.getTaskCount() - 1;
+        }
+        if (launchTaskIndex != -1) {
+            // Stop all animations
+            mUIDozeTrigger.stopDozing();
+            cancelAllTaskViewAnimations();
+
+            Task launchTask = mStack.getStackTasks().get(launchTaskIndex);
+            EventBus.getDefault().send(new LaunchTaskEvent(getChildViewForTask(launchTask),
+                    launchTask, null, INVALID_STACK_ID, false /* screenPinningRequested */));
+        }
+    }
+
     public final void onBusEvent(LaunchTaskStartedEvent event) {
         mAnimationHelper.startLaunchTaskAnimation(event.taskView, event.screenPinningRequested,
                 event.getAnimationTrigger());
@@ -1762,21 +1781,6 @@
         }
     }
 
-    public final void onBusEvent(EnterRecentsTaskStackAnimationCompletedEvent event) {
-        RecentsDebugFlags debugFlags = Recents.getDebugFlags();
-        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
-        if (!launchState.launchedWithAltTab && debugFlags.isFastToggleRecentsEnabled() &&
-                RecentsDebugFlags.Static.EnableFastToggleTimeoutOnEnter) {
-            if (mFocusedTask != null) {
-                int timerIndicatorDuration = getResources().getInteger(
-                        R.integer.recents_auto_advance_duration);
-                int focusedTaskIndex = mStack.indexOfStackTask(mFocusedTask);
-                setFocusedTask(focusedTaskIndex, false /* scrollToTask */,
-                        false /* requestViewFocus */, timerIndicatorDuration);
-            }
-        }
-    }
-
     public final void onBusEvent(UpdateFreeformTaskViewVisibilityEvent event) {
         List<TaskView> taskViews = getTaskViews();
         int taskViewCount = taskViews.size();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index b8b5068..d6680fd 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -29,6 +29,7 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewParent;
+import android.view.animation.Animation;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
@@ -497,6 +498,7 @@
         // onBeginDrag().
         mSv.removeIgnoreTask(tv.getTask());
         mSv.updateLayoutAlgorithm(false /* boundScroll */);
+        mSv.relayoutTaskViews(AnimationProps.IMMEDIATE);
         mSwipeHelperAnimations.remove(v);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 703005f..439d96f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -242,7 +242,7 @@
     }
 
     void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform,
-                                             AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) {
+            AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) {
         RecentsConfiguration config = Recents.getConfiguration();
         cancelTransformAnimation();
 
@@ -261,14 +261,16 @@
                 updateCallback.onAnimationUpdate(null);
             }
         } else {
+            // Both the progress and the update are a function of the bounds movement of the task
             if (Float.compare(getTaskProgress(), toTransform.p) != 0) {
-                mTmpAnimators.add(ObjectAnimator.ofFloat(this, TASK_PROGRESS, getTaskProgress(),
-                        toTransform.p));
+                ObjectAnimator anim = ObjectAnimator.ofFloat(this, TASK_PROGRESS, getTaskProgress(),
+                        toTransform.p);
+                mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, anim));
             }
             if (updateCallback != null) {
                 ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1);
                 updateCallbackAnim.addUpdateListener(updateCallback);
-                mTmpAnimators.add(updateCallbackAnim);
+                mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, updateCallbackAnim));
             }
 
             // Create the animator
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java
index 5ef56f3..12e2713 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java
@@ -26,10 +26,9 @@
 import android.graphics.Paint;
 import android.util.AttributeSet;
 import android.util.Property;
-import android.view.animation.AnimationUtils;
-import android.view.animation.Interpolator;
 import android.widget.ImageButton;
 
+import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 
 /**
@@ -71,7 +70,6 @@
     private final int mWidth;
     private final int mHeight;
     private final int mCircleDiameter;
-    private final Interpolator mFastOutSlowInInterpolator;
     private int mCurrentWidth;
     private int mCurrentHeight;
     private AnimatorSet mAnimator;
@@ -85,8 +83,6 @@
         mCurrentWidth = mWidth;
         mCurrentHeight = mHeight;
         mCircleDiameter = (mWidth + mHeight) / 3;
-        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
-                android.R.interpolator.fast_out_slow_in);
     }
 
     public void setTouching(boolean touching, boolean animate) {
@@ -120,8 +116,8 @@
                 ? DividerView.TOUCH_ANIMATION_DURATION
                 : DividerView.TOUCH_RELEASE_ANIMATION_DURATION);
         mAnimator.setInterpolator(touching
-                ? DividerView.TOUCH_RESPONSE_INTERPOLATOR
-                : mFastOutSlowInInterpolator);
+                ? Interpolators.TOUCH_RESPONSE
+                : Interpolators.FAST_OUT_SLOW_IN);
         mAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index 1e11fa8..83c22b1 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -48,10 +48,12 @@
 import com.android.internal.policy.DividerSnapAlgorithm;
 import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
 import com.android.internal.policy.DockedDividerUtils;
+import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.DockingTopTaskEvent;
 import com.android.systemui.recents.events.activity.RecentsActivityStartingEvent;
+import com.android.systemui.recents.events.activity.UndockingTaskEvent;
 import com.android.systemui.recents.events.ui.RecentsDrawnEvent;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 import com.android.systemui.statusbar.phone.NavigationBarGestureHelper;
@@ -67,8 +69,6 @@
 
     static final long TOUCH_ANIMATION_DURATION = 150;
     static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;
-    static final Interpolator TOUCH_RESPONSE_INTERPOLATOR =
-            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
 
     private static final String TAG = "DividerView";
 
@@ -116,7 +116,6 @@
     private final Rect mOtherInsetRect = new Rect();
     private final Rect mLastResizeRect = new Rect();
     private final WindowManagerProxy mWindowManagerProxy = WindowManagerProxy.getInstance();
-    private Interpolator mFastOutSlowInInterpolator;
     private DividerWindowManager mWindowManager;
     private VelocityTracker mVelocityTracker;
     private FlingAnimationUtils mFlingAnimationUtils;
@@ -158,8 +157,6 @@
         mTouchElevation = getResources().getDimensionPixelSize(
                 R.dimen.docked_stack_divider_lift_elevation);
         mGrowRecents = getResources().getBoolean(R.bool.recents_grow_in_multiwindow);
-        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
-                android.R.interpolator.fast_out_slow_in);
         mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
         mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.3f);
         updateDisplayInfo();
@@ -192,7 +189,7 @@
                     insets.getStableInsetRight(), insets.getStableInsetBottom());
             if (mSnapAlgorithm != null) {
                 mSnapAlgorithm = null;
-                getSnapAlgorithm();
+                initializeSnapAlgorithm();
             }
         }
         return super.onApplyWindowInsets(insets);
@@ -211,17 +208,13 @@
             mHandle.setTouching(true, animate);
         }
         mDockSide = mWindowManagerProxy.getDockSide();
-        getSnapAlgorithm();
-        if (mDockSide != WindowManager.DOCKED_INVALID) {
-            mWindowManagerProxy.setResizing(true);
-            mWindowManager.setSlippery(false);
-            if (touching) {
-                liftBackground();
-            }
-            return true;
-        } else {
-            return false;
+        initializeSnapAlgorithm();
+        mWindowManagerProxy.setResizing(true);
+        mWindowManager.setSlippery(false);
+        if (touching) {
+            liftBackground();
         }
+        return mDockSide != WindowManager.DOCKED_INVALID;
     }
 
     public void stopDragging(int position, float velocity, boolean avoidDismissStart) {
@@ -233,17 +226,36 @@
 
     public void stopDragging(int position, SnapTarget target, long duration,
             Interpolator interpolator) {
+        stopDragging(position, target, duration, 0 /* startDelay*/, interpolator);
+    }
+
+    public void stopDragging(int position, SnapTarget target, long duration, long startDelay,
+            Interpolator interpolator) {
         mHandle.setTouching(false, true /* animate */);
-        flingTo(position, target, duration, interpolator);
+        flingTo(position, target, duration, startDelay, interpolator);
         mWindowManager.setSlippery(true);
         releaseBackground();
     }
 
-    public DividerSnapAlgorithm getSnapAlgorithm() {
+    private void stopDragging() {
+        mHandle.setTouching(false, true /* animate */);
+        mWindowManager.setSlippery(true);
+        releaseBackground();
+    }
+
+    private void updateDockSide() {
+        mDockSide = mWindowManagerProxy.getDockSide();
+    }
+
+    private void initializeSnapAlgorithm() {
         if (mSnapAlgorithm == null) {
             mSnapAlgorithm = new DividerSnapAlgorithm(getContext().getResources(), mDisplayWidth,
                     mDisplayHeight, mDividerSize, isHorizontalDivision(), mStableInsets);
         }
+    }
+
+    public DividerSnapAlgorithm getSnapAlgorithm() {
+        initializeSnapAlgorithm();
         return mSnapAlgorithm;
     }
 
@@ -267,6 +279,11 @@
                 mStartX = (int) event.getX();
                 mStartY = (int) event.getY();
                 boolean result = startDragging(true /* animate */, true /* touching */);
+                if (!result) {
+
+                    // Weren't able to start dragging successfully, so cancel it again.
+                    stopDragging();
+                }
                 mStartPosition = getCurrentPosition();
                 mMoving = false;
                 return result;
@@ -320,10 +337,11 @@
         anim.start();
     }
 
-    private void flingTo(int position, SnapTarget target, long duration,
+    private void flingTo(int position, SnapTarget target, long duration, long startDelay,
             Interpolator interpolator) {
         ValueAnimator anim = getFlingAnimator(position, target);
         anim.setDuration(duration);
+        anim.setStartDelay(startDelay);
         anim.setInterpolator(interpolator);
         anim.start();
     }
@@ -377,7 +395,7 @@
             mBackground.animate().scaleX(1.4f);
         }
         mBackground.animate()
-                .setInterpolator(TOUCH_RESPONSE_INTERPOLATOR)
+                .setInterpolator(Interpolators.TOUCH_RESPONSE)
                 .setDuration(TOUCH_ANIMATION_DURATION)
                 .translationZ(mTouchElevation)
                 .start();
@@ -385,7 +403,7 @@
         // Lift handle as well so it doesn't get behind the background, even though it doesn't
         // cast shadow.
         mHandle.animate()
-                .setInterpolator(TOUCH_RESPONSE_INTERPOLATOR)
+                .setInterpolator(Interpolators.TOUCH_RESPONSE)
                 .setDuration(TOUCH_ANIMATION_DURATION)
                 .translationZ(mTouchElevation)
                 .start();
@@ -393,14 +411,14 @@
 
     private void releaseBackground() {
         mBackground.animate()
-                .setInterpolator(mFastOutSlowInInterpolator)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
                 .translationZ(0)
                 .scaleX(1f)
                 .scaleY(1f)
                 .start();
         mHandle.animate()
-                .setInterpolator(mFastOutSlowInInterpolator)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
                 .translationZ(0)
                 .start();
@@ -421,6 +439,7 @@
         mDisplayWidth = info.logicalWidth;
         mDisplayHeight = info.logicalHeight;
         mSnapAlgorithm = null;
+        initializeSnapAlgorithm();
     }
 
     private int calculatePosition(int touchX, int touchY) {
@@ -725,13 +744,29 @@
     public final void onBusEvent(RecentsDrawnEvent drawnEvent) {
         if (mAnimateAfterRecentsDrawn) {
             mAnimateAfterRecentsDrawn = false;
+            updateDockSide();
             stopDragging(getCurrentPosition(), mSnapAlgorithm.getMiddleTarget(), 250,
-                    TOUCH_RESPONSE_INTERPOLATOR);
+                    Interpolators.TOUCH_RESPONSE);
         }
         if (mGrowAfterRecentsDrawn) {
             mGrowAfterRecentsDrawn = false;
+            updateDockSide();
             stopDragging(getCurrentPosition(), mSnapAlgorithm.getMiddleTarget(), 250,
-                    TOUCH_RESPONSE_INTERPOLATOR);
+                    Interpolators.TOUCH_RESPONSE);
+        }
+    }
+
+    public final void onBusEvent(UndockingTaskEvent undockingTaskEvent) {
+        int dockSide = mWindowManagerProxy.getDockSide();
+        if (dockSide != WindowManager.DOCKED_INVALID) {
+            startDragging(false /* animate */, false /* touching */);
+            SnapTarget target = dockSideTopLeft(dockSide)
+                    ? mSnapAlgorithm.getDismissEndTarget()
+                    : mSnapAlgorithm.getDismissStartTarget();
+
+            // Don't start immediately - give a little bit time to settle the drag resize change.
+            stopDragging(getCurrentPosition(), target, 336 /* duration */, 100 /* startDelay */,
+                    Interpolators.TOUCH_RESPONSE);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
index 24ab506..15bcaf8 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
@@ -97,7 +97,7 @@
         @Override
         public void run() {
             try {
-                ActivityManagerNative.getDefault().resizeStack(DOCKED_STACK_ID, null, true, false,
+                ActivityManagerNative.getDefault().resizeStack(DOCKED_STACK_ID, null, true, true,
                         false);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed to resize stack: " + e);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java b/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java
index d9276bf..3067714 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/DismissView.java
@@ -17,14 +17,12 @@
 package com.android.systemui.statusbar;
 
 import android.content.Context;
-import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.View;
 
 import com.android.systemui.R;
 
 public class DismissView extends StackScrollerDecorView {
-    private boolean mDismissAllInProgress;
     private DismissViewButton mDismissButton;
 
     public DismissView(Context context, AttributeSet attrs) {
@@ -53,27 +51,7 @@
                 || touchY > mContent.getY() + mContent.getHeight();
     }
 
-    public void showClearButton() {
-        mDismissButton.showButton();
-    }
-
-    public void setDismissAllInProgress(boolean dismissAllInProgress) {
-        if (dismissAllInProgress) {
-            setClipBounds(null);
-        }
-        mDismissAllInProgress = dismissAllInProgress;
-    }
-
-    @Override
-    public void setClipBounds(Rect clipBounds) {
-        if (mDismissAllInProgress) {
-            // we don't want any clipping to happen!
-            return;
-        }
-        super.setClipBounds(clipBounds);
-    }
-
     public boolean isButtonVisible() {
-        return mDismissButton.isButtonStatic();
+        return mDismissButton.getAlpha() != 0.0f;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java
index 46060f1..b608d67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/DismissViewButton.java
@@ -17,22 +17,13 @@
 package com.android.systemui.statusbar;
 
 import android.content.Context;
-import android.graphics.Canvas;
 import android.graphics.Rect;
-import android.graphics.drawable.AnimatedVectorDrawable;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
 
-import com.android.systemui.R;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
 
-public class DismissViewButton extends Button {
-    private AnimatedVectorDrawable mAnimatedDismissDrawable;
-    private final Drawable mStaticDismissDrawable;
-    private Drawable mActiveDrawable;
+public class DismissViewButton extends AlphaOptimizedButton {
 
     public DismissViewButton(Context context) {
         this(context, null);
@@ -49,55 +40,6 @@
     public DismissViewButton(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mAnimatedDismissDrawable = (AnimatedVectorDrawable) getContext().getDrawable(
-                R.drawable.dismiss_all_shape_animation).mutate();
-        mAnimatedDismissDrawable.setCallback(this);
-        mAnimatedDismissDrawable.setBounds(0,
-                0,
-                mAnimatedDismissDrawable.getIntrinsicWidth(),
-                mAnimatedDismissDrawable.getIntrinsicHeight());
-        mStaticDismissDrawable = getContext().getDrawable(R.drawable.dismiss_all_shape);
-        mStaticDismissDrawable.setBounds(0,
-                0,
-                mStaticDismissDrawable.getIntrinsicWidth(),
-                mStaticDismissDrawable.getIntrinsicHeight());
-        mStaticDismissDrawable.setCallback(this);
-        mActiveDrawable = mStaticDismissDrawable;
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-        canvas.save();
-        int drawableHeight = mActiveDrawable.getBounds().height();
-        boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
-        int dx = isRtl ? getWidth() / 2 + drawableHeight / 2 : getWidth() / 2 - drawableHeight / 2;
-        canvas.translate(dx, getHeight() / 2.0f + drawableHeight /
-                2.0f);
-        canvas.scale(isRtl ? -1.0f : 1.0f, -1.0f);
-        mActiveDrawable.draw(canvas);
-        canvas.restore();
-    }
-
-    @Override
-    public boolean performClick() {
-        if (!mAnimatedDismissDrawable.isRunning()) {
-            mActiveDrawable = mAnimatedDismissDrawable;
-            mAnimatedDismissDrawable.start();
-        }
-        return super.performClick();
-    }
-
-    @Override
-    protected boolean verifyDrawable(Drawable who) {
-        return super.verifyDrawable(who)
-                || who == mAnimatedDismissDrawable
-                || who == mStaticDismissDrawable;
-    }
-
-    @Override
-    public boolean hasOverlappingRendering() {
-        return false;
     }
 
     /**
@@ -118,16 +60,4 @@
         outRect.top += translationY;
         outRect.bottom += translationY;
     }
-
-    public void showButton() {
-        mActiveDrawable = mStaticDismissDrawable;
-        invalidate();
-    }
-
-    /**
-     * @return Whether the button is currently static and not being animated.
-     */
-    public boolean isButtonStatic() {
-        return mActiveDrawable == mStaticDismissDrawable;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index 963920c..0b7bfa8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -23,17 +23,15 @@
 import android.content.DialogInterface.OnClickListener;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.DisplayMetrics;
+import android.view.ContextThemeWrapper;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
 import android.view.KeyboardShortcutInfo;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup.LayoutParams;
 import android.view.Window;
 import android.view.WindowManager.KeyboardShortcutsReceiver;
 import android.widget.LinearLayout;
-import android.widget.ScrollView;
 import android.widget.TextView;
 
 import com.android.systemui.R;
@@ -65,7 +63,7 @@
     private Dialog mKeyboardShortcutsDialog;
 
     public KeyboardShortcuts(Context context) {
-        this.mContext = context;
+        this.mContext = new ContextThemeWrapper(context, android.R.style.Theme_Material_Light);
     }
 
     public void toggleKeyboardShortcuts() {
@@ -108,40 +106,28 @@
         mHandler.post(new Runnable() {
             @Override
             public void run() {
-                // TODO: break all this code out into a handleShowKeyboard...
-                // Might add more things posted; should consider adding a custom handler so
-                // you can send the keyboardShortcutsGroups as part of the message.
-                AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
-                LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
-                        LAYOUT_INFLATER_SERVICE);
-                final View keyboardShortcutsView = inflater.inflate(
-                        R.layout.keyboard_shortcuts_view, null);
-                DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
-                ScrollView scrollView = (ScrollView) keyboardShortcutsView.findViewById(
-                        R.id.keyboard_shortcuts_scroll_view);
-                // TODO: find a better way to set the height.
-                scrollView.setLayoutParams(new LinearLayout.LayoutParams(
-                        LayoutParams.WRAP_CONTENT,
-                        (int) (dm.heightPixels * dm.density)));
-
-                populateKeyboardShortcuts((LinearLayout) keyboardShortcutsView.findViewById(
-                        R.id.keyboard_shortcuts_container), keyboardShortcutGroups);
-                dialogBuilder.setView(keyboardShortcutsView);
-                dialogBuilder.setPositiveButton(R.string.quick_settings_done, dialogCloseListener);
-                mKeyboardShortcutsDialog = dialogBuilder.create();
-                mKeyboardShortcutsDialog.setCanceledOnTouchOutside(true);
-
-                // Setup window.
-                Window keyboardShortcutsWindow = mKeyboardShortcutsDialog.getWindow();
-                keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
-                keyboardShortcutsWindow.setBackgroundDrawable(
-                        mContext.getDrawable(R.color.ksh_dialog_background_color));
-                keyboardShortcutsWindow.setGravity(TOP);
-                mKeyboardShortcutsDialog.show();
+                handleShowKeyboardShortcuts(keyboardShortcutGroups);
             }
         });
     }
 
+    private void handleShowKeyboardShortcuts(List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
+        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                LAYOUT_INFLATER_SERVICE);
+        final View keyboardShortcutsView = inflater.inflate(
+                R.layout.keyboard_shortcuts_view, null);
+        populateKeyboardShortcuts((LinearLayout) keyboardShortcutsView.findViewById(
+                R.id.keyboard_shortcuts_container), keyboardShortcutGroups);
+        dialogBuilder.setView(keyboardShortcutsView);
+        dialogBuilder.setPositiveButton(R.string.quick_settings_done, dialogCloseListener);
+        mKeyboardShortcutsDialog = dialogBuilder.create();
+        mKeyboardShortcutsDialog.setCanceledOnTouchOutside(true);
+        Window keyboardShortcutsWindow = mKeyboardShortcutsDialog.getWindow();
+        keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
+        mKeyboardShortcutsDialog.show();
+    }
+
     private void populateKeyboardShortcuts(LinearLayout keyboardShortcutsLayout,
             List<KeyboardShortcutGroup> keyboardShortcutGroups) {
         LayoutInflater inflater = LayoutInflater.from(mContext);
@@ -156,36 +142,37 @@
                     : mContext.getColor(R.color.ksh_application_group_color));
             keyboardShortcutsLayout.addView(categoryTitle);
 
-            LinearLayout shortcutWrapper = (LinearLayout) inflater.inflate(
-                    R.layout.keyboard_shortcuts_wrapper, null);
+            LinearLayout shortcutContainer = (LinearLayout) inflater.inflate(
+                    R.layout.keyboard_shortcuts_container, keyboardShortcutsLayout, false);
             final int itemsSize = group.getItems().size();
             for (int j = 0; j < itemsSize; j++) {
                 KeyboardShortcutInfo info = group.getItems().get(j);
-                View shortcutView = inflater.inflate(R.layout.keyboard_shortcut_app_item, null);
+                View shortcutView = inflater.inflate(R.layout.keyboard_shortcut_app_item,
+                        shortcutContainer, false);
                 TextView textView = (TextView) shortcutView
                         .findViewById(R.id.keyboard_shortcuts_keyword);
                 textView.setText(info.getLabel());
 
+                LinearLayout shortcutItemsContainer = (LinearLayout) shortcutView
+                        .findViewById(R.id.keyboard_shortcuts_item_container);
                 List<String> shortcutKeys = getHumanReadableShortcutKeys(info);
                 final int shortcutKeysSize = shortcutKeys.size();
                 for (int k = 0; k < shortcutKeysSize; k++) {
                     String shortcutKey = shortcutKeys.get(k);
                     TextView shortcutKeyView = (TextView) inflater.inflate(
-                            R.layout.keyboard_shortcuts_key_view, null);
+                            R.layout.keyboard_shortcuts_key_view, shortcutItemsContainer, false);
                     shortcutKeyView.setText(shortcutKey);
-                    LinearLayout shortcutItemsContainer = (LinearLayout) shortcutView
-                            .findViewById(R.id.keyboard_shortcuts_item_container);
                     shortcutItemsContainer.addView(shortcutKeyView);
                 }
-                shortcutWrapper.addView(shortcutView);
+                shortcutContainer.addView(shortcutView);
             }
-
-            // TODO: merge container and wrapper into one xml file - wrapper is always a child of
-            // container.
-            LinearLayout shortcutsContainer = (LinearLayout) inflater.inflate(
-                    R.layout.keyboard_shortcuts_container, null);
-            shortcutsContainer.addView(shortcutWrapper);
-            keyboardShortcutsLayout.addView(shortcutsContainer);
+            keyboardShortcutsLayout.addView(shortcutContainer);
+            if (i < keyboardShortcutGroupsSize - 1) {
+                View separator = inflater.inflate(
+                        R.layout.keyboard_shortcuts_category_separator, keyboardShortcutsLayout,
+                        false);
+                keyboardShortcutsLayout.addView(separator);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java
index 98a37f9..c70aad2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java
@@ -74,8 +74,7 @@
         }
 
         private void applyToChild(View view, boolean shouldApply, int originalColor) {
-            if (view.getVisibility() == View.VISIBLE
-                    && originalColor != NotificationHeaderView.NO_COLOR) {
+            if (originalColor != NotificationHeaderView.NO_COLOR) {
                 ImageView imageView = (ImageView) view;
                 imageView.getDrawable().mutate();
                 if (shouldApply) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
index ec73935..81144d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
@@ -107,12 +107,13 @@
 
     public void bind(CharSequence title, CharSequence text) {
         mTitleView.setText(title);
-        if (TextUtils.isEmpty(title)) {
-            mTitleView.setVisibility(GONE);
-        }
-        mTextView.setText(text);
+        mTitleView.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE);
         if (TextUtils.isEmpty(text)) {
             mTextView.setVisibility(GONE);
+            mTextView.setText(null);
+        } else {
+            mTextView.setVisibility(VISIBLE);
+            mTextView.setText(text.toString());
         }
         requestLayout();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
index 285d53f..28bb66f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
@@ -18,8 +18,13 @@
 
 import android.app.Notification;
 import android.content.Context;
+import android.text.BidiFormatter;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
 import android.view.LayoutInflater;
+import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.systemui.R;
@@ -34,12 +39,12 @@
 
     private final Context mContext;
     private ViewGroup mParent;
-    private String mConcadenationString;
+    private String mDivider;
 
     public HybridNotificationViewManager(Context ctx, ViewGroup parent) {
         mContext = ctx;
         mParent = parent;
-        mConcadenationString = mContext.getString(R.string.group_summary_concadenation);
+        mDivider = " • ";
     }
 
     private HybridNotificationView inflateHybridView() {
@@ -83,7 +88,7 @@
         if (reusableView == null) {
             reusableView = inflateHybridView();
         }
-        CharSequence summary = null;
+        SpannableStringBuilder summary = new SpannableStringBuilder();
         int childCount = group.size();
         for (int i = startIndex; i < childCount; i++) {
             ExpandableNotificationRow child = group.get(i);
@@ -92,15 +97,18 @@
             if (titleText == null) {
                 continue;
             }
-            if (TextUtils.isEmpty(summary)) {
-                summary = titleText;
-            } else if (reusableView.isLayoutRtl()) {
-                summary = titleText + mConcadenationString + summary;
-            } else {
-                summary = summary + mConcadenationString + titleText;
+            if (!TextUtils.isEmpty(summary)) {
+                summary.append(mDivider,
+                        new TextAppearanceSpan(mContext, R.style.
+                                TextAppearance_Material_Notification_HybridNotificationDivider),
+                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
             }
+            summary.append(BidiFormatter.getInstance().unicodeWrap(titleText));
         }
-        reusableView.bind(summary);
+        // We want to force the same orientation as the layout RTL mode
+        BidiFormatter formater = BidiFormatter.getInstance(
+                reusableView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
+        reusableView.bind(formater.unicodeWrap(summary));
         return reusableView;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java
new file mode 100644
index 0000000..487a7a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationBigTextTemplateViewWrapper.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 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.systemui.statusbar.notification;
+
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+
+import com.android.internal.widget.ImageFloatingTextView;
+import com.android.systemui.statusbar.TransformableView;
+
+/**
+ * Wraps a notification containing a big text template
+ */
+public class NotificationBigTextTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+    private ImageFloatingTextView mBigtext;
+
+    protected NotificationBigTextTemplateViewWrapper(Context ctx, View view) {
+        super(ctx, view);
+    }
+
+    private void resolveViews(StatusBarNotification notification) {
+        mBigtext = (ImageFloatingTextView) mView.findViewById(com.android.internal.R.id.big_text);
+    }
+
+    @Override
+    public void notifyContentUpdated(StatusBarNotification notification) {
+        // Reinspect the notification. Before the super call, because the super call also updates
+        // the transformation types and we need to have our values set by then.
+        resolveViews(notification);
+        super.notifyContentUpdated(notification);
+    }
+
+    @Override
+    protected void updateTransformedTypes() {
+        // This also clears the existing types
+        super.updateTransformedTypes();
+        if (mBigtext != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
+                    mBigtext);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
index b060245..0c21f0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
@@ -55,11 +55,12 @@
                         }
                         TransformState otherState = notification.getCurrentState(
                                 TRANSFORMING_VIEW_TITLE);
-                        CrossFadeHelper.fadeOut(mText, endRunnable);
+                        final View text = ownState.getTransformedView();
+                        CrossFadeHelper.fadeOut(text, endRunnable);
                         if (otherState != null) {
                             int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
                             int[] ownPosition = ownState.getLaidOutLocationOnScreen();
-                            mText.animate()
+                            text.animate()
                                     .translationY((otherStablePosition[1]
                                             + otherState.getTransformedView().getHeight()
                                             - ownPosition[1]) * 0.33f)
@@ -72,11 +73,11 @@
                                             if (endRunnable != null) {
                                                 endRunnable.run();
                                             }
-                                            TransformState.setClippingDeactivated(mText,
+                                            TransformState.setClippingDeactivated(text,
                                                     false);
                                         }
                                     });
-                            TransformState.setClippingDeactivated(mText, true);
+                            TransformState.setClippingDeactivated(text, true);
                             otherState.recycle();
                         }
                         return true;
@@ -90,17 +91,18 @@
                         }
                         TransformState otherState = notification.getCurrentState(
                                 TRANSFORMING_VIEW_TITLE);
-                        boolean isVisible = mText.getVisibility() == View.VISIBLE;
-                        CrossFadeHelper.fadeIn(mText);
+                        final View text = ownState.getTransformedView();
+                        boolean isVisible = text.getVisibility() == View.VISIBLE;
+                        CrossFadeHelper.fadeIn(text);
                         if (otherState != null) {
                             int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
                             int[] ownStablePosition = ownState.getLaidOutLocationOnScreen();
                             if (!isVisible) {
-                                mText.setTranslationY((otherStablePosition[1]
+                                text.setTranslationY((otherStablePosition[1]
                                         + otherState.getTransformedView().getHeight()
                                         - ownStablePosition[1]) * 0.33f);
                             }
-                            mText.animate()
+                            text.animate()
                                     .translationY(0)
                                     .setDuration(
                                             StackStateAnimator.ANIMATION_DURATION_STANDARD)
@@ -108,11 +110,11 @@
                                     .withEndAction(new Runnable() {
                                         @Override
                                         public void run() {
-                                            TransformState.setClippingDeactivated(mText,
+                                            TransformState.setClippingDeactivated(text,
                                                     false);
                                         }
                                     });
-                            TransformState.setClippingDeactivated(mText, true);
+                            TransformState.setClippingDeactivated(text, true);
                             otherState.recycle();
                         }
                         return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
index f50b976..a2b4c5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
@@ -37,6 +37,8 @@
         if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
             if ("bigPicture".equals(v.getTag())) {
                 return new NotificationBigPictureTemplateViewWrapper(ctx, v);
+            } else if ("bigText".equals(v.getTag())) {
+                return new NotificationBigTextTemplateViewWrapper(ctx, v);
             }
             return new NotificationTemplateViewWrapper(ctx, v);
         } else if (v instanceof NotificationHeaderView) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
index a2586f1..bb03454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
@@ -304,8 +304,7 @@
     public void onTuningChanged(String key, String newValue) {
         switch (key) {
             case KEY_DOCK_WINDOW_GESTURE:
-                mDockWindowEnabled = (newValue == null) ||
-                        (Integer.parseInt(newValue) != 0);
+                mDockWindowEnabled = newValue != null && (Integer.parseInt(newValue) != 0);
                 break;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 9359301..d625fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -27,7 +27,6 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.Space;
-
 import com.android.systemui.R;
 import com.android.systemui.statusbar.policy.KeyButtonView;
 import com.android.systemui.tuner.TunerService;
@@ -58,8 +57,9 @@
     public static final String KEY_IMAGE_DELIM = ":";
     public static final String KEY_CODE_END = ")";
 
-    protected final LayoutInflater mLayoutInflater;
-    protected final LayoutInflater mLandscapeInflater;
+    protected LayoutInflater mLayoutInflater;
+    protected LayoutInflater mLandscapeInflater;
+    private int mDensity;
 
     protected FrameLayout mRot0;
     protected FrameLayout mRot90;
@@ -69,11 +69,27 @@
 
     public NavigationBarInflaterView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mLayoutInflater = LayoutInflater.from(context);
+        mDensity = context.getResources().getConfiguration().densityDpi;
+        createInflaters();
+    }
+
+    private void createInflaters() {
+        mLayoutInflater = LayoutInflater.from(mContext);
         Configuration landscape = new Configuration();
-        landscape.setTo(context.getResources().getConfiguration());
+        landscape.setTo(mContext.getResources().getConfiguration());
         landscape.orientation = Configuration.ORIENTATION_LANDSCAPE;
-        mLandscapeInflater = LayoutInflater.from(context.createConfigurationContext(landscape));
+        mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape));
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (mDensity != newConfig.densityDpi) {
+            mDensity = newConfig.densityDpi;
+            createInflaters();
+            clearViews();
+            inflateLayout(mCurrentLayout);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index 3130eb9..d3681b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -154,10 +154,14 @@
         }
         mBarState = newState;
         if (mBarState == StatusBarState.KEYGUARD) {
-            for (NotificationGroup group : mGroupMap.values()) {
-                if (group.expanded) {
-                    setGroupExpanded(group, false);
-                }
+            collapseAllGroups();
+        }
+    }
+
+    public void collapseAllGroups() {
+        for (NotificationGroup group : mGroupMap.values()) {
+            if (group.expanded) {
+                setGroupExpanded(group, false);
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 575eda7..f822bd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -39,8 +39,6 @@
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
-import android.view.animation.Interpolator;
-import android.view.animation.PathInterpolator;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
@@ -212,10 +210,6 @@
         }
     };
 
-    /** Interpolator to be used for animations that respond directly to a touch */
-    private final Interpolator mTouchResponseInterpolator =
-            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
-
     public NotificationPanelView(Context context, AttributeSet attrs) {
         super(context, attrs);
         setWillNotDraw(!DEBUG);
@@ -1447,7 +1441,7 @@
         mScrollView.setBlockFlinging(true);
         ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
         if (isClick) {
-            animator.setInterpolator(mTouchResponseInterpolator);
+            animator.setInterpolator(Interpolators.TOUCH_RESPONSE);
             animator.setDuration(368);
         } else {
             mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
index cbb71c5..fd28b09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.phone;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.view.View;
@@ -55,6 +56,20 @@
     }
 
     @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        reloadWidth(mScrollView);
+        reloadWidth(mStackScroller);
+    }
+
+    private void reloadWidth(View view) {
+        LayoutParams params = (LayoutParams) view.getLayoutParams();
+        params.width = getContext().getResources().getDimensionPixelSize(
+                R.dimen.notification_panel_width);
+        view.setLayoutParams(params);
+    }
+
+    @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
         return insets;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 07dc4fd..4dee51d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -118,7 +118,10 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.qs.QSPanel;
 import com.android.systemui.recents.ScreenPinningRequest;
+import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.events.activity.UndockingTaskEvent;
 import com.android.systemui.stackdivider.Divider;
+import com.android.systemui.stackdivider.WindowManagerProxy;
 import com.android.systemui.statusbar.ActivatableNotificationView;
 import com.android.systemui.statusbar.BackDropView;
 import com.android.systemui.statusbar.BaseStatusBar;
@@ -1112,26 +1115,25 @@
         @Override
         public boolean onLongClick(View v) {
             if (mRecents != null) {
-                Point realSize = new Point();
-                mContext.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY)
-                        .getRealSize(realSize);
-                Rect initialBounds;
-
-                // Hack level over 9000: Make it one pixel smaller so activity manager doesn't
-                // dismiss it immediately again. Remove once b/26777526 is fixed.
-                if (mContext.getResources().getConfiguration().orientation
-                        == Configuration.ORIENTATION_LANDSCAPE) {
-                    initialBounds = new Rect(0, 0, realSize.x - 1, realSize.y);
+                int dockSide = WindowManagerProxy.getInstance().getDockSide();
+                if (dockSide == WindowManager.DOCKED_INVALID) {
+                    Point realSize = new Point();
+                    mContext.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY)
+                            .getRealSize(realSize);
+                    Rect initialBounds= new Rect(0, 0, realSize.x, realSize.y);
+                    boolean docked = mRecents.dockTopTask(NavigationBarGestureHelper.DRAG_MODE_NONE,
+                            ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT,
+                            initialBounds);
+                    if (docked) {
+                        MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS);
+                        return true;
+                    }
                 } else {
-                    initialBounds = new Rect(0, 0, realSize.x, realSize.y - 1);
-                }
-                boolean docked = mRecents.dockTopTask(NavigationBarGestureHelper.DRAG_MODE_NONE,
-                        ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT,
-                        initialBounds);
-                if (docked) {
-                    MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS);
+                    EventBus.getDefault().send(new UndockingTaskEvent());
+                    MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
                     return true;
                 }
+
             }
             return false;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 9996b75..45aae2d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -30,12 +30,12 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.TypedValue;
+import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
-
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.systemui.BatteryMeterView;
 import com.android.systemui.FontSizeUtils;
@@ -226,12 +226,25 @@
 
     public void setExternalIcon(String slot) {
         int viewIndex = getViewIndex(getSlotIndex(slot));
+        int height = mContext.getResources().getDimensionPixelSize(
+                R.dimen.status_bar_icon_drawing_size);
         ImageView imageView = (ImageView) mStatusIcons.getChildAt(viewIndex);
         imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
         imageView.setAdjustViewBounds(true);
+        setHeightAndCenter(imageView, height);
         imageView = (ImageView) mStatusIconsKeyguard.getChildAt(viewIndex);
         imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
         imageView.setAdjustViewBounds(true);
+        setHeightAndCenter(imageView, height);
+    }
+
+    private void setHeightAndCenter(ImageView imageView, int height) {
+        ViewGroup.LayoutParams params = imageView.getLayoutParams();
+        params.height = height;
+        if (params instanceof LinearLayout.LayoutParams) {
+            ((LinearLayout.LayoutParams) params).gravity = Gravity.CENTER_VERTICAL;
+        }
+        imageView.setLayoutParams(params);
     }
 
     public void setIcon(String slot, StatusBarIcon icon) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java
index b036936..500d603 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java
@@ -20,7 +20,6 @@
     void addCallback(Callback callback);
     void removeCallback(Callback callback);
     boolean isHotspotEnabled();
-    boolean isHotspotSupported();
     void setHotspotEnabled(boolean enabled);
     boolean isTetheringAllowed();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
index 61d26c7..07b7409 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
@@ -27,8 +27,6 @@
 import android.os.UserManager;
 import android.util.Log;
 
-import com.android.settingslib.TetherUtil;
-
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -98,11 +96,6 @@
     }
 
     @Override
-    public boolean isHotspotSupported() {
-        return TetherUtil.isTetheringSupported(mContext);
-    }
-
-    @Override
     public boolean isTetheringAllowed() {
         return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING,
                 UserHandle.of(mCurrentUser));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
index 409dac1..5cfcd89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.stack;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -58,6 +59,7 @@
     private HybridNotificationView mGroupOverflowContainer;
     private ViewState mGroupOverFlowState;
     private int mRealHeight;
+    private int mLayoutDirection = LAYOUT_DIRECTION_UNDEFINED;
 
     public NotificationChildrenContainer(Context context) {
         this(context, null);
@@ -209,6 +211,16 @@
         }
     }
 
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        int layoutDirection = getLayoutDirection();
+        if (layoutDirection != mLayoutDirection) {
+            updateGroupOverflow();
+            mLayoutDirection = layoutDirection;
+        }
+    }
+
     private View inflateDivider() {
         return LayoutInflater.from(mContext).inflate(
                 R.layout.notification_children_divider, this, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index d6276b8..cc0e67d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -1648,6 +1648,9 @@
                 bottom = (int) (lastView.getTranslationY() + lastView.getActualHeight());
                 bottom = Math.min(bottom, getHeight());
             }
+        } else if (mPhoneStatusBar.getBarState() == StatusBarState.KEYGUARD) {
+            top = mTopPadding;
+            bottom = top;
         }
         mBackgroundBounds.top = Math.max(0, top);
         mBackgroundBounds.bottom = Math.min(getHeight(), bottom);
@@ -2093,12 +2096,6 @@
         generateAddAnimation(child, false /* fromMoreCard */);
         updateAnimationState(child);
         updateChronometerForChild(child);
-        if (canChildBeDismissed(child)) {
-            // Make sure the dismissButton is visible and not in the animated state.
-            // We need to do this to avoid a race where a clearable notification is added after the
-            // dismiss animation is finished
-            mDismissView.showClearButton();
-        }
     }
 
     private void updateHideSensitiveForChild(View child) {
@@ -2608,6 +2605,9 @@
         mIsExpanded = isExpanded;
         mStackScrollAlgorithm.setIsExpanded(isExpanded);
         if (changed) {
+            if (!mIsExpanded) {
+                mGroupManager.collapseAllGroups();
+            }
             updateNotificationAnimationStates();
             updateChronometers();
         }
@@ -2977,7 +2977,6 @@
                     mDismissView.performVisibilityAnimation(false, dimissHideFinishRunnable);
                 } else {
                     dimissHideFinishRunnable.run();
-                    mDismissView.showClearButton();
                 }
             }
         }
@@ -2985,7 +2984,6 @@
 
     public void setDismissAllInProgress(boolean dismissAllInProgress) {
         mDismissAllInProgress = dismissAllInProgress;
-        mDismissView.setDismissAllInProgress(dismissAllInProgress);
         mAmbientState.setDismissAllInProgress(dismissAllInProgress);
         if (dismissAllInProgress) {
             disableClipOptimization();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index bc8c825..784f610 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -80,10 +80,6 @@
             boolean showImeSwitcher) {
     }
 
-    @Override
-    public void toggleRecentApps() {
-    }
-
     @Override // CommandQueue
     public void setWindowState(int window, int state) {
     }
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/ColorMatrixTile.java b/packages/SystemUI/src/com/android/systemui/tuner/ColorMatrixTile.java
index 7b06393..d8cf2e2 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/ColorMatrixTile.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/ColorMatrixTile.java
@@ -56,7 +56,7 @@
     }
 
     @Override
-    protected State newTileState() {
+    public State newTileState() {
         return new State();
     }
 
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 0e67a24..e1dd87f 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -348,5 +348,9 @@
     // OS: 6.1
     // GMS: 7.8.99
     USER_CREDENTIALS = 285;
+
+    // Logged when the user undocks a previously docked window by long pressing recents while in
+    // docked mode.
+    ACTION_WINDOW_UNDOCK_LONGPRESS = 286;
   }
 }
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 3fd8b40..4300920 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -2670,18 +2670,21 @@
     // if ro.tether.denied = true we default to no tethering
     // gservices could set the secure setting to 1 though to enable it on a build where it
     // had previously been turned off.
+    @Override
     public boolean isTetheringSupported() {
         enforceTetherAccessPermission();
         int defaultVal = (SystemProperties.get("ro.tether.denied").equals("true") ? 0 : 1);
         boolean tetherEnabledInSettings = (Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.TETHER_SUPPORTED, defaultVal) != 0)
                 && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
-        return tetherEnabledInSettings && ((mTethering.getTetherableUsbRegexs().length != 0 ||
+        return tetherEnabledInSettings && mUserManager.isAdminUser() &&
+                ((mTethering.getTetherableUsbRegexs().length != 0 ||
                 mTethering.getTetherableWifiRegexs().length != 0 ||
                 mTethering.getTetherableBluetoothRegexs().length != 0) &&
                 mTethering.getUpstreamIfaceTypes().length != 0);
     }
 
+    @Override
     public void startTethering(int type, ResultReceiver receiver,
             boolean showProvisioningUi) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
@@ -2692,6 +2695,7 @@
         mTethering.startTethering(type, receiver, showProvisioningUi);
     }
 
+    @Override
     public void stopTethering(int type) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
         mTethering.stopTethering(type);
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index 9bd79c9..423ef84 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -1649,6 +1649,7 @@
             // Whoops, there is an upcoming alarm.  We don't actually want to go idle.
             if (mState != STATE_ACTIVE) {
                 becomeActiveLocked("alarm", Process.myUid());
+                becomeInactiveIfAppropriateLocked();
             }
             return;
         }
diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java
index 81607a9..ecba0a4 100644
--- a/services/core/java/com/android/server/LockSettingsService.java
+++ b/services/core/java/com/android/server/LockSettingsService.java
@@ -762,39 +762,24 @@
         }
 
         VerifyCredentialResponse response;
-        boolean shouldReEnroll = false;;
-        if (hasChallenge) {
-            byte[] token = null;
-            GateKeeperResponse gateKeeperResponse = getGateKeeperService()
-                    .verifyChallenge(userId, challenge, storedHash.hash, credential.getBytes());
-            int responseCode = gateKeeperResponse.getResponseCode();
-            if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
-                 response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
-            } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
-                token = gateKeeperResponse.getPayload();
-                if (token == null) {
-                    // something's wrong if there's no payload with a challenge
-                    Slog.e(TAG, "verifyChallenge response had no associated payload");
-                    response = VerifyCredentialResponse.ERROR;
-                } else {
-                    shouldReEnroll = gateKeeperResponse.getShouldReEnroll();
-                    response = new VerifyCredentialResponse(token);
-                }
-            } else {
+        boolean shouldReEnroll = false;
+        GateKeeperResponse gateKeeperResponse = getGateKeeperService()
+                .verifyChallenge(userId, challenge, storedHash.hash, credential.getBytes());
+        int responseCode = gateKeeperResponse.getResponseCode();
+        if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+             response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
+        } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+            byte[] token = gateKeeperResponse.getPayload();
+            if (token == null) {
+                // something's wrong if there's no payload with a challenge
+                Slog.e(TAG, "verifyChallenge response had no associated payload");
                 response = VerifyCredentialResponse.ERROR;
+            } else {
+                shouldReEnroll = gateKeeperResponse.getShouldReEnroll();
+                response = new VerifyCredentialResponse(token);
             }
         } else {
-            GateKeeperResponse gateKeeperResponse = getGateKeeperService().verify(
-                    userId, storedHash.hash, credential.getBytes());
-            int responseCode = gateKeeperResponse.getResponseCode();
-            if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
-                response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
-            } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
-                shouldReEnroll = gateKeeperResponse.getShouldReEnroll();
-                response = VerifyCredentialResponse.OK;
-            } else {
-                response = VerifyCredentialResponse.ERROR;
-            }
+            response = VerifyCredentialResponse.ERROR;
         }
 
         if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index 3ce4452..5120e1b 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -2800,14 +2800,13 @@
     }
 
     @Override
-    public void prepareUserStorage(
-            String volumeUuid, int userId, int serialNumber, boolean ephemeral) {
+    public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) {
         enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
         waitForReady();
 
         try {
             mCryptConnector.execute("cryptfs", "prepare_user_storage", escapeNull(volumeUuid),
-                    userId, serialNumber, ephemeral ? 1 : 0);
+                    userId, serialNumber, flags);
         } catch (NativeDaemonConnectorException e) {
             throw e.rethrowAsParcelableException();
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
index 4f0d4d9..bef6f0a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
+++ b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
@@ -62,7 +62,7 @@
     static final boolean DEBUG_LRU = DEBUG_ALL || false;
     static final boolean DEBUG_MU = DEBUG_ALL || false;
     static final boolean DEBUG_OOM_ADJ = DEBUG_ALL || false;
-    static final boolean DEBUG_PAUSE = DEBUG_ALL || false;
+    static final boolean DEBUG_PAUSE = DEBUG_ALL || true;
     static final boolean DEBUG_POWER = DEBUG_ALL || false;
     static final boolean DEBUG_POWER_QUICK = DEBUG_POWER || false;
     static final boolean DEBUG_PROCESS_OBSERVERS = DEBUG_ALL || false;
@@ -77,7 +77,7 @@
     static final boolean DEBUG_SERVICE = DEBUG_ALL || false;
     static final boolean DEBUG_SERVICE_EXECUTING = DEBUG_ALL || false;
     static final boolean DEBUG_STACK = DEBUG_ALL || false;
-    static final boolean DEBUG_STATES = DEBUG_ALL_ACTIVITIES || false;
+    static final boolean DEBUG_STATES = DEBUG_ALL_ACTIVITIES || true;
     static final boolean DEBUG_SWITCH = DEBUG_ALL || false;
     static final boolean DEBUG_TASKS = DEBUG_ALL || false;
     static final boolean DEBUG_THUMBNAILS = DEBUG_ALL || false;
@@ -85,7 +85,7 @@
     static final boolean DEBUG_UID_OBSERVERS = DEBUG_ALL || false;
     static final boolean DEBUG_URI_PERMISSION = DEBUG_ALL || false;
     static final boolean DEBUG_USER_LEAVING = DEBUG_ALL || false;
-    static final boolean DEBUG_VISIBILITY = DEBUG_ALL || false;
+    static final boolean DEBUG_VISIBILITY = DEBUG_ALL || true;
     static final boolean DEBUG_VISIBLE_BEHIND = DEBUG_ALL_ACTIVITIES || false;
     static final boolean DEBUG_USAGE_STATS = DEBUG_ALL || false;
     static final boolean DEBUG_PERMISSIONS_REVIEW = DEBUG_ALL || false;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 54c4ced..7aac9d7 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -252,10 +252,12 @@
 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
 import static android.app.ActivityManager.RESIZE_MODE_PRESERVE_WINDOW;
 import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
+import static android.app.ActivityManager.StackId.FIRST_STATIC_STACK_ID;
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.HOME_STACK_ID;
 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+import static android.app.ActivityManager.StackId.LAST_STATIC_STACK_ID;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
 import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
@@ -600,6 +602,8 @@
 
     final AppErrors mAppErrors;
 
+    boolean mDoingSetFocusedActivity;
+
     public boolean canShowErrorDialogs() {
         return mShowDialogs && !mSleeping && !mShuttingDown;
     }
@@ -1300,6 +1304,7 @@
     int mMemWatchDumpPid;
     int mMemWatchDumpUid;
     String mTrackAllocationApp = null;
+    String mNativeDebuggingApp = null;
 
     final long[] mTmpLong = new long[2];
 
@@ -2730,6 +2735,12 @@
         }
 
         if (DEBUG_FOCUS) Slog.d(TAG_FOCUS, "setFocusedActivityLocked: r=" + r);
+
+        final boolean wasDoingSetFocusedActivity = mDoingSetFocusedActivity;
+        if (wasDoingSetFocusedActivity) Slog.w(TAG,
+                "setFocusedActivityLocked: called recursively, r=" + r + ", reason=" + reason);
+        mDoingSetFocusedActivity = true;
+
         final ActivityRecord last = mFocusedActivity;
         mFocusedActivity = r;
         if (r.task.isApplicationTask()) {
@@ -2781,6 +2792,12 @@
             mLastFocusedUserId = mFocusedActivity.userId;
         }
 
+        // Log a warning if the focused app is changed during the process. This could
+        // indicate a problem of the focus setting logic!
+        if (mFocusedActivity != r) Slog.w(TAG,
+                "setFocusedActivityLocked: r=" + r + " but focused to " + mFocusedActivity);
+        mDoingSetFocusedActivity = wasDoingSetFocusedActivity;
+
         EventLogTags.writeAmFocusedActivity(
                 mFocusedActivity == null ? -1 : mFocusedActivity.userId,
                 mFocusedActivity == null ? "NULL" : mFocusedActivity.shortComponentName,
@@ -3469,6 +3486,13 @@
             if ("1".equals(SystemProperties.get("debug.assert"))) {
                 debugFlags |= Zygote.DEBUG_ENABLE_ASSERT;
             }
+            if (mNativeDebuggingApp != null && mNativeDebuggingApp.equals(app.processName)) {
+                // Enable all debug flags required by the native debugger.
+                debugFlags |= Zygote.DEBUG_ALWAYS_JIT;          // Don't interpret anything
+                debugFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO; // Generate debug info
+                debugFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE;   // Disbale optimizations
+                mNativeDebuggingApp = null;
+            }
 
             String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;
             if (requiredAbi == null) {
@@ -5303,28 +5327,42 @@
 
     @Override
     public void killAllBackgroundProcesses() {
+        killAllBackgroundProcesses(-1);
+    }
+
+    /**
+     * Kills all background processes with targetSdkVersion below the specified
+     * target SDK version.
+     *
+     * @param targetSdkVersion the target SDK version below which to kill
+     *                         processes, or {@code -1} to kill all processes
+     */
+    private void killAllBackgroundProcesses(int targetSdkVersion) {
         if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
                 != PackageManager.PERMISSION_GRANTED) {
-            String msg = "Permission Denial: killAllBackgroundProcesses() from pid="
-                    + Binder.getCallingPid()
-                    + ", uid=" + Binder.getCallingUid()
+            final String msg = "Permission Denial: killAllBackgroundProcesses() from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
                     + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
 
-        long callingId = Binder.clearCallingIdentity();
+        final long callingId = Binder.clearCallingIdentity();
         try {
-            synchronized(this) {
-                ArrayList<ProcessRecord> procs = new ArrayList<ProcessRecord>();
+            synchronized (this) {
+                final ArrayList<ProcessRecord> procs = new ArrayList<>();
                 final int NP = mProcessNames.getMap().size();
-                for (int ip=0; ip<NP; ip++) {
-                    SparseArray<ProcessRecord> apps = mProcessNames.getMap().valueAt(ip);
+                for (int ip = 0; ip < NP; ip++) {
+                    final SparseArray<ProcessRecord> apps = mProcessNames.getMap().valueAt(ip);
                     final int NA = apps.size();
-                    for (int ia=0; ia<NA; ia++) {
-                        ProcessRecord app = apps.valueAt(ia);
+                    for (int ia = 0; ia < NA; ia++) {
+                        final ProcessRecord app = apps.valueAt(ia);
                         if (app.persistent) {
-                            // we don't kill persistent processes
+                            // We don't kill persistent processes.
+                            continue;
+                        }
+                        if (targetSdkVersion > 0
+                                && app.info.targetSdkVersion >= targetSdkVersion) {
                             continue;
                         }
                         if (app.removed) {
@@ -5336,11 +5374,13 @@
                     }
                 }
 
-                int N = procs.size();
-                for (int i=0; i<N; i++) {
+                final int N = procs.size();
+                for (int i = 0; i < N; i++) {
                     removeProcessLocked(procs.get(i), false, true, "kill all background");
                 }
+
                 mAllowLowerMemLevel = true;
+
                 updateOomAdjLocked();
                 doLowMemReportIfNeededLocked(null);
             }
@@ -8738,6 +8778,13 @@
                         continue;
                     }
 
+                    if (!tr.mUserSetupComplete) {
+                        // Don't include task launched while user is not done setting-up.
+                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                                "Skipping, user setup not complete: " + tr);
+                        continue;
+                    }
+
                     ActivityManager.RecentTaskInfo rti = createRecentTaskInfoFromTaskRecord(tr);
                     if (!detailed) {
                         rti.baseIntent.replaceExtras((Bundle)null);
@@ -11245,6 +11292,16 @@
         }
     }
 
+    void setNativeDebuggingAppLocked(ApplicationInfo app, String processName) {
+        boolean isDebuggable = "1".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0"));
+        if (!isDebuggable) {
+            if ((app.flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) {
+                throw new SecurityException("Process not debuggable: " + app.packageName);
+            }
+        }
+        mNativeDebuggingApp = processName;
+    }
+
     @Override
     public void setAlwaysFinish(boolean enabled) {
         enforceCallingPermission(android.Manifest.permission.SET_ALWAYS_FINISH,
@@ -12393,9 +12450,8 @@
             mLocalDeviceIdleController
                     = LocalServices.getService(DeviceIdleController.LocalService.class);
 
-            // Make sure we have the current profile info, since it is needed for
-            // security checks.
-            mUserController.updateCurrentProfileIdsLocked();
+            // Make sure we have the current profile info, since it is needed for security checks.
+            mUserController.onSystemReady();
 
             mRecentTasks.onSystemReady();
             // Check to see if there are any update receivers to run.
@@ -13939,6 +13995,15 @@
                 pw.println("  mProfileType=" + mProfileType);
             }
         }
+        if (mNativeDebuggingApp != null) {
+            if (dumpPackage == null || dumpPackage.equals(mNativeDebuggingApp)) {
+                if (needSep) {
+                    pw.println();
+                    needSep = false;
+                }
+                pw.println("  mNativeDebuggingApp=" + mNativeDebuggingApp);
+            }
+        }
         if (dumpPackage == null) {
             if (mAlwaysFinishActivities || mController != null) {
                 pw.println("  mAlwaysFinishActivities=" + mAlwaysFinishActivities
@@ -17616,6 +17681,22 @@
             final long origId = Binder.clearCallingIdentity();
             final ActivityStack stack = mStackSupervisor.getStack(fromStackId);
             if (stack != null) {
+                if (fromStackId == DOCKED_STACK_ID) {
+
+                    // We are moving all tasks from the docked stack to the fullscreen stack, which
+                    // is dismissing the docked stack, so resize all other stacks to fullscreen here
+                    // already so we don't end up with resize trashing.
+                    for (int i = FIRST_STATIC_STACK_ID; i <= LAST_STATIC_STACK_ID; i++) {
+                        if (StackId.isResizeableByDockedStack(i)) {
+                            ActivityStack otherStack = mStackSupervisor.getStack(i);
+                            if (otherStack != null) {
+                                mStackSupervisor.resizeStackLocked(i,
+                                        null, null, null, PRESERVE_WINDOWS,
+                                        true /* allowResizeInDockedMode */);
+                            }
+                        }
+                    }
+                }
                 final ArrayList<TaskRecord> tasks = stack.getAllTasks();
                 final int size = tasks.size();
                 if (onTop) {
@@ -17754,7 +17835,7 @@
                     if (values.getLocales().size() == 1) {
                         // This is an optimization to avoid the JNI call when the result of
                         // getFirstMatch() does not depend on the supported locales.
-                        locale = values.getLocales().getPrimary();
+                        locale = values.getLocales().get(0);
                     } else {
                         if (mSupportedSystemLocales == null) {
                             mSupportedSystemLocales =
@@ -17805,6 +17886,11 @@
                     mHandler.sendMessage(msg);
                 }
 
+                final boolean isDensityChange = (changes & ActivityInfo.CONFIG_DENSITY) != 0;
+                if (isDensityChange) {
+                    killAllBackgroundProcesses(Build.VERSION_CODES.N);
+                }
+
                 for (int i=mLruProcesses.size()-1; i>=0; i--) {
                     ProcessRecord app = mLruProcesses.get(i);
                     try {
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 5a0c1c1..c352fc8 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -2168,7 +2168,9 @@
             if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Resume running: " + next);
 
             // This activity is now becoming visible.
-            mWindowManager.setAppVisibility(next.appToken, true);
+            if (!next.visible) {
+                mWindowManager.setAppVisibility(next.appToken, true);
+            }
 
             // schedule launch ticks to collect information about slow apps.
             next.startLaunchTickingLocked();
@@ -4304,6 +4306,12 @@
             oldTaskOverride = record.task.extractOverrideConfig(record.configuration);
         }
 
+        // Conversely, do the same when going the other direction.
+        if (Configuration.EMPTY.equals(taskConfig)
+                && !Configuration.EMPTY.equals(oldTaskOverride)) {
+            taskConfig = record.task.extractOverrideConfig(record.configuration);
+        }
+
         // Determine what has changed.  May be nothing, if this is a config
         // that has come back from the app after going idle.  In that case
         // we just want to leave the official config object now in the
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 95bc95a..f53e71a 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -583,7 +583,7 @@
         }
 
         final ActivityRecord r = topRunningActivityLocked();
-        if (mService.mFocusedActivity != r) {
+        if (!mService.mDoingSetFocusedActivity && mService.mFocusedActivity != r) {
             // The focus activity should always be the top activity in the focused stack.
             // There will be chaos and anarchy if it isn't...
             mService.setFocusedActivityLocked(r, reason + " setFocusStack");
@@ -1028,6 +1028,10 @@
                     mService.setDebugApp(aInfo.processName, true, false);
                 }
 
+                if ((startFlags & ActivityManager.START_FLAG_NATIVE_DEBUGGING) != 0) {
+                    mService.setNativeDebuggingAppLocked(aInfo.applicationInfo, aInfo.processName);
+                }
+
                 if ((startFlags & ActivityManager.START_FLAG_TRACK_ALLOCATION) != 0) {
                     mService.setTrackAllocationApp(aInfo.applicationInfo, aInfo.processName);
                 }
@@ -1889,12 +1893,6 @@
 
     private void resizeStackUncheckedLocked(ActivityStack stack, Rect bounds, Rect tempTaskBounds,
             Rect tempTaskInsetBounds) {
-        if (bounds != null && mWindowManager.isFullscreenBounds(stack.mStackId, bounds)) {
-            // The bounds passed in corresponds to the fullscreen bounds which we normally
-            // represent with null. Go ahead and set it to null so that all tasks configuration
-            // can have the right fullscreen state.
-            bounds = null;
-        }
         bounds = TaskRecord.validateBounds(bounds);
 
         mTmpBounds.clear();
@@ -1913,7 +1911,8 @@
                     task.updateOverrideConfiguration(tempRect2);
                 } else {
                     task.updateOverrideConfiguration(
-                            tempTaskBounds != null ? tempTaskBounds : bounds);
+                            tempTaskBounds != null ? tempTaskBounds : bounds,
+                            tempTaskInsetBounds != null ? tempTaskInsetBounds : bounds);
                 }
             }
 
@@ -2213,9 +2212,9 @@
         // Temporarily disable resizeablility of task we are moving. We don't want it to be resized
         // if a docked stack is created below which will lead to the stack we are moving from and
         // its resizeable tasks being resized.
-        task.mResizeMode = RESIZE_MODE_UNRESIZEABLE;
+        task.mTemporarilyUnresizable = true;
         final ActivityStack stack = getStack(stackId, CREATE_IF_NEEDED, toTop);
-        task.mResizeMode = resizeMode;
+        task.mTemporarilyUnresizable = false;
         mWindowManager.moveTaskToStack(task.taskId, stack.mStackId, toTop);
         stack.addTask(task, toTop, reason);
 
@@ -2606,9 +2605,11 @@
 
         stack.setVisibleBehindActivity(visible ? r : null);
         if (!visible) {
-            // Make the activity immediately above r opaque.
+            // If there is a translucent home activity, we need to force it stop being translucent,
+            // because we can't depend on the application to necessarily perform that operation.
+            // Check out b/14469711 for details.
             final ActivityRecord next = stack.findNextTranslucentActivity(r);
-            if (next != null) {
+            if (next != null && next.isHomeActivity()) {
                 mService.convertFromTranslucent(next.appToken);
             }
         }
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 97ef10b..28882de 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -81,12 +81,6 @@
     Context mContext;
     PowerManagerInternal mPowerManagerInternal;
 
-    final int UPDATE_CPU = 0x01;
-    final int UPDATE_WIFI = 0x02;
-    final int UPDATE_RADIO = 0x04;
-    final int UPDATE_BT = 0x08;
-    final int UPDATE_ALL = UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT;
-
     class BatteryStatsHandler extends Handler implements BatteryStatsImpl.ExternalStatsSync {
         public static final int MSG_SYNC_EXTERNAL_STATS = 1;
         public static final int MSG_WRITE_TO_DISK = 2;
@@ -133,16 +127,9 @@
         }
 
         @Override
-        public void scheduleSync(String reason) {
+        public void scheduleSync(String reason, int updateFlags) {
             synchronized (this) {
-                scheduleSyncLocked(reason, UPDATE_ALL);
-            }
-        }
-
-        @Override
-        public void scheduleWifiSync(String reason) {
-            synchronized (this) {
-                scheduleSyncLocked(reason, UPDATE_WIFI);
+                scheduleSyncLocked(reason, updateFlags);
             }
         }
 
@@ -194,7 +181,7 @@
     public void shutdown() {
         Slog.w("BatteryStats", "Writing battery stats before shutdown...");
 
-        updateExternalStats("shutdown", UPDATE_ALL);
+        updateExternalStats("shutdown", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
         synchronized (mStats) {
             mStats.shutdownLocked();
         }
@@ -294,7 +281,7 @@
         //Slog.i("foo", "SENDING BATTERY INFO:");
         //mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM));
         Parcel out = Parcel.obtain();
-        updateExternalStats("get-stats", UPDATE_ALL);
+        updateExternalStats("get-stats", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
         synchronized (mStats) {
             mStats.writeToParcel(out, 0);
         }
@@ -309,7 +296,7 @@
         //Slog.i("foo", "SENDING BATTERY INFO:");
         //mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM));
         Parcel out = Parcel.obtain();
-        updateExternalStats("get-stats", UPDATE_ALL);
+        updateExternalStats("get-stats", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
         synchronized (mStats) {
             mStats.writeToParcel(out, 0);
         }
@@ -672,7 +659,8 @@
                 final String type = (powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH ||
                         powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM) ? "active"
                         : "inactive";
-                mHandler.scheduleWifiSync("wifi-data: " + type);
+                mHandler.scheduleSync("wifi-data: " + type,
+                        BatteryStatsImpl.ExternalStatsSync.UPDATE_WIFI);
             }
             mStats.noteWifiRadioPowerState(powerState, tsNanos);
         }
@@ -860,13 +848,25 @@
     @Override
     public void noteBleScanStarted(WorkSource ws) {
         enforceCallingPermission();
-        Slog.d(TAG, "BLE scan started for " + ws);
+        synchronized (mStats) {
+            mStats.noteBluetoothScanStartedFromSourceLocked(ws);
+        }
     }
 
     @Override
     public void noteBleScanStopped(WorkSource ws) {
         enforceCallingPermission();
-        Slog.d(TAG, "BLE scan stopped for " + ws);
+        synchronized (mStats) {
+            mStats.noteBluetoothScanStoppedFromSourceLocked(ws);
+        }
+    }
+
+    @Override
+    public void noteResetBleScan() {
+        enforceCallingPermission();
+        synchronized (mStats) {
+            mStats.noteResetBluetoothScanLocked();
+        }
     }
 
     public boolean isOnBattery() {
@@ -895,7 +895,7 @@
 
                 // Sync external stats first as the battery has changed states. If we don't sync
                 // immediately here, we may not collect the relevant data later.
-                updateExternalStats("battery-state", UPDATE_ALL);
+                updateExternalStats("battery-state", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
                 synchronized (mStats) {
                     mStats.setBatteryStateLocked(status, health, plugType, level, temp, volt);
                 }
@@ -1082,9 +1082,9 @@
                         pw.println("Battery stats reset.");
                         noOutput = true;
                     }
-                    updateExternalStats("dump", UPDATE_ALL);
+                    updateExternalStats("dump", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
                 } else if ("--write".equals(arg)) {
-                    updateExternalStats("dump", UPDATE_ALL);
+                    updateExternalStats("dump", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
                     synchronized (mStats) {
                         mStats.writeSyncLocked();
                         pw.println("Battery stats written.");
@@ -1148,7 +1148,7 @@
                 flags |= BatteryStats.DUMP_DEVICE_WIFI_ONLY;
             }
             // Fetch data from external sources and update the BatteryStatsImpl object with them.
-            updateExternalStats("dump", UPDATE_ALL);
+            updateExternalStats("dump", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -1358,8 +1358,10 @@
      *
      * @param reason The reason why this collection was requested. Useful for debugging.
      * @param updateFlags Which external stats to update. Can be a combination of
-     *                    {@link #UPDATE_CPU}, {@link #UPDATE_RADIO}, {@link #UPDATE_WIFI},
-     *                    and {@link #UPDATE_BT}.
+     *                    {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_CPU},
+     *                    {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_RADIO},
+     *                    {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_WIFI},
+     *                    and {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_BT}.
      */
     void updateExternalStats(final String reason, final int updateFlags) {
         synchronized (mExternalStatsLock) {
@@ -1374,17 +1376,17 @@
             }
 
             WifiActivityEnergyInfo wifiEnergyInfo = null;
-            if ((updateFlags & UPDATE_WIFI) != 0) {
+            if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_WIFI) != 0) {
                 wifiEnergyInfo = pullWifiEnergyInfoLocked();
             }
 
             ModemActivityInfo modemActivityInfo = null;
-            if ((updateFlags & UPDATE_RADIO) != 0) {
+            if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO) != 0) {
                 modemActivityInfo = pullModemActivityInfoLocked();
             }
 
             BluetoothActivityEnergyInfo bluetoothEnergyInfo = null;
-            if ((updateFlags & UPDATE_BT) != 0) {
+            if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_BT) != 0) {
                 // We only pull bluetooth stats when we have to, as we are not distributing its
                 // use amongst apps and the sampling frequency does not matter.
                 bluetoothEnergyInfo = pullBluetoothEnergyInfoLocked();
@@ -1398,20 +1400,20 @@
                             BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS, reason, 0);
                 }
 
-                if ((updateFlags & UPDATE_CPU) != 0) {
+                if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_CPU) != 0) {
                     mStats.updateCpuTimeLocked();
                     mStats.updateKernelWakelocksLocked();
                 }
 
-                if ((updateFlags & UPDATE_RADIO) != 0) {
+                if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO) != 0) {
                     mStats.updateMobileRadioStateLocked(elapsedRealtime, modemActivityInfo);
                 }
 
-                if ((updateFlags & UPDATE_WIFI) != 0) {
+                if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_WIFI) != 0) {
                     mStats.updateWifiStateLocked(wifiEnergyInfo);
                 }
 
-                if ((updateFlags & UPDATE_BT) != 0) {
+                if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_BT) != 0) {
                     mStats.updateBluetoothStateLocked(bluetoothEnergyInfo);
                 }
             }
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index a7d948c..10f0977 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -16,37 +16,7 @@
 
 package com.android.server.am;
 
-import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
-import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
-import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
-import static android.app.ActivityManager.StackId.HOME_STACK_ID;
-import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
-import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
-import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
-import static android.content.pm.ActivityInfo.RESIZE_MODE_CROP_WINDOWS;
-import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
-import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
-import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS;
-import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT;
-import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_IF_WHITELISTED;
-import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_NEVER;
-import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ADD_REMOVE;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LOCKTASK;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RECENTS;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_TASKS;
-import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_ADD_REMOVE;
-import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_LOCKTASK;
-import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_RECENTS;
-import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_TASKS;
-import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
-import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.am.ActivityManagerService.LOCK_SCREEN_SHOWN;
-import static com.android.server.am.ActivityRecord.APPLICATION_ACTIVITY_TYPE;
-import static com.android.server.am.ActivityRecord.HOME_ACTIVITY_TYPE;
-import static com.android.server.am.ActivityRecord.RECENTS_ACTIVITY_TYPE;
-
+import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityManager.StackId;
@@ -55,6 +25,7 @@
 import android.app.ActivityManager.TaskThumbnailInfo;
 import android.app.ActivityOptions;
 import android.app.AppGlobals;
+import android.app.IActivityManager;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -86,6 +57,37 @@
 import java.util.ArrayList;
 import java.util.Objects;
 
+import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
+import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
+import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
+import static android.app.ActivityManager.StackId.HOME_STACK_ID;
+import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
+import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS;
+import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT;
+import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_IF_WHITELISTED;
+import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_NEVER;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_CROP_WINDOWS;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ADD_REMOVE;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LOCKTASK;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RECENTS;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_TASKS;
+import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_ADD_REMOVE;
+import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_LOCKTASK;
+import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_RECENTS;
+import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_TASKS;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.am.ActivityManagerService.LOCK_SCREEN_SHOWN;
+import static com.android.server.am.ActivityRecord.APPLICATION_ACTIVITY_TYPE;
+import static com.android.server.am.ActivityRecord.HOME_ACTIVITY_TYPE;
+import static com.android.server.am.ActivityRecord.RECENTS_ACTIVITY_TYPE;
+
 final class TaskRecord {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskRecord" : TAG_AM;
     private static final String TAG_ADD_REMOVE = TAG + POSTFIX_ADD_REMOVE;
@@ -106,6 +108,7 @@
     private static final String ATTR_AUTOREMOVERECENTS = "auto_remove_recents";
     private static final String ATTR_ASKEDCOMPATMODE = "asked_compat_mode";
     private static final String ATTR_USERID = "user_id";
+    private static final String ATTR_USER_SETUP_COMPLETE = "user_setup_complete";
     private static final String ATTR_EFFECTIVE_UID = "effective_uid";
     private static final String ATTR_TASKTYPE = "task_type";
     private static final String ATTR_FIRSTACTIVETIME = "first_active_time";
@@ -155,11 +158,15 @@
 
     String stringName;      // caching of toString() result.
     int userId;             // user for which this task was created
+    boolean mUserSetupComplete; // The user set-up is complete as of the last time the task activity
+                                // was changed.
 
     int numFullscreen;      // Number of fullscreen activities.
 
     int mResizeMode;        // The resize mode of this task and its activities.
                             // Based on the {@link ActivityInfo#resizeMode} of the root activity.
+    boolean mTemporarilyUnresizable; // Separate flag from mResizeMode used to suppress resize
+                                     // changes on a temporary basis.
     int mLockTaskMode;      // Which tasklock mode to launch this task in. One of
                             // ActivityManager.LOCK_TASK_LAUNCH_MODE_*
     private boolean mPrivileged;    // The root activity application of this task holds
@@ -238,6 +245,9 @@
 
     // Bounds of the Task. null for fullscreen tasks.
     Rect mBounds = null;
+    private final Rect mTmpRect = new Rect();
+    private final Rect mTmpRect2 = new Rect();
+
     // Last non-fullscreen bounds the task was launched in or resized to.
     // The information is persisted and used to determine the appropriate stack to launch the
     // task into on restore.
@@ -311,7 +321,8 @@
             boolean neverRelinquishIdentity, TaskDescription _lastTaskDescription,
             TaskThumbnailInfo lastThumbnailInfo, int taskAffiliation, int prevTaskId,
             int nextTaskId, int taskAffiliationColor, int callingUid, String callingPackage,
-            int resizeMode, boolean privileged, boolean _realActivitySuspended) {
+            int resizeMode, boolean privileged, boolean _realActivitySuspended,
+            boolean userSetupComplete) {
         mService = service;
         mFilename = String.valueOf(_taskId) + TASK_THUMBNAIL_SUFFIX +
                 TaskPersister.IMAGE_EXTENSION;
@@ -334,6 +345,7 @@
         taskType = _taskType;
         mTaskToReturnTo = HOME_ACTIVITY_TYPE;
         userId = _userId;
+        mUserSetupComplete = userSetupComplete;
         effectiveUid = _effectiveUid;
         firstActiveTime = _firstActiveTime;
         lastActiveTime = _lastActiveTime;
@@ -434,6 +446,7 @@
         }
 
         userId = UserHandle.getUserId(info.applicationInfo.uid);
+        mUserSetupComplete = mService.mUserController.isUserSetupCompleteLocked(userId);
         if ((info.flags & ActivityInfo.FLAG_AUTO_REMOVE_FROM_RECENTS) != 0) {
             // If the activity itself has requested auto-remove, then just always do it.
             autoRemoveRecents = true;
@@ -934,7 +947,7 @@
 
     boolean isResizeable() {
         return !isHomeTask() && (mService.mForceResizableActivities
-                || ActivityInfo.isResizeableMode(mResizeMode));
+                || ActivityInfo.isResizeableMode(mResizeMode)) && !mTemporarilyUnresizable;
     }
 
     boolean inCropWindowsResizeMode() {
@@ -1064,6 +1077,7 @@
         out.attribute(null, ATTR_AUTOREMOVERECENTS, String.valueOf(autoRemoveRecents));
         out.attribute(null, ATTR_ASKEDCOMPATMODE, String.valueOf(askedCompatMode));
         out.attribute(null, ATTR_USERID, String.valueOf(userId));
+        out.attribute(null, ATTR_USER_SETUP_COMPLETE, String.valueOf(mUserSetupComplete));
         out.attribute(null, ATTR_EFFECTIVE_UID, String.valueOf(effectiveUid));
         out.attribute(null, ATTR_TASKTYPE, String.valueOf(taskType));
         out.attribute(null, ATTR_FIRSTACTIVETIME, String.valueOf(firstActiveTime));
@@ -1133,6 +1147,7 @@
         boolean askedCompatMode = false;
         int taskType = ActivityRecord.APPLICATION_ACTIVITY_TYPE;
         int userId = 0;
+        boolean userSetupComplete = true;
         int effectiveUid = -1;
         String lastDescription = null;
         long firstActiveTime = -1;
@@ -1179,6 +1194,8 @@
                 askedCompatMode = Boolean.valueOf(attrValue);
             } else if (ATTR_USERID.equals(attrName)) {
                 userId = Integer.valueOf(attrValue);
+            } else if (ATTR_USER_SETUP_COMPLETE.equals(attrName)) {
+                userSetupComplete = Boolean.valueOf(attrValue);
             } else if (ATTR_EFFECTIVE_UID.equals(attrName)) {
                 effectiveUid = Integer.valueOf(attrValue);
             } else if (ATTR_TASKTYPE.equals(attrName)) {
@@ -1278,7 +1295,7 @@
                 activities, firstActiveTime, lastActiveTime, lastTimeOnTop, neverRelinquishIdentity,
                 taskDescription, thumbnailInfo, taskAffiliation, prevTaskId, nextTaskId,
                 taskAffiliationColor, callingUid, callingPackage, resizeMode, privileged,
-                realActivitySuspended);
+                realActivitySuspended, userSetupComplete);
         task.updateOverrideConfiguration(bounds);
 
         for (int activityNdx = activities.size() - 1; activityNdx >=0; --activityNdx) {
@@ -1291,9 +1308,22 @@
 
     /**
      * Update task's override configuration based on the bounds.
+     * @param bounds The bounds of the task.
      * @return Update configuration or null if there is no change.
      */
     Configuration updateOverrideConfiguration(Rect bounds) {
+        return updateOverrideConfiguration(bounds, null /* insetBounds */);
+    }
+
+    /**
+     * Update task's override configuration based on the bounds.
+     * @param bounds The bounds of the task.
+     * @param insetBounds The bounds used to calculate the system insets, which is used here to
+     *                    subtract the navigation bar/status bar size from the screen size reported
+     *                    to the application. See {@link IActivityManager#resizeDockedStack}.
+     * @return Update configuration or null if there is no change.
+     */
+    Configuration updateOverrideConfiguration(Rect bounds, @Nullable Rect insetBounds) {
         if (Objects.equals(mBounds, bounds)) {
             return null;
         }
@@ -1316,7 +1346,12 @@
             if (stack == null || StackId.persistTaskBounds(stack.mStackId)) {
                 mLastNonFullscreenBounds = mBounds;
             }
-            mOverrideConfig = calculateOverrideConfig(mBounds);
+
+            // Stable insets need to be subtracted because we also subtract it in the fullscreen
+            // configuration.
+            mTmpRect.set(bounds);
+            subtractStableInsets(mTmpRect, insetBounds != null ? insetBounds : mTmpRect);
+            mOverrideConfig = calculateOverrideConfig(mTmpRect);
         }
 
         if (mFullscreen != oldFullscreen) {
@@ -1326,6 +1361,16 @@
         return !mOverrideConfig.equals(oldConfig) ? mOverrideConfig : null;
     }
 
+    private void subtractStableInsets(Rect inOutBounds, Rect inInsetBounds) {
+        mTmpRect2.set(inInsetBounds);
+        mService.mWindowManager.subtractStableInsets(mTmpRect2);
+        int leftInset = mTmpRect2.left - inInsetBounds.left;
+        int topInset = mTmpRect2.top - inInsetBounds.top;
+        int rightInset = inInsetBounds.right - mTmpRect2.right;
+        int bottomInset = inInsetBounds.bottom - mTmpRect2.bottom;
+        inOutBounds.inset(leftInset, topInset, rightInset, bottomInset);
+    }
+
     Configuration calculateOverrideConfig(Rect bounds) {
         final Configuration serviceConfig = mService.mConfiguration;
         final Configuration config = new Configuration(Configuration.EMPTY);
@@ -1341,8 +1386,9 @@
                 ? Configuration.ORIENTATION_PORTRAIT
                 : Configuration.ORIENTATION_LANDSCAPE;
         final int sl = Configuration.resetScreenLayout(serviceConfig.screenLayout);
-        config.screenLayout = Configuration.reduceScreenLayout(
-                sl, config.screenWidthDp, config.screenHeightDp);
+        int longSize = Math.max(config.screenWidthDp, config.screenHeightDp);
+        int shortSize = Math.min(config.screenWidthDp, config.screenHeightDp);
+        config.screenLayout = Configuration.reduceScreenLayout(sl, longSize, shortSize);
         return config;
     }
 
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 551f332..2f63b2d3 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -24,6 +24,7 @@
 import static android.app.ActivityManager.USER_OP_SUCCESS;
 import static android.content.Context.KEYGUARD_SERVICE;
 import static android.os.Process.SYSTEM_UID;
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -45,11 +46,14 @@
 import android.app.IStopUserCallback;
 import android.app.IUserSwitchObserver;
 import android.app.KeyguardManager;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
 import android.os.BatteryStats;
 import android.os.Binder;
 import android.os.Bundle;
@@ -66,10 +70,12 @@
 import android.os.UserManager;
 import android.os.storage.IMountService;
 import android.os.storage.StorageManager;
+import android.provider.Settings;
 import android.util.IntArray;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 
 import com.android.internal.R;
@@ -145,6 +151,34 @@
 
     private final LockPatternUtils mLockPatternUtils;
 
+    // Set of users who have completed the set-up process.
+    private final SparseBooleanArray mSetupCompletedUsers = new SparseBooleanArray();
+    private final UserSetupCompleteContentObserver mUserSetupCompleteContentObserver;
+
+    private class UserSetupCompleteContentObserver extends ContentObserver {
+        private final Uri mUserSetupComplete = Settings.Secure.getUriFor(USER_SETUP_COMPLETE);
+
+        public UserSetupCompleteContentObserver(Handler handler) {
+            super(handler);
+        }
+
+        void register(ContentResolver resolver) {
+            resolver.registerContentObserver(mUserSetupComplete, false, this, UserHandle.USER_ALL);
+            synchronized (mService) {
+                updateCurrentUserSetupCompleteLocked();
+            }
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (mUserSetupComplete.equals(uri)) {
+                synchronized (mService) {
+                    updateCurrentUserSetupCompleteLocked();
+                }
+            }
+        }
+    }
+
     UserController(ActivityManagerService service) {
         mService = service;
         mHandler = mService.mHandler;
@@ -154,6 +188,7 @@
         mUserLru.add(UserHandle.USER_SYSTEM);
         mLockPatternUtils = new LockPatternUtils(mService.mContext);
         updateStartedUserArrayLocked();
+        mUserSetupCompleteContentObserver = new UserSetupCompleteContentObserver(mHandler);
     }
 
     void finishUserSwitch(UserState uss) {
@@ -424,6 +459,7 @@
                 mStartedUsers.remove(userId);
                 mUserLru.remove(Integer.valueOf(userId));
                 updateStartedUserArrayLocked();
+                mSetupCompletedUsers.delete(userId);
 
                 mService.onUserStoppedLocked(userId);
                 // Clean up all state and processes associated with the user.
@@ -619,6 +655,7 @@
                 final Integer userIdInt = userId;
                 mUserLru.remove(userIdInt);
                 mUserLru.add(userIdInt);
+                updateCurrentUserSetupCompleteLocked();
 
                 if (foreground) {
                     mCurrentUserId = userId;
@@ -833,6 +870,17 @@
         mUserSwitchObservers.finishBroadcast();
     }
 
+    void updateCurrentUserSetupCompleteLocked() {
+        final ContentResolver cr = mService.mContext.getContentResolver();
+        final boolean setupComplete =
+                Settings.Secure.getIntForUser(cr, USER_SETUP_COMPLETE, 0, mCurrentUserId) != 0;
+        mSetupCompletedUsers.put(mCurrentUserId, setupComplete);
+    }
+
+    boolean isUserSetupCompleteLocked(int userId) {
+        return mSetupCompletedUsers.get(userId);
+    }
+
     private void stopBackgroundUsersIfEnforced(int oldUserId) {
         // Never stop system user
         if (oldUserId == UserHandle.USER_SYSTEM) {
@@ -1141,12 +1189,17 @@
         }
     }
 
+    void onSystemReady() {
+        updateCurrentProfileIdsLocked();
+        mUserSetupCompleteContentObserver.register(mService.mContext.getContentResolver());
+    }
+
     /**
      * Refreshes the list of users related to the current user when either a
      * user switch happens or when a new related user is started in the
      * background.
      */
-    void updateCurrentProfileIdsLocked() {
+    private void updateCurrentProfileIdsLocked() {
         final List<UserInfo> profiles = getUserManager().getProfiles(mCurrentUserId,
                 false /* enabledOnly */);
         int[] currentProfileIds = new int[profiles.size()]; // profiles will not be null
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index a73a67a..760b218 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -451,7 +451,7 @@
                 ((BluetoothPan) proxy).setBluetoothTethering(enable);
                 // TODO: Enabling bluetooth tethering can fail asynchronously here.
                 // We should figure out a way to bubble up that failure instead of sending success.
-                int result = ((BluetoothPan) proxy).isTetheringOn() ?
+                int result = ((BluetoothPan) proxy).isTetheringOn() == enable ?
                         ConnectivityManager.TETHER_ERROR_NO_ERROR :
                         ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
                 sendTetherResult(receiver, result);
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
index be55799..e749433 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -349,9 +349,9 @@
         pw.print(prefix); UserHandle.formatUid(pw, uId);
         pw.print(" tag="); pw.println(tag);
         pw.print(prefix);
-        pw.print("Source: uid="); UserHandle.formatUid(pw, sourceUid);
-        pw.print(" user="); pw.print(sourceUserId);
-        pw.print(" pkg="); pw.println(sourcePackageName);
+        pw.print("Source: uid="); UserHandle.formatUid(pw, getSourceUid());
+        pw.print(" user="); pw.print(getSourceUserId());
+        pw.print(" pkg="); pw.println(getSourcePackageName());
         pw.print(prefix); pw.println("JobInfo:");
         pw.print(prefix); pw.print("  Service: ");
         pw.println(job.getService().flattenToShortString());
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 544f255..7f8099e 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -192,7 +192,7 @@
  * enforcement.
  */
 public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
-    private static final String TAG = "NetworkPolicy";
+    static final String TAG = "NetworkPolicy";
     private static final boolean LOGD = false;
     private static final boolean LOGV = false;
 
@@ -1689,6 +1689,7 @@
             }
             writePolicy = true;
         }
+        updateRulesForGlobalChangeLocked(true);
 
         // Remove associated UID policies
         int[] uids = new int[0];
@@ -1862,8 +1863,8 @@
         Slog.i(TAG, "adding uid " + uid + " to restrict background whitelist");
         synchronized (mRulesLock) {
             mRestrictBackgroundWhitelistUids.append(uid, true);
+            updateRulesForGlobalChangeLocked(true);
             writePolicyLocked();
-            // TODO: call other update methods like updateNetworkRulesLocked?
         }
         mHandler.obtainMessage(MSG_RESTRICT_BACKGROUND_WHITELIST_CHANGED, uid, 0).sendToTarget();
     }
@@ -1878,9 +1879,10 @@
         mHandler.obtainMessage(MSG_RESTRICT_BACKGROUND_WHITELIST_CHANGED, uid, 0).sendToTarget();
     }
 
-    private void removeRestrictBackgroundWhitelistedUidLocked(int uid, boolean writePolicy) {
+    private void removeRestrictBackgroundWhitelistedUidLocked(int uid, boolean updateNow) {
         mRestrictBackgroundWhitelistUids.delete(uid);
-        if (writePolicy) {
+        if (updateNow) {
+            updateRulesForGlobalChangeLocked(true);
             writePolicyLocked();
         }
     }
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java b/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java
index 5830b0e..281c3d0 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java
@@ -16,19 +16,24 @@
 
 package com.android.server.net;
 
-import java.io.PrintWriter;
+import static com.android.server.net.NetworkPolicyManagerService.TAG;
 
-import android.content.Intent;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
 import android.net.INetworkPolicyManager;
+import android.net.NetworkPolicy;
 import android.os.Binder;
 import android.os.RemoteException;
 import android.os.ShellCommand;
+import android.util.Log;
 
-public class NetworkPolicyManagerShellCommand extends ShellCommand {
+class NetworkPolicyManagerShellCommand extends ShellCommand {
 
     final INetworkPolicyManager mInterface;
 
-    NetworkPolicyManagerShellCommand(NetworkPolicyManagerService service) {
+    NetworkPolicyManagerShellCommand(INetworkPolicyManager service) {
         mInterface = service;
     }
 
@@ -66,16 +71,24 @@
         pw.println("  help");
         pw.println("    Print this help text.");
         pw.println("");
-        pw.println("  get restrict-background");
-        pw.println("    Gets the global restrict background usage status.");
-        pw.println("  set restrict-background BOOLEAN");
-        pw.println("    Sets the global restrict background usage status.");
-        pw.println("  list restrict-background-whitelist");
-        pw.println("    Prints UID that are whitelisted for restrict background usage.");
         pw.println("  add restrict-background-whitelist UID");
         pw.println("    Adds a UID to the whitelist for restrict background usage.");
+        pw.println("  get metered-network ID");
+        pw.println("    Checks whether the given non-mobile network is metered or not.");
+        pw.println("  get restrict-background");
+        pw.println("    Gets the global restrict background usage status.");
+        pw.println("  list metered-networks [BOOLEAN]");
+        pw.println("    Lists all non-mobile networks and whether they are metered or not.");
+        pw.println("    If a boolean argument is passed, filters just the metered (or unmetered)");
+        pw.println("    networks.");
+        pw.println("  list restrict-background-whitelist");
+        pw.println("    Lists UIDs that are whitelisted for restrict background usage.");
         pw.println("  remove restrict-background-whitelist UID");
         pw.println("    Removes a UID from the whitelist for restrict background usage.");
+        pw.println("  set metered-network ID BOOLEAN");
+        pw.println("    Toggles whether the given non-mobile network is metered.");
+        pw.println("  set restrict-background BOOLEAN");
+        pw.println("    Sets the global restrict background usage status.");
     }
 
     private int runGet() throws RemoteException {
@@ -86,6 +99,8 @@
             return -1;
         }
         switch(type) {
+            case "metered-network":
+                return getNonMobileMeteredNetwork();
             case "restrict-background":
                 return getRestrictBackground();
         }
@@ -101,6 +116,8 @@
             return -1;
         }
         switch(type) {
+            case "metered-network":
+                return setNonMobileMeteredNetwork();
             case "restrict-background":
                 return setRestrictBackground();
         }
@@ -116,6 +133,8 @@
             return -1;
         }
         switch(type) {
+            case "metered-networks":
+                return listNonMobileMeteredNetworks();
             case "restrict-background-whitelist":
                 return runListRestrictBackgroundWhitelist();
         }
@@ -196,7 +215,12 @@
       if (uid < 0) {
           return uid;
       }
-      mInterface.addRestrictBackgroundWhitelistedUid(uid);
+      final long token = Binder.clearCallingIdentity();
+      try {
+          mInterface.addRestrictBackgroundWhitelistedUid(uid);
+      } finally {
+          Binder.restoreCallingIdentity(token);
+      }
       return 0;
     }
 
@@ -205,10 +229,95 @@
         if (uid < 0) {
             return uid;
         }
-        mInterface.removeRestrictBackgroundWhitelistedUid(uid);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mInterface.removeRestrictBackgroundWhitelistedUid(uid);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
         return 0;
     }
 
+    private int listNonMobileMeteredNetworks() throws RemoteException {
+        final PrintWriter pw = getOutPrintWriter();
+        final String arg = getNextArg();
+        final Boolean filter = arg == null ? null : Boolean.valueOf(arg);
+        for (NetworkPolicy policy : getNonMobilePolicies()) {
+            if (filter != null && filter.booleanValue() != policy.metered) {
+                continue;
+            }
+            pw.print(getNetworkId(policy));
+            pw.print(';');
+            pw.println(policy.metered);
+        }
+        return 0;
+    }
+
+    private int getNonMobileMeteredNetwork() throws RemoteException {
+        final PrintWriter pw = getOutPrintWriter();
+        final String id = getNextArg();
+        if (id == null) {
+            pw.println("Error: didn't specify ID");
+            return -1;
+        }
+        final List<NetworkPolicy> policies = getNonMobilePolicies();
+        for (NetworkPolicy policy: policies) {
+            if (id.equals(getNetworkId(policy))) {
+                pw.println(policy.metered);
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+    private int setNonMobileMeteredNetwork() throws RemoteException {
+        final PrintWriter pw = getOutPrintWriter();
+        final String id = getNextArg();
+        if (id == null) {
+            pw.println("Error: didn't specify ID");
+            return -1;
+        }
+        final String arg = getNextArg();
+        if (arg == null) {
+            pw.println("Error: didn't specify BOOLEAN");
+            return -1;
+        }
+        final boolean metered = Boolean.valueOf(arg);
+        final NetworkPolicy[] policies = mInterface.getNetworkPolicies(null);
+        boolean changed = false;
+        for (NetworkPolicy policy : policies) {
+            if (policy.template.isMatchRuleMobile() || policy.metered == metered) {
+                continue;
+            }
+            final String networkId = getNetworkId(policy);
+            if (id.equals(networkId)) {
+                Log.i(TAG, "Changing " + networkId + " metered status to " + metered);
+                policy.metered = metered;
+                changed = true;
+            }
+        }
+        if (changed) {
+            mInterface.setNetworkPolicies(policies);
+        }
+        return 0;
+    }
+
+    private List<NetworkPolicy> getNonMobilePolicies() throws RemoteException {
+        final NetworkPolicy[] policies = mInterface.getNetworkPolicies(null);
+        final List<NetworkPolicy> nonMobilePolicies = new ArrayList<NetworkPolicy>(policies.length);
+        for (NetworkPolicy policy: policies) {
+            if (!policy.template.isMatchRuleMobile()) {
+                nonMobilePolicies.add(policy);
+            }
+        }
+        return nonMobilePolicies;
+    }
+
+    private String getNetworkId(NetworkPolicy policy) {
+        // ids are typically enclosed on double quotes (")
+        return policy.template.getNetworkId().replaceAll("^\"|\"$", "");
+    }
+
     private int getNextBooleanArg() {
         final PrintWriter pw = getOutPrintWriter();
         final String arg = getNextArg();
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index f5da52e..29d52c1 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -250,6 +250,12 @@
         rebindServices();
     }
 
+    public void onUserUnlocked(int user) {
+        if (DEBUG) Slog.d(TAG, "onUserUnlocked u=" + user);
+        rebuildRestoredPackages();
+        rebindServices();
+    }
+
     public ManagedServiceInfo getServiceFromTokenLocked(IInterface service) {
         if (service == null) {
             return null;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 078094c..bcb2c59 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -816,6 +816,12 @@
             } else if (action.equals(Intent.ACTION_USER_REMOVED)) {
                 final int user = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
                 mZenModeHelper.onUserRemoved(user);
+            } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
+                final int user = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+                mConditionProviders.onUserUnlocked(user);
+                mListeners.onUserUnlocked(user);
+                mAssistant.onUserUnlocked(user);
+                mZenModeHelper.onUserUnlocked(user);
             }
         }
     };
@@ -994,6 +1000,7 @@
         filter.addAction(Intent.ACTION_USER_SWITCHED);
         filter.addAction(Intent.ACTION_USER_ADDED);
         filter.addAction(Intent.ACTION_USER_REMOVED);
+        filter.addAction(Intent.ACTION_USER_UNLOCKED);
         filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABILITY_CHANGED);
         getContext().registerReceiver(mIntentReceiver, filter);
 
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index b7abce21..7518c6e 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -16,7 +16,6 @@
 
 package com.android.server.notification;
 
-import static android.media.AudioAttributes.USAGE_ALARM;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
 import static android.media.AudioAttributes.USAGE_UNKNOWN;
@@ -195,19 +194,7 @@
     }
 
     public void onUserSwitched(int user) {
-        if (mUser == user || user < UserHandle.USER_SYSTEM) return;
-        mUser = user;
-        if (DEBUG) Log.d(TAG, "onUserSwitched u=" + user);
-        ZenModeConfig config = mConfigs.get(user);
-        if (config == null) {
-            if (DEBUG) Log.d(TAG, "onUserSwitched: generating default config for user " + user);
-            config = mDefaultConfig.copy();
-            config.user = user;
-        }
-        synchronized (mConfig) {
-            setConfigLocked(config, "onUserSwitched");
-        }
-        cleanUpZenRules();
+        loadConfigForUser(user, "onUserSwitched");
     }
 
     public void onUserRemoved(int user) {
@@ -216,6 +203,26 @@
         mConfigs.remove(user);
     }
 
+    public void onUserUnlocked(int user) {
+        loadConfigForUser(user, "onUserUnlocked");
+    }
+
+    private void loadConfigForUser(int user, String reason) {
+        if (mUser == user || user < UserHandle.USER_SYSTEM) return;
+        mUser = user;
+        if (DEBUG) Log.d(TAG, reason + " u=" + user);
+        ZenModeConfig config = mConfigs.get(user);
+        if (config == null) {
+            if (DEBUG) Log.d(TAG, reason + " generating default config for user " + user);
+            config = mDefaultConfig.copy();
+            config.user = user;
+        }
+        synchronized (mConfig) {
+            setConfigLocked(config, reason);
+        }
+        cleanUpZenRules();
+    }
+
     public int getZenModeListenerInterruptionFilter() {
         return NotificationManager.zenModeToInterruptionFilter(mZenMode);
     }
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index dac89ec..c08f713 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -16,11 +16,11 @@
 
 package com.android.server.pm;
 
-import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.PackageStats;
 import android.os.Build;
+import android.os.storage.StorageManager;
 import android.util.Slog;
 
 import com.android.internal.os.InstallerConnection;
@@ -29,9 +29,6 @@
 
 import dalvik.system.VMRuntime;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
 public final class Installer extends SystemService {
     private static final String TAG = "Installer";
 
@@ -52,19 +49,9 @@
     /** This is an OTA update dexopt */
     public static final int DEXOPT_OTA          = 1 << 6;
 
-    /** @hide */
-    @IntDef(flag = true, value = {
-            FLAG_DE_STORAGE,
-            FLAG_CE_STORAGE,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface StorageFlags {}
-
-    public static final int FLAG_DE_STORAGE = 1 << 0;
-    public static final int FLAG_CE_STORAGE = 1 << 1;
-
-    public static final int FLAG_CLEAR_CACHE_ONLY = 1 << 2;
-    public static final int FLAG_CLEAR_CODE_CACHE_ONLY = 1 << 3;
+    // NOTE: keep in sync with installd
+    public static final int FLAG_CLEAR_CACHE_ONLY = 1 << 8;
+    public static final int FLAG_CLEAR_CODE_CACHE_ONLY = 1 << 9;
 
     private final InstallerConnection mInstaller;
 
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 64af213..a3af561 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -176,8 +176,14 @@
                 dexoptNeeded = adjustDexoptNeeded(dexoptNeeded);
 
                 if (dexoptNeeded == DexFile.NO_DEXOPT_NEEDED) {
-                    // No dexopt needed and we don't use profiles. Nothing to do.
-                    continue;
+                    if (useProfiles) {
+                        // Profiles may trigger re-compilation. The final decision is taken in
+                        // installd.
+                        dexoptNeeded = DexFile.DEX2OAT_NEEDED;
+                    } else {
+                        // No dexopt needed and we don't use profiles. Nothing to do.
+                        continue;
+                    }
                 }
                 final String dexoptType;
                 String oatDir = null;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 553a9a2..504ce31 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -235,7 +235,6 @@
 import com.android.server.ServiceThread;
 import com.android.server.SystemConfig;
 import com.android.server.Watchdog;
-import com.android.server.pm.Installer.StorageFlags;
 import com.android.server.pm.PermissionsState.PermissionState;
 import com.android.server.pm.Settings.DatabaseVersion;
 import com.android.server.pm.Settings.VersionInfo;
@@ -2388,9 +2387,9 @@
             // can't wait for user to start
             final int flags;
             if (StorageManager.isFileBasedEncryptionEnabled()) {
-                flags = Installer.FLAG_DE_STORAGE;
+                flags = StorageManager.FLAG_STORAGE_DE;
             } else {
-                flags = Installer.FLAG_DE_STORAGE | Installer.FLAG_CE_STORAGE;
+                flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
             }
             reconcileAppsData(StorageManager.UUID_PRIVATE_INTERNAL, UserHandle.USER_SYSTEM, flags);
 
@@ -6860,7 +6859,7 @@
 
     private boolean removeDataDirsLI(String volumeUuid, String packageName) {
         // TODO: triage flags as part of 26466827
-        final int flags = Installer.FLAG_CE_STORAGE | Installer.FLAG_DE_STORAGE;
+        final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE;
 
         boolean res = true;
         final int[] users = sUserManager.getUserIds();
@@ -6906,7 +6905,7 @@
 
     private void deleteCodeCacheDirsLI(String volumeUuid, String packageName) {
         // TODO: triage flags as part of 26466827
-        final int flags = Installer.FLAG_CE_STORAGE | Installer.FLAG_DE_STORAGE;
+        final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE;
 
         final int[] users = sUserManager.getUserIds();
         for (int user : users) {
@@ -13947,7 +13946,8 @@
                 outInfo.removedUsers = new int[] {removeUser};
             }
             // TODO: triage flags as part of 26466827
-            final int installerFlags = Installer.FLAG_CE_STORAGE | Installer.FLAG_DE_STORAGE;
+            final int installerFlags = StorageManager.FLAG_STORAGE_CE
+                    | StorageManager.FLAG_STORAGE_DE;
             try {
                 mInstaller.destroyAppData(ps.volumeUuid, packageName, removeUser, installerFlags);
             } catch (InstallerException e) {
@@ -14127,7 +14127,7 @@
         // record of app. This helps users recover from UID mismatches without
         // resorting to a full data wipe.
         // TODO: triage flags as part of 26466827
-        final int flags = Installer.FLAG_CE_STORAGE | Installer.FLAG_DE_STORAGE;
+        final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE;
         try {
             mInstaller.clearAppData(pkg.volumeUuid, packageName, userId, flags);
         } catch (InstallerException e) {
@@ -14362,7 +14362,7 @@
             return false;
         }
         // TODO: triage flags as part of 26466827
-        final int flags = Installer.FLAG_CE_STORAGE | Installer.FLAG_DE_STORAGE;
+        final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE;
         try {
             mInstaller.clearAppData(p.volumeUuid, packageName, userId,
                     flags | Installer.FLAG_CLEAR_CACHE_ONLY);
@@ -14463,7 +14463,7 @@
         }
 
         // TODO: triage flags as part of 26466827
-        final int flags = Installer.FLAG_CE_STORAGE | Installer.FLAG_DE_STORAGE;
+        final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE;
         try {
             mInstaller.getAppSize(p.volumeUuid, packageName, userHandle, flags, apkPath,
                     libDirRoot, publicSrcDir, asecPath, dexCodeInstructionSets, pStats);
@@ -16787,16 +16787,20 @@
         }
 
         // Reconcile app data for all started/unlocked users
+        final StorageManager sm = mContext.getSystemService(StorageManager.class);
         final UserManager um = mContext.getSystemService(UserManager.class);
         for (UserInfo user : um.getUsers()) {
+            final int flags;
             if (um.isUserUnlocked(user.id)) {
-                reconcileAppsData(volumeUuid, user.id,
-                        Installer.FLAG_DE_STORAGE | Installer.FLAG_CE_STORAGE);
+                flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
             } else if (um.isUserRunning(user.id)) {
-                reconcileAppsData(volumeUuid, user.id, Installer.FLAG_DE_STORAGE);
+                flags = StorageManager.FLAG_STORAGE_DE;
             } else {
                 continue;
             }
+
+            sm.prepareUserStorage(volumeUuid, user.id, user.serialNumber, flags);
+            reconcileAppsData(volumeUuid, user.id, flags);
         }
 
         synchronized (mPackages) {
@@ -16905,20 +16909,6 @@
                 }
             }
         }
-
-        final StorageManager sm = mContext.getSystemService(StorageManager.class);
-        final UserManager um = mContext.getSystemService(UserManager.class);
-        for (UserInfo user : um.getUsers()) {
-            final File userDir = Environment.getDataUserDirectory(volumeUuid, user.id);
-            if (userDir.exists()) continue;
-
-            try {
-                sm.prepareUserStorage(volumeUuid, user.id, user.serialNumber, user.isEphemeral());
-                UserManagerService.enforceSerialNumber(userDir, user.serialNumber);
-            } catch (IOException e) {
-                Log.wtf(TAG, "Failed to create user directory on " + volumeUuid, e);
-            }
-        }
     }
 
     private void assertPackageKnown(String volumeUuid, String packageName)
@@ -16988,7 +16978,7 @@
      * Verifies that directories exist and that ownership and labeling is
      * correct for all installed apps on all mounted volumes.
      */
-    void reconcileAppsData(int userId, @StorageFlags int flags) {
+    void reconcileAppsData(int userId, int flags) {
         final StorageManager storage = mContext.getSystemService(StorageManager.class);
         for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
             final String volumeUuid = vol.getFsUuid();
@@ -17005,7 +16995,7 @@
      * Verifies that directories exist and that ownership and labeling is
      * correct for all installed apps.
      */
-    private void reconcileAppsData(String volumeUuid, int userId, @StorageFlags int flags) {
+    private void reconcileAppsData(String volumeUuid, int userId, int flags) {
         Slog.v(TAG, "reconcileAppsData for " + volumeUuid + " u" + userId + " 0x"
                 + Integer.toHexString(flags));
 
@@ -17016,7 +17006,7 @@
 
         // First look for stale data that doesn't belong, and check if things
         // have changed since we did our last restorecon
-        if ((flags & Installer.FLAG_CE_STORAGE) != 0) {
+        if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
             if (!isUserKeyUnlocked(userId)) {
                 throw new RuntimeException(
                         "Yikes, someone asked us to reconcile CE storage while " + userId
@@ -17034,12 +17024,12 @@
                     logCriticalInfo(Log.WARN, "Destroying " + file + " due to: " + e);
                     synchronized (mInstallLock) {
                         destroyAppDataLI(volumeUuid, packageName, userId,
-                                Installer.FLAG_CE_STORAGE);
+                                StorageManager.FLAG_STORAGE_CE);
                     }
                 }
             }
         }
-        if ((flags & Installer.FLAG_DE_STORAGE) != 0) {
+        if ((flags & StorageManager.FLAG_STORAGE_DE) != 0) {
             restoreconNeeded |= SELinuxMMAC.isRestoreconNeeded(deDir);
 
             final File[] files = FileUtils.listFilesOrEmpty(deDir);
@@ -17051,7 +17041,7 @@
                     logCriticalInfo(Log.WARN, "Destroying " + file + " due to: " + e);
                     synchronized (mInstallLock) {
                         destroyAppDataLI(volumeUuid, packageName, userId,
-                                Installer.FLAG_DE_STORAGE);
+                                StorageManager.FLAG_STORAGE_DE);
                     }
                 }
             }
@@ -17080,10 +17070,10 @@
         }
 
         if (restoreconNeeded) {
-            if ((flags & Installer.FLAG_CE_STORAGE) != 0) {
+            if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
                 SELinuxMMAC.setRestoreconDone(ceDir);
             }
-            if ((flags & Installer.FLAG_DE_STORAGE) != 0) {
+            if ((flags & StorageManager.FLAG_STORAGE_DE) != 0) {
                 SELinuxMMAC.setRestoreconDone(deDir);
             }
         }
@@ -17112,9 +17102,9 @@
         for (UserInfo user : um.getUsers()) {
             final int flags;
             if (um.isUserUnlocked(user.id)) {
-                flags = Installer.FLAG_DE_STORAGE | Installer.FLAG_CE_STORAGE;
+                flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
             } else if (um.isUserRunning(user.id)) {
-                flags = Installer.FLAG_DE_STORAGE;
+                flags = StorageManager.FLAG_STORAGE_DE;
             } else {
                 continue;
             }
@@ -17135,7 +17125,7 @@
      * will try recovering system apps by wiping data; third-party app data is
      * left intact.
      */
-    private void prepareAppData(String volumeUuid, int userId, @StorageFlags int flags,
+    private void prepareAppData(String volumeUuid, int userId, int flags,
             PackageParser.Package pkg, boolean restoreconNeeded) {
         if (DEBUG_APP_DATA) {
             Slog.v(TAG, "prepareAppData for " + pkg.packageName + " u" + userId + " 0x"
@@ -17173,7 +17163,7 @@
                 restoreconAppDataLI(volumeUuid, packageName, userId, flags, appId, app.seinfo);
             }
 
-            if ((flags & Installer.FLAG_CE_STORAGE) != 0) {
+            if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
                 // Create a native library symlink only if we have native libraries
                 // and if the native libraries are 32 bit libraries. We do not provide
                 // this symlink for 64 bit libraries.
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index bbbe693..fcb777b 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -3787,7 +3787,7 @@
                 continue;
             }
             // TODO: triage flags!
-            final int flags = Installer.FLAG_CE_STORAGE | Installer.FLAG_DE_STORAGE;
+            final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE;
             try {
                 installer.createAppData(volumeUuids[i], names[i], userHandle, flags, appIds[i],
                         seinfos[i], targetSdkVersions[i]);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 5f46567..3cc7b10 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -25,7 +25,6 @@
 import android.app.ActivityManagerNative;
 import android.app.IActivityManager;
 import android.app.IStopUserCallback;
-import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -78,7 +77,9 @@
 import com.android.internal.util.XmlUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.LocalServices;
-import com.android.server.pm.Installer.StorageFlags;
+
+import libcore.io.IoUtils;
+import libcore.util.Objects;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -96,9 +97,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import libcore.io.IoUtils;
-import libcore.util.Objects;
-
 /**
  * Service for {@link UserManager}.
  *
@@ -1893,17 +1891,8 @@
             }
             final StorageManager storage = mContext.getSystemService(StorageManager.class);
             storage.createUserKey(userId, userInfo.serialNumber, userInfo.isEphemeral());
-            for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
-                final String volumeUuid = vol.getFsUuid();
-                try {
-                    final File userDir = Environment.getDataUserDirectory(volumeUuid, userId);
-                    storage.prepareUserStorage(
-                            volumeUuid, userId, userInfo.serialNumber, userInfo.isEphemeral());
-                    enforceSerialNumber(userDir, userInfo.serialNumber);
-                } catch (IOException e) {
-                    Log.wtf(LOG_TAG, "Failed to create user directory on " + volumeUuid, e);
-                }
-            }
+            prepareUserStorage(userId, userInfo.serialNumber,
+                    StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
             mPm.createNewUser(userId);
             userInfo.partial = false;
             synchronized (mPackagesLock) {
@@ -2466,11 +2455,24 @@
     }
 
     /**
+     * Prepare storage areas for given user on all mounted devices.
+     */
+    private void prepareUserStorage(int userId, int userSerial, int flags) {
+        final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
+            final String volumeUuid = vol.getFsUuid();
+            storage.prepareUserStorage(volumeUuid, userId, userSerial, flags);
+        }
+    }
+
+    /**
      * Called right before a user is started. This gives us a chance to prepare
      * app storage and apply any user restrictions.
      */
     public void onBeforeStartUser(int userId) {
-        mPm.reconcileAppsData(userId, Installer.FLAG_DE_STORAGE);
+        final int userSerial = getUserSerialNumber(userId);
+        prepareUserStorage(userId, userSerial, StorageManager.FLAG_STORAGE_DE);
+        mPm.reconcileAppsData(userId, StorageManager.FLAG_STORAGE_DE);
 
         if (userId != UserHandle.USER_SYSTEM) {
             synchronized (mRestrictionsLock) {
@@ -2484,7 +2486,9 @@
      * app storage.
      */
     public void onBeforeUnlockUser(int userId) {
-        mPm.reconcileAppsData(userId, Installer.FLAG_CE_STORAGE);
+        final int userSerial = getUserSerialNumber(userId);
+        prepareUserStorage(userId, userSerial, StorageManager.FLAG_STORAGE_CE);
+        mPm.reconcileAppsData(userId, StorageManager.FLAG_STORAGE_CE);
     }
 
     /**
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 43b82e9..a92cc31 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -6877,7 +6877,12 @@
         final boolean dockedStackVisible = mWindowManagerInternal.isStackVisible(DOCKED_STACK_ID);
         final boolean freeformStackVisible =
                 mWindowManagerInternal.isStackVisible(FREEFORM_WORKSPACE_STACK_ID);
-        final boolean forceShowSystemBars = dockedStackVisible || freeformStackVisible;
+        final boolean resizing = mWindowManagerInternal.isDockedDividerResizing();
+
+        // We need to force system bars when the docked stack is visible, when the freeform stack
+        // is visible but also when we are resizing for the transitions when docked stack
+        // visibility changes.
+        final boolean forceShowSystemBars = dockedStackVisible || freeformStackVisible || resizing;
         final boolean forceOpaqueSystemBars = forceShowSystemBars && !mForceStatusBarFromKeyguard;
 
         // apply translucent bar vis flags
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index b065e85..342c078 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -584,14 +584,14 @@
         for (IBinder sessionToken : serviceState.sessionTokens) {
             SessionState sessionState = userState.sessionStateMap.get(sessionToken);
             if (sessionState.session == null && (inputId == null
-                    || sessionState.info.getId().equals(inputId))) {
+                    || sessionState.inputId.equals(inputId))) {
                 sessionsToAbort.add(sessionState);
             }
         }
         for (SessionState sessionState : sessionsToAbort) {
             removeSessionStateLocked(sessionState.sessionToken, sessionState.userId);
             sendSessionTokenToClientLocked(sessionState.client,
-                    sessionState.info.getId(), null, null, sessionState.seq);
+                    sessionState.inputId, null, null, sessionState.seq);
         }
         updateServiceConnectionLocked(serviceState.component, userId);
     }
@@ -601,7 +601,7 @@
         UserState userState = getOrCreateUserStateLocked(userId);
         SessionState sessionState = userState.sessionStateMap.get(sessionToken);
         if (DEBUG) {
-            Slog.d(TAG, "createSessionInternalLocked(inputId=" + sessionState.info.getId() + ")");
+            Slog.d(TAG, "createSessionInternalLocked(inputId=" + sessionState.inputId + ")");
         }
         InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString());
 
@@ -611,14 +611,14 @@
         // Create a session. When failed, send a null token immediately.
         try {
             if (sessionState.isRecordingSession) {
-                service.createRecordingSession(callback, sessionState.info.getId());
+                service.createRecordingSession(callback, sessionState.inputId);
             } else {
-                service.createSession(channels[1], callback, sessionState.info.getId());
+                service.createSession(channels[1], callback, sessionState.inputId);
             }
         } catch (RemoteException e) {
             Slog.e(TAG, "error in createSession", e);
             removeSessionStateLocked(sessionToken, userId);
-            sendSessionTokenToClientLocked(sessionState.client, sessionState.info.getId(), null,
+            sendSessionTokenToClientLocked(sessionState.client, sessionState.inputId, null,
                     null, sessionState.seq);
         }
         channels[1].dispose();
@@ -684,14 +684,11 @@
             }
         }
 
-        TvInputInfo info = sessionState.info;
-        if (info != null) {
-            ServiceState serviceState = userState.serviceStateMap.get(info.getComponent());
-            if (serviceState != null) {
-                serviceState.sessionTokens.remove(sessionToken);
-            }
+        ServiceState serviceState = userState.serviceStateMap.get(sessionState.componentName);
+        if (serviceState != null) {
+            serviceState.sessionTokens.remove(sessionToken);
         }
-        updateServiceConnectionLocked(sessionState.info.getComponent(), userId);
+        updateServiceConnectionLocked(sessionState.componentName, userId);
 
         // Log the end of watch.
         SomeArgs args = SomeArgs.obtain();
@@ -707,7 +704,7 @@
                 sessionState = getSessionStateLocked(sessionState.hardwareSessionToken,
                         Process.SYSTEM_UID, userId);
             }
-            ServiceState serviceState = getServiceStateLocked(sessionState.info.getComponent(), userId);
+            ServiceState serviceState = getServiceStateLocked(sessionState.componentName, userId);
             if (!serviceState.isHardware) {
                 return;
             }
@@ -1091,8 +1088,9 @@
 
                     // Create a new session token and a session state.
                     IBinder sessionToken = new Binder();
-                    SessionState sessionState = new SessionState(sessionToken, info,
-                            isRecordingSession, client, seq, callingUid, resolvedUserId);
+                    SessionState sessionState = new SessionState(sessionToken, info.getId(),
+                            info.getComponent(), isRecordingSession, client, seq, callingUid,
+                            resolvedUserId);
 
                     // Add them to the global session state map of the current user.
                     userState.sessionStateMap.put(sessionToken, sessionState);
@@ -1273,7 +1271,7 @@
 
                         // Log the start of watch.
                         SomeArgs args = SomeArgs.obtain();
-                        args.arg1 = sessionState.info.getComponent().getPackageName();
+                        args.arg1 = sessionState.componentName.getPackageName();
                         args.arg2 = System.currentTimeMillis();
                         args.arg3 = ContentUris.parseId(channelUri);
                         args.arg4 = params;
@@ -1779,10 +1777,10 @@
                         return false;
                     }
                     for (SessionState sessionState : userState.sessionStateMap.values()) {
-                        if (sessionState.info.getId().equals(inputId)
+                        if (sessionState.inputId.equals(inputId)
                                 && sessionState.hardwareSessionToken != null) {
                             hardwareInputId = userState.sessionStateMap.get(
-                                    sessionState.hardwareSessionToken).info.getId();
+                                    sessionState.hardwareSessionToken).inputId;
                             break;
                         }
                     }
@@ -1918,7 +1916,7 @@
                         pw.println(entry.getKey() + ": " + session);
 
                         pw.increaseIndent();
-                        pw.println("info: " + session.info);
+                        pw.println("inputId: " + session.inputId);
                         pw.println("client: " + session.client);
                         pw.println("seq: " + session.seq);
                         pw.println("callingUid: " + session.callingUid);
@@ -2046,7 +2044,8 @@
     }
 
     private final class SessionState implements IBinder.DeathRecipient {
-        private final TvInputInfo info;
+        private final String inputId;
+        private final ComponentName componentName;
         private final boolean isRecordingSession;
         private final ITvInputClient client;
         private final int seq;
@@ -2058,10 +2057,12 @@
         // Not null if this session represents an external device connected to a hardware TV input.
         private IBinder hardwareSessionToken;
 
-        private SessionState(IBinder sessionToken, TvInputInfo info, boolean isRecordingSession,
-                ITvInputClient client, int seq, int callingUid, int userId) {
+        private SessionState(IBinder sessionToken, String inputId, ComponentName componentName,
+                boolean isRecordingSession, ITvInputClient client, int seq, int callingUid,
+                int userId) {
             this.sessionToken = sessionToken;
-            this.info = info;
+            this.inputId = inputId;
+            this.componentName = componentName;
             this.isRecordingSession = isRecordingSession;
             this.client = client;
             this.seq = seq;
@@ -2274,19 +2275,19 @@
         @Override
         public void onSessionCreated(ITvInputSession session, IBinder hardwareSessionToken) {
             if (DEBUG) {
-                Slog.d(TAG, "onSessionCreated(inputId=" + mSessionState.info.getId() + ")");
+                Slog.d(TAG, "onSessionCreated(inputId=" + mSessionState.inputId + ")");
             }
             synchronized (mLock) {
                 mSessionState.session = session;
                 mSessionState.hardwareSessionToken = hardwareSessionToken;
                 if (session != null && addSessionTokenToClientStateLocked(session)) {
                     sendSessionTokenToClientLocked(mSessionState.client,
-                            mSessionState.info.getId(), mSessionState.sessionToken, mChannels[0],
+                            mSessionState.inputId, mSessionState.sessionToken, mChannels[0],
                             mSessionState.seq);
                 } else {
                     removeSessionStateLocked(mSessionState.sessionToken, mSessionState.userId);
                     sendSessionTokenToClientLocked(mSessionState.client,
-                            mSessionState.info.getId(), null, null, mSessionState.seq);
+                            mSessionState.inputId, null, null, mSessionState.seq);
                 }
                 mChannels[0].dispose();
             }
diff --git a/services/core/java/com/android/server/wm/BoundsAnimationController.java b/services/core/java/com/android/server/wm/BoundsAnimationController.java
index 5f97478..1bfdcce 100644
--- a/services/core/java/com/android/server/wm/BoundsAnimationController.java
+++ b/services/core/java/com/android/server/wm/BoundsAnimationController.java
@@ -22,6 +22,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.graphics.Rect;
 import android.util.ArrayMap;
@@ -50,13 +51,15 @@
         private final Rect mFrom;
         private final Rect mTo;
         private final Rect mTmpRect;
+        private final boolean mMoveToFullScreen;
 
-        BoundsAnimator(AnimateBoundsUser target, Rect from, Rect to) {
+        BoundsAnimator(AnimateBoundsUser target, Rect from, Rect to, boolean moveToFullScreen) {
             super();
             mTarget = target;
             mFrom = from;
             mTo = to;
             mTmpRect = new Rect();
+            mMoveToFullScreen = moveToFullScreen;
             addUpdateListener(this);
             addListener(this);
         }
@@ -88,6 +91,9 @@
         @Override
         public void onAnimationEnd(Animator animation) {
             finishAnimation();
+            if (mMoveToFullScreen) {
+                mTarget.moveToFullscreen();
+            }
         }
 
         @Override
@@ -125,14 +131,25 @@
          * necessary cleanup.
          */
         void finishBoundsAnimation();
+
+        void moveToFullscreen();
+
+        void getFullScreenBounds(Rect bounds);
     }
 
-    void animateBounds(AnimateBoundsUser target, Rect from, Rect to) {
+    void animateBounds(final AnimateBoundsUser target, Rect from, Rect to) {
+        boolean moveToFullscreen = false;
+        if (to == null) {
+            to = new Rect();
+            target.getFullScreenBounds(to);
+            moveToFullscreen = true;
+        }
+
         final BoundsAnimator existing = mRunningAnimations.get(target);
         if (existing != null) {
             existing.cancel();
         }
-        BoundsAnimator animator = new BoundsAnimator(target, from, to);
+        BoundsAnimator animator = new BoundsAnimator(target, from, to, moveToFullscreen);
         mRunningAnimations.put(target, animator);
         animator.setFloatValues(0f, 1f);
         animator.setDuration(DEFAULT_APP_TRANSITION_DURATION);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index be1b85c..a06d3fc 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -289,11 +289,9 @@
         if (displayContent != null) {
             displayContent.getLogicalDisplayRect(mTmpRect);
             rotation = displayContent.getDisplayInfo().rotation;
-            if (bounds == null) {
+            mFullscreen = bounds == null;
+            if (mFullscreen) {
                 bounds = mTmpRect;
-                mFullscreen = true;
-            } else {
-                mFullscreen = mTmpRect.equals(bounds);
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index 4659131..40ca1c5 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -255,11 +255,9 @@
         if (mDisplayContent != null) {
             mDisplayContent.getLogicalDisplayRect(mTmpRect);
             rotation = mDisplayContent.getDisplayInfo().rotation;
-            if (bounds == null) {
+            mFullscreen = bounds == null;
+            if (mFullscreen) {
                 bounds = mTmpRect;
-                mFullscreen = true;
-            } else {
-                mFullscreen = mTmpRect.equals(bounds);
             }
         }
 
@@ -587,7 +585,8 @@
     }
 
     void getStackDockedModeBoundsLocked(Rect outBounds, boolean ignoreVisibility) {
-        if (!StackId.isResizeableByDockedStack(mStackId) || mDisplayContent == null) {
+        if ((mStackId != DOCKED_STACK_ID && !StackId.isResizeableByDockedStack(mStackId))
+                || mDisplayContent == null) {
             outBounds.set(mBounds);
             return;
         }
@@ -616,8 +615,7 @@
 
         mDisplayContent.getLogicalDisplayRect(mTmpRect);
         dockedStack.getRawBounds(mTmpRect2);
-        final boolean dockedOnTopOrLeft = dockedSide == DOCKED_TOP
-                || dockedSide == DOCKED_LEFT;
+        final boolean dockedOnTopOrLeft = dockedSide == DOCKED_TOP || dockedSide == DOCKED_LEFT;
         getStackDockedModeBounds(mTmpRect, outBounds, mStackId, mTmpRect2,
                 mDisplayContent.mDividerControllerLocked.getContentWidth(), dockedOnTopOrLeft);
 
@@ -722,6 +720,19 @@
         }
     }
 
+    void resetDockedStackToMiddle() {
+        if (mStackId != DOCKED_STACK_ID) {
+            throw new IllegalStateException("Not a docked stack=" + this);
+        }
+
+        mService.mDockedStackCreateBounds = null;
+
+        final Rect bounds = new Rect();
+        getStackDockedModeBoundsLocked(bounds, true /*ignoreVisibility*/);
+        mService.mH.obtainMessage(RESIZE_STACK, DOCKED_STACK_ID,
+                1 /*allowResizeInDockedMode*/, bounds).sendToTarget();
+    }
+
     void detachDisplay() {
         EventLog.writeEvent(EventLogTags.WM_STACK_REMOVED, mStackId);
 
@@ -865,14 +876,14 @@
         final int orientation = mService.mCurConfiguration.orientation;
         if (orientation == Configuration.ORIENTATION_PORTRAIT) {
             // Portrait mode, docked either at the top or the bottom.
-            if (bounds.top - mTmpRect.top < mTmpRect.bottom - bounds.bottom) {
+            if (bounds.top - mTmpRect.top <= mTmpRect.bottom - bounds.bottom) {
                 return DOCKED_TOP;
             } else {
                 return DOCKED_BOTTOM;
             }
         } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
             // Landscape mode, docked either on the left or on the right.
-            if (bounds.left - mTmpRect.left < mTmpRect.right - bounds.right) {
+            if (bounds.left - mTmpRect.left <= mTmpRect.right - bounds.right) {
                 return DOCKED_LEFT;
             } else {
                 return DOCKED_RIGHT;
@@ -927,4 +938,18 @@
             }
         }
     }
+
+    @Override
+    public void moveToFullscreen() {
+        try {
+            mService.mActivityManager.moveTasksToFullscreenStack(mStackId, true);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void getFullScreenBounds(Rect bounds) {
+        getDisplayContent().getContentRect(bounds);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a26430e..ae6c89a 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -464,6 +464,8 @@
     EmulatorDisplayOverlay mEmulatorDisplayOverlay;
 
     final float[] mTmpFloats = new float[9];
+    final Rect mTmpRect = new Rect();
+    final Rect mTmpRect2 = new Rect();
 
     boolean mDisplayReady;
     boolean mSafeMode;
@@ -4845,17 +4847,6 @@
         }
     }
 
-    /** Returns true if the input bounds corresponds to the fullscreen bounds the stack is on. */
-    public boolean isFullscreenBounds(int stackId, Rect bounds) {
-        synchronized (mWindowMap) {
-            final TaskStack stack = mStackIdToStack.get(stackId);
-            if (stack == null || bounds == null) {
-                return true;
-            }
-            return stack.isFullscreenBounds(bounds);
-        }
-    }
-
     /**
      * Re-sizes a stack and its containing tasks.
      * @param stackId Id of stack to resize.
@@ -5373,8 +5364,18 @@
             mWindowPlacerLocked.performSurfacePlacement();
 
             // Notify whether the docked stack exists for the current user
-            getDefaultDisplayContentLocked().mDividerControllerLocked
+            final DisplayContent displayContent = getDefaultDisplayContentLocked();
+            displayContent.mDividerControllerLocked
                     .notifyDockedStackExistsChanged(hasDockedTasksForUser(newUserId));
+
+            // If the display is already prepared, update the density.
+            // Otherwise, we'll update it when it's prepared.
+            if (mDisplayReady) {
+                final int forcedDensity = getForcedDisplayDensityForUserLocked(newUserId);
+                final int targetDensity = forcedDensity != 0 ? forcedDensity
+                        : displayContent.mInitialDisplayDensity;
+                setForcedDisplayDensityLocked(displayContent, targetDensity);
+            }
         }
     }
 
@@ -8370,21 +8371,9 @@
         }
 
         // Display density.
-        String densityStr = Settings.Global.getString(mContext.getContentResolver(),
-                Settings.Global.DISPLAY_DENSITY_FORCED);
-        if (densityStr == null || densityStr.length() == 0) {
-            densityStr = SystemProperties.get(DENSITY_OVERRIDE, null);
-        }
-        if (densityStr != null && densityStr.length() > 0) {
-            int density;
-            try {
-                density = Integer.parseInt(densityStr);
-                if (displayContent.mBaseDisplayDensity != density) {
-                    Slog.i(TAG_WM, "FORCED DISPLAY DENSITY: " + density);
-                    displayContent.mBaseDisplayDensity = density;
-                }
-            } catch (NumberFormatException ex) {
-            }
+        final int density = getForcedDisplayDensityForUserLocked(mCurrentUserId);
+        if (density != 0) {
+            displayContent.mBaseDisplayDensity = density;
         }
 
         // Display scaling mode.
@@ -8470,8 +8459,9 @@
                 final DisplayContent displayContent = getDisplayContentLocked(displayId);
                 if (displayContent != null) {
                     setForcedDisplayDensityLocked(displayContent, density);
-                    Settings.Global.putString(mContext.getContentResolver(),
-                            Settings.Global.DISPLAY_DENSITY_FORCED, Integer.toString(density));
+                    Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                            Settings.Secure.DISPLAY_DENSITY_FORCED,
+                            Integer.toString(density), mCurrentUserId);
                 }
             }
         } finally {
@@ -8479,13 +8469,6 @@
         }
     }
 
-    // displayContent must not be null
-    private void setForcedDisplayDensityLocked(DisplayContent displayContent, int density) {
-        Slog.i(TAG_WM, "Using new display density: " + density);
-        displayContent.mBaseDisplayDensity = density;
-        reconfigureDisplayLocked(displayContent);
-    }
-
     @Override
     public void clearForcedDisplayDensity(int displayId) {
         if (mContext.checkCallingOrSelfPermission(
@@ -8504,8 +8487,8 @@
                 if (displayContent != null) {
                     setForcedDisplayDensityLocked(displayContent,
                             displayContent.mInitialDisplayDensity);
-                    Settings.Global.putString(mContext.getContentResolver(),
-                            Settings.Global.DISPLAY_DENSITY_FORCED, "");
+                    Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                            Settings.Secure.DISPLAY_DENSITY_FORCED, "", mCurrentUserId);
                 }
             }
         } finally {
@@ -8513,6 +8496,38 @@
         }
     }
 
+    /**
+     * @param userId the ID of the user
+     * @return the forced display density for the specified user, if set, or
+     *         {@code 0} if not set
+     */
+    private int getForcedDisplayDensityForUserLocked(int userId) {
+        String densityStr = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.DISPLAY_DENSITY_FORCED, userId);
+        if (densityStr == null || densityStr.length() == 0) {
+            densityStr = SystemProperties.get(DENSITY_OVERRIDE, null);
+        }
+        if (densityStr != null && densityStr.length() > 0) {
+            try {
+                return Integer.parseInt(densityStr);
+            } catch (NumberFormatException ex) {
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Forces the given display to the use the specified density.
+     *
+     * @param displayContent the display to modify
+     * @param density the density in DPI to use
+     */
+    private void setForcedDisplayDensityLocked(@NonNull DisplayContent displayContent,
+            int density) {
+        displayContent.mBaseDisplayDensity = density;
+        reconfigureDisplayLocked(displayContent);
+    }
+
     // displayContent must not be null
     private void reconfigureDisplayLocked(DisplayContent displayContent) {
         // TODO: Multidisplay: for now only use with default display.
@@ -10394,8 +10409,28 @@
     @Override
     public void getStableInsets(Rect outInsets) throws RemoteException {
         synchronized (mWindowMap) {
+            getStableInsetsLocked(outInsets);
+        }
+    }
+
+    private void getStableInsetsLocked(Rect outInsets) {
+        final DisplayInfo di = getDefaultDisplayInfoLocked();
+        mPolicy.getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight, outInsets);
+    }
+
+    /**
+     * Intersects the specified {@code inOutBounds} with the display frame that excludes the stable
+     * inset areas.
+     *
+     * @param inOutBounds The inOutBounds to subtract the stable inset areas from.
+     */
+    public void subtractStableInsets(Rect inOutBounds) {
+        synchronized (mWindowMap) {
+            getStableInsetsLocked(mTmpRect2);
             final DisplayInfo di = getDefaultDisplayInfoLocked();
-            mPolicy.getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight, outInsets);
+            mTmpRect.set(0, 0, di.logicalWidth, di.logicalHeight);
+            mTmpRect.inset(mTmpRect2);
+            inOutBounds.intersect(mTmpRect);
         }
     }
 
@@ -10599,5 +10634,12 @@
                 return WindowManagerService.this.isStackVisibleLocked(stackId);
             }
         }
+
+        @Override
+        public boolean isDockedDividerResizing() {
+            synchronized (mWindowMap) {
+                return getDefaultDisplayContentLocked().getDockedDividerController().isResizing();
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 465c7e0..880514c 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -16,8 +16,6 @@
 
 package com.android.server.wm;
 
-import com.android.server.input.InputWindowHandle;
-
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.content.Context;
@@ -53,10 +51,13 @@
 import android.view.WindowManager;
 import android.view.WindowManagerPolicy;
 
+import com.android.server.input.InputWindowHandle;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
 import static android.app.ActivityManager.StackId;
+import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT;
@@ -75,8 +76,8 @@
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
@@ -1259,8 +1260,11 @@
      * it may obscure windows behind it.
      */
     boolean isOpaqueDrawn() {
-        return (mAttrs.format == PixelFormat.OPAQUE
-                        || mAttrs.type == TYPE_WALLPAPER)
+        // When there is keyguard, wallpaper could be placed over the secure app
+        // window but invisible. We need to check wallpaper visibility explicitly
+        // to determine if it's occluding apps.
+        return ((!mIsWallpaper && mAttrs.format == PixelFormat.OPAQUE)
+                || (mIsWallpaper && mWallpaperVisible))
                 && isDrawnLw() && mWinAnimator.mAnimation == null
                 && (mAppToken == null || mAppToken.mAppAnimator.animation == null);
     }
@@ -1608,6 +1612,14 @@
                             win.mAppToken.appDied = true;
                         }
                         mService.removeWindowLocked(win);
+                        if (win.mAttrs.type == TYPE_DOCK_DIVIDER) {
+                            // The owner of the docked divider died :( We reset the docked stack,
+                            // just in case they have the divider at an unstable position.
+                            final TaskStack stack = mService.mStackIdToStack.get(DOCKED_STACK_ID);
+                            if (stack != null) {
+                                stack.resetDockedStackToMiddle();
+                            }
+                        }
                     } else if (mHasSurface) {
                         Slog.e(TAG, "!!! LEAK !!! Window removed but surface still valid.");
                         mService.removeWindowLocked(WindowState.this);
@@ -2133,7 +2145,8 @@
         // background.
         return (mDisplayContent.mDividerControllerLocked.isResizing()
                         || mAppToken != null && !mAppToken.mFrozenBounds.isEmpty()) &&
-                !task.inFreeformWorkspace() && !task.isFullscreen();
+                !task.inFreeformWorkspace();
+
     }
 
     void setDragResizing() {
@@ -2327,6 +2340,12 @@
         if (mDrawLock != null) {
             pw.print(prefix); pw.println("mDrawLock=" + mDrawLock);
         }
+        if (isDragResizing()) {
+            pw.print(prefix); pw.println("isDragResizing=" + isDragResizing());
+        }
+        if (computeDragResizing()) {
+            pw.print(prefix); pw.println("computeDragResizing=" + computeDragResizing());
+        }
     }
 
     String makeInputChannelName() {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 908d2f0..f296d68 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -4132,7 +4132,7 @@
     public void choosePrivateKeyAlias(final int uid, final Uri uri, final String alias,
             final IBinder response) {
         // Caller UID needs to be trusted, so we restrict this method to SYSTEM_UID callers.
-        if (!UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID)) {
+        if (!isCallerWithSystemUid()) {
             return;
         }
 
@@ -5860,8 +5860,7 @@
         }
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, null);
-        if (hasUserSetupCompleted(userHandle)
-                && !UserHandle.isSameApp(callingUid, Process.SYSTEM_UID)) {
+        if (hasUserSetupCompleted(userHandle) && !isCallerWithSystemUid()) {
             throw new IllegalStateException("Cannot set the profile owner on a user which is "
                     + "already set-up");
         }
@@ -5921,8 +5920,7 @@
 
     private void enforceManageUsers() {
         final int callingUid = mInjector.binderGetCallingUid();
-        if (!(UserHandle.isSameApp(callingUid, Process.SYSTEM_UID)
-                || callingUid == Process.ROOT_UID)) {
+        if (!(isCallerWithSystemUid() || callingUid == Process.ROOT_UID)) {
             mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USERS, null);
         }
     }
@@ -5945,8 +5943,7 @@
         if (userHandle == UserHandle.getUserId(callingUid)) {
             return;
         }
-        if (!(UserHandle.isSameApp(callingUid, Process.SYSTEM_UID)
-                || callingUid == Process.ROOT_UID)) {
+        if (!(isCallerWithSystemUid() || callingUid == Process.ROOT_UID)) {
             mContext.enforceCallingOrSelfPermission(permission,
                     "Must be system or have " + permission + " permission");
         }
@@ -5964,6 +5961,10 @@
         }
     }
 
+    private boolean isCallerWithSystemUid() {
+        return UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID);
+    }
+
     private int getProfileParentId(int userHandle) {
         final long ident = mInjector.binderClearCallingIdentity();
         try {
@@ -6248,7 +6249,7 @@
     @Override
     public ComponentName getRestrictionsProvider(int userHandle) {
         synchronized (this) {
-            if (!UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID)) {
+            if (!isCallerWithSystemUid()) {
                 throw new SecurityException("Only the system can query the permission provider");
             }
             DevicePolicyData userData = getUserData(userHandle);
@@ -6321,8 +6322,7 @@
      * permittedList or are a system app.
      */
     private boolean checkPackagesInPermittedListOrSystem(List<String> enabledPackages,
-            List<String> permittedList) {
-        int userIdToCheck = UserHandle.getCallingUserId();
+            List<String> permittedList, int userIdToCheck) {
         long id = mInjector.binderClearCallingIdentity();
         try {
             // If we have an enabled packages list for a managed profile the packages
@@ -6389,7 +6389,8 @@
                 for (AccessibilityServiceInfo service : enabledServices) {
                     enabledPackages.add(service.getResolveInfo().serviceInfo.packageName);
                 }
-                if (!checkPackagesInPermittedListOrSystem(enabledPackages, packageList)) {
+                if (!checkPackagesInPermittedListOrSystem(enabledPackages, packageList,
+                        userId)) {
                     Slog.e(LOG_TAG, "Cannot set permitted accessibility services, "
                             + "because it contains already enabled accesibility services.");
                     return false;
@@ -6481,6 +6482,28 @@
         }
     }
 
+    @Override
+    public boolean isAccessibilityServicePermittedByAdmin(ComponentName who, String packageName,
+            int userHandle) {
+        if (!mHasFeature) {
+            return true;
+        }
+        Preconditions.checkNotNull(who, "ComponentName is null");
+        Preconditions.checkStringNotEmpty(packageName, "packageName is null");
+        if (!isCallerWithSystemUid()){
+            throw new SecurityException(
+                    "Only the system can query if an accessibility service is disabled by admin");
+        }
+        synchronized (this) {
+            ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
+            if (admin.permittedAccessiblityServices == null) {
+                return true;
+            }
+            return checkPackagesInPermittedListOrSystem(Arrays.asList(packageName),
+                    admin.permittedAccessiblityServices, userHandle);
+        }
+    }
+
     private boolean checkCallerIsCurrentUserOrProfile() {
         int callingUserId = UserHandle.getCallingUserId();
         long token = mInjector.binderClearCallingIdentity();
@@ -6536,7 +6559,8 @@
                 for (InputMethodInfo ime : enabledImes) {
                     enabledPackages.add(ime.getPackageName());
                 }
-                if (!checkPackagesInPermittedListOrSystem(enabledPackages, packageList)) {
+                if (!checkPackagesInPermittedListOrSystem(enabledPackages, packageList,
+                        mInjector.binderGetCallingUserHandle().getIdentifier())) {
                     Slog.e(LOG_TAG, "Cannot set permitted input methods, "
                             + "because it contains already enabled input method.");
                     return false;
@@ -6629,6 +6653,28 @@
     }
 
     @Override
+    public boolean isInputMethodPermittedByAdmin(ComponentName who, String packageName,
+            int userHandle) {
+        if (!mHasFeature) {
+            return true;
+        }
+        Preconditions.checkNotNull(who, "ComponentName is null");
+        Preconditions.checkStringNotEmpty(packageName, "packageName is null");
+        if (!isCallerWithSystemUid()) {
+            throw new SecurityException(
+                    "Only the system can query if an input method is disabled by admin");
+        }
+        synchronized (this) {
+            ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
+            if (admin.permittedInputMethods == null) {
+                return true;
+            }
+            return checkPackagesInPermittedListOrSystem(Arrays.asList(packageName),
+                    admin.permittedInputMethods, userHandle);
+        }
+    }
+
+    @Override
     public UserHandle createUser(ComponentName who, String name) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         synchronized (this) {
@@ -7425,7 +7471,7 @@
 
     @Override
     public void notifyLockTaskModeChanged(boolean isEnabled, String pkg, int userHandle) {
-        if (!UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID)) {
+        if (!isCallerWithSystemUid()) {
             throw new SecurityException("notifyLockTaskModeChanged can only be called by system");
         }
         synchronized (this) {
@@ -8180,7 +8226,7 @@
             return null;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        if (!UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID)) {
+        if (!isCallerWithSystemUid()) {
             throw new SecurityException("Only the system can query support message for user");
         }
         synchronized (this) {
@@ -8198,7 +8244,7 @@
             return null;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        if (!UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID)) {
+        if (!isCallerWithSystemUid()) {
             throw new SecurityException("Only the system can query support message for user");
         }
         synchronized (this) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index b64db57..0cf9328 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -496,6 +496,7 @@
         boolean disableNonCoreServices = SystemProperties.getBoolean("config.disable_noncore", false);
         boolean disableNetwork = SystemProperties.getBoolean("config.disable_network", false);
         boolean disableNetworkTime = SystemProperties.getBoolean("config.disable_networktime", false);
+        boolean disableRtt = SystemProperties.getBoolean("config.disable_rtt", false);
         boolean isEmulator = SystemProperties.get("ro.kernel.qemu").equals("1");
 
         try {
@@ -788,7 +789,9 @@
                 mSystemServiceManager.startService(
                             "com.android.server.wifi.WifiScanningService");
 
-                mSystemServiceManager.startService("com.android.server.wifi.RttService");
+                if (!disableRtt) {
+                    mSystemServiceManager.startService("com.android.server.wifi.RttService");
+                }
 
                 if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_ETHERNET) ||
                     mPackageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
@@ -1088,7 +1091,9 @@
 
                 mSystemServiceManager.startService(TrustManagerService.class);
 
-                mSystemServiceManager.startService(FingerprintService.class);
+                if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+                    mSystemServiceManager.startService(FingerprintService.class);
+                }
 
                 traceBeginAndSlog("StartBackgroundDexOptService");
                 try {
diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk
index 25cb64c..071ec1b0 100644
--- a/services/tests/servicestests/Android.mk
+++ b/services/tests/servicestests/Android.mk
@@ -15,6 +15,7 @@
     services.core \
     services.devicepolicy \
     services.net \
+    services.usage \
     easymocklib \
     guava \
     android-support-test \
@@ -26,7 +27,9 @@
 
 LOCAL_CERTIFICATE := platform
 
-LOCAL_JNI_SHARED_LIBRARIES := libapfjni
+LOCAL_JNI_SHARED_LIBRARIES := \
+    libapfjni \
+    libnativehelper
 
 include $(BUILD_PACKAGE)
 
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java
index 3dc1a9a..53ca45d 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java
@@ -29,6 +29,7 @@
 import java.io.File;
 import java.util.List;
 
+import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doReturn;
 
@@ -135,8 +136,7 @@
 
         doReturn(realResolveInfo).when(mMockContext.packageManager).queryBroadcastReceiversAsUser(
                 MockUtils.checkIntentComponent(admin),
-                eq(PackageManager.GET_META_DATA
-                        | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS),
+                anyInt(),
                 eq(UserHandle.getUserId(packageUid)));
 
         // Set up getPackageInfo().
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java b/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java
new file mode 100644
index 0000000..9ccb1a6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 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.server.usage;
+
+import android.os.FileUtils;
+import android.test.AndroidTestCase;
+
+import java.io.File;
+
+public class AppIdleHistoryTests extends AndroidTestCase {
+
+    File mStorageDir;
+
+    final static String PACKAGE_1 = "com.android.testpackage1";
+    final static String PACKAGE_2 = "com.android.testpackage2";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mStorageDir = new File(getContext().getFilesDir(), "appidle");
+        mStorageDir.mkdirs();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        FileUtils.deleteContents(mStorageDir);
+        super.tearDown();
+    }
+
+    public void testFilesCreation() {
+        final int userId = 0;
+        AppIdleHistory aih = new AppIdleHistory(mStorageDir, 0);
+
+        aih.updateDisplayLocked(true, /* elapsedRealtime= */ 1000);
+        aih.updateDisplayLocked(false, /* elapsedRealtime= */ 2000);
+        // Screen On time file should be written right away
+        assertTrue(aih.getScreenOnTimeFile().exists());
+
+        aih.writeAppIdleTimesLocked(userId);
+        // stats file should be written now
+        assertTrue(new File(new File(mStorageDir, "users/" + userId),
+                AppIdleHistory.APP_IDLE_FILENAME).exists());
+    }
+
+    public void testScreenOnTime() {
+        AppIdleHistory aih = new AppIdleHistory(mStorageDir, 1000);
+        aih.updateDisplayLocked(false, 2000);
+        assertEquals(aih.getScreenOnTimeLocked(2000), 0);
+        aih.updateDisplayLocked(true, 3000);
+        assertEquals(aih.getScreenOnTimeLocked(4000), 1000);
+        assertEquals(aih.getScreenOnTimeLocked(5000), 2000);
+        aih.updateDisplayLocked(false, 6000);
+        // Screen on time should not keep progressing with screen is off
+        assertEquals(aih.getScreenOnTimeLocked(7000), 3000);
+        assertEquals(aih.getScreenOnTimeLocked(8000), 3000);
+        aih.writeElapsedTimeLocked();
+
+        // Check if the screen on time is persisted across instantiations
+        AppIdleHistory aih2 = new AppIdleHistory(mStorageDir, 0);
+        assertEquals(aih2.getScreenOnTimeLocked(11000), 3000);
+        aih2.updateDisplayLocked(true, 4000);
+        aih2.updateDisplayLocked(false, 5000);
+        assertEquals(aih2.getScreenOnTimeLocked(13000), 4000);
+    }
+
+    public void testPackageEvents() {
+        AppIdleHistory aih = new AppIdleHistory(mStorageDir, 1000);
+        aih.setThresholds(4000, 1000);
+        aih.updateDisplayLocked(true, 1000);
+        // App is not-idle by default
+        assertFalse(aih.isIdleLocked(PACKAGE_1, 0, 1500));
+        // Still not idle
+        assertFalse(aih.isIdleLocked(PACKAGE_1, 0, 3000));
+        // Idle now
+        assertTrue(aih.isIdleLocked(PACKAGE_1, 0, 8000));
+        // Not idle
+        assertFalse(aih.isIdleLocked(PACKAGE_2, 0, 9000));
+
+        // Screen off
+        aih.updateDisplayLocked(false, 9100);
+        // Still idle after 10 seconds because screen hasn't been on long enough
+        assertFalse(aih.isIdleLocked(PACKAGE_2, 0, 20000));
+        aih.updateDisplayLocked(true, 21000);
+        assertTrue(aih.isIdleLocked(PACKAGE_2, 0, 23000));
+    }
+}
\ No newline at end of file
diff --git a/services/usage/java/com/android/server/usage/AppIdleHistory.java b/services/usage/java/com/android/server/usage/AppIdleHistory.java
index e3c0868..3e2b43d 100644
--- a/services/usage/java/com/android/server/usage/AppIdleHistory.java
+++ b/services/usage/java/com/android/server/usage/AppIdleHistory.java
@@ -16,19 +16,45 @@
 
 package com.android.server.usage;
 
+import android.os.Environment;
+import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Slog;
 import android.util.SparseArray;
+import android.util.TimeUtils;
+import android.util.Xml;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.IndentingPrintWriter;
 
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
 /**
  * Keeps track of recent active state changes in apps.
  * Access should be guarded by a lock by the caller.
  */
 public class AppIdleHistory {
 
-    private SparseArray<ArrayMap<String,byte[]>> mIdleHistory = new SparseArray<>();
-    private long lastPeriod = 0;
+    private static final String TAG = "AppIdleHistory";
+
+    // History for all users and all packages
+    private SparseArray<ArrayMap<String,PackageHistory>> mIdleHistory = new SparseArray<>();
+    private long mLastPeriod = 0;
     private static final long ONE_MINUTE = 60 * 1000;
     private static final int HISTORY_SIZE = 100;
     private static final int FLAG_LAST_STATE = 2;
@@ -36,77 +62,353 @@
     private static final long PERIOD_DURATION = UsageStatsService.COMPRESS_TIME ? ONE_MINUTE
             : 60 * ONE_MINUTE;
 
-    public void addEntry(String packageName, int userId, boolean idle, long timeNow) {
-        ArrayMap<String, byte[]> userHistory = getUserHistory(userId);
-        byte[] packageHistory = getPackageHistory(userHistory, packageName);
+    @VisibleForTesting
+    static final String APP_IDLE_FILENAME = "app_idle_stats.xml";
+    private static final String TAG_PACKAGES = "packages";
+    private static final String TAG_PACKAGE = "package";
+    private static final String ATTR_NAME = "name";
+    // Screen on timebase time when app was last used
+    private static final String ATTR_SCREEN_IDLE = "screenIdleTime";
+    // Elapsed timebase time when app was last used
+    private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime";
 
-        long thisPeriod = timeNow / PERIOD_DURATION;
+    // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
+    private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
+    private long mElapsedDuration; // Total device on duration since device was "born"
+
+    // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot)
+    private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration
+    private long mScreenOnDuration; // Total screen on duration since device was "born"
+
+    private long mElapsedTimeThreshold;
+    private long mScreenOnTimeThreshold;
+    private final File mStorageDir;
+
+    private boolean mScreenOn;
+
+    private static class PackageHistory {
+        final byte[] recent = new byte[HISTORY_SIZE];
+        long lastUsedElapsedTime;
+        long lastUsedScreenTime;
+    }
+
+    AppIdleHistory(long elapsedRealtime) {
+        this(Environment.getDataSystemDirectory(), elapsedRealtime);
+    }
+
+    @VisibleForTesting
+    AppIdleHistory(File storageDir, long elapsedRealtime) {
+        mElapsedSnapshot = elapsedRealtime;
+        mScreenOnSnapshot = elapsedRealtime;
+        mStorageDir = storageDir;
+        readScreenOnTimeLocked();
+    }
+
+    public void setThresholds(long elapsedTimeThreshold, long screenOnTimeThreshold) {
+        mElapsedTimeThreshold = elapsedTimeThreshold;
+        mScreenOnTimeThreshold = screenOnTimeThreshold;
+    }
+
+    public void updateDisplayLocked(boolean screenOn, long elapsedRealtime) {
+        if (screenOn == mScreenOn) return;
+
+        mScreenOn = screenOn;
+        if (mScreenOn) {
+            mScreenOnSnapshot = elapsedRealtime;
+        } else {
+            mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot;
+            mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
+            writeScreenOnTimeLocked();
+            mElapsedSnapshot = elapsedRealtime;
+        }
+    }
+
+    public long getScreenOnTimeLocked(long elapsedRealtime) {
+        long screenOnTime = mScreenOnDuration;
+        if (mScreenOn) {
+            screenOnTime += elapsedRealtime - mScreenOnSnapshot;
+        }
+        return screenOnTime;
+    }
+
+    @VisibleForTesting
+    File getScreenOnTimeFile() {
+        return new File(mStorageDir, "screen_on_time");
+    }
+
+    private void readScreenOnTimeLocked() {
+        File screenOnTimeFile = getScreenOnTimeFile();
+        if (screenOnTimeFile.exists()) {
+            try {
+                BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile));
+                mScreenOnDuration = Long.parseLong(reader.readLine());
+                mElapsedDuration = Long.parseLong(reader.readLine());
+                reader.close();
+            } catch (IOException | NumberFormatException e) {
+            }
+        } else {
+            writeScreenOnTimeLocked();
+        }
+    }
+
+    private void writeScreenOnTimeLocked() {
+        AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile());
+        FileOutputStream fos = null;
+        try {
+            fos = screenOnTimeFile.startWrite();
+            fos.write((Long.toString(mScreenOnDuration) + "\n"
+                    + Long.toString(mElapsedDuration) + "\n").getBytes());
+            screenOnTimeFile.finishWrite(fos);
+        } catch (IOException ioe) {
+            screenOnTimeFile.failWrite(fos);
+        }
+    }
+
+    /**
+     * To be called periodically to keep track of elapsed time when app idle times are written
+     */
+    public void writeElapsedTimeLocked() {
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
+        // Only bump up and snapshot the elapsed time. Don't change screen on duration.
+        mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
+        mElapsedSnapshot = elapsedRealtime;
+        writeScreenOnTimeLocked();
+    }
+
+    public void reportUsageLocked(String packageName, int userId, long elapsedRealtime) {
+        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
+        PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName,
+                elapsedRealtime);
+
+        shiftHistoryToNow(userHistory, elapsedRealtime);
+
+        packageHistory.lastUsedElapsedTime = mElapsedDuration
+                + (elapsedRealtime - mElapsedSnapshot);
+        packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime);
+        packageHistory.recent[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE;
+    }
+
+    public void setIdle(String packageName, int userId, long elapsedRealtime) {
+        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
+        PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName,
+                elapsedRealtime);
+
+        shiftHistoryToNow(userHistory, elapsedRealtime);
+
+        packageHistory.recent[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE;
+    }
+
+    private void shiftHistoryToNow(ArrayMap<String, PackageHistory> userHistory,
+            long elapsedRealtime) {
+        long thisPeriod = elapsedRealtime / PERIOD_DURATION;
         // Has the period switched over? Slide all users' package histories
-        if (lastPeriod != 0 && lastPeriod < thisPeriod
-                && (thisPeriod - lastPeriod) < HISTORY_SIZE - 1) {
-            int diff = (int) (thisPeriod - lastPeriod);
+        if (mLastPeriod != 0 && mLastPeriod < thisPeriod
+                && (thisPeriod - mLastPeriod) < HISTORY_SIZE - 1) {
+            int diff = (int) (thisPeriod - mLastPeriod);
             final int NUSERS = mIdleHistory.size();
             for (int u = 0; u < NUSERS; u++) {
                 userHistory = mIdleHistory.valueAt(u);
-                for (byte[] history : userHistory.values()) {
+                for (PackageHistory idleState : userHistory.values()) {
                     // Shift left
-                    System.arraycopy(history, diff, history, 0, HISTORY_SIZE - diff);
+                    System.arraycopy(idleState.recent, diff, idleState.recent, 0,
+                            HISTORY_SIZE - diff);
                     // Replicate last state across the diff
                     for (int i = 0; i < diff; i++) {
-                        history[HISTORY_SIZE - i - 1] =
-                                (byte) (history[HISTORY_SIZE - diff - 1] & FLAG_LAST_STATE);
+                        idleState.recent[HISTORY_SIZE - i - 1] =
+                            (byte) (idleState.recent[HISTORY_SIZE - diff - 1] & FLAG_LAST_STATE);
                     }
                 }
             }
         }
-        lastPeriod = thisPeriod;
-        if (!idle) {
-            packageHistory[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE;
-        } else {
-            packageHistory[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE;
-        }
+        mLastPeriod = thisPeriod;
     }
 
-    private ArrayMap<String, byte[]> getUserHistory(int userId) {
-        ArrayMap<String, byte[]> userHistory = mIdleHistory.get(userId);
+    private ArrayMap<String, PackageHistory> getUserHistoryLocked(int userId) {
+        ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
         if (userHistory == null) {
             userHistory = new ArrayMap<>();
             mIdleHistory.put(userId, userHistory);
+            readAppIdleTimesLocked(userId, userHistory);
         }
         return userHistory;
     }
 
-    private byte[] getPackageHistory(ArrayMap<String, byte[]> userHistory, String packageName) {
-        byte[] packageHistory = userHistory.get(packageName);
+    private PackageHistory getPackageHistoryLocked(ArrayMap<String, PackageHistory> userHistory,
+            String packageName, long elapsedRealtime) {
+        PackageHistory packageHistory = userHistory.get(packageName);
         if (packageHistory == null) {
-            packageHistory = new byte[HISTORY_SIZE];
+            packageHistory = new PackageHistory();
+            packageHistory.lastUsedElapsedTime = getElapsedTimeLocked(elapsedRealtime);
+            packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime);
             userHistory.put(packageName, packageHistory);
         }
         return packageHistory;
     }
 
-    public void removeUser(int userId) {
+    public void onUserRemoved(int userId) {
         mIdleHistory.remove(userId);
     }
 
-    public boolean isIdle(int userId, String packageName) {
-        ArrayMap<String, byte[]> userHistory = getUserHistory(userId);
-        byte[] packageHistory = getPackageHistory(userHistory, packageName);
-        return (packageHistory[HISTORY_SIZE - 1] & FLAG_LAST_STATE) == 0;
+    public boolean isIdleLocked(String packageName, int userId, long elapsedRealtime) {
+        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
+        PackageHistory packageHistory =
+                getPackageHistoryLocked(userHistory, packageName, elapsedRealtime);
+        if (packageHistory == null) {
+            return false; // Default to not idle
+        } else {
+            return hasPassedThresholdsLocked(packageHistory, elapsedRealtime);
+        }
+    }
+
+    private long getElapsedTimeLocked(long elapsedRealtime) {
+        return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
+    }
+
+    public void setIdleLocked(String packageName, int userId, boolean idle, long elapsedRealtime) {
+        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
+        PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName,
+                elapsedRealtime);
+        packageHistory.lastUsedElapsedTime = getElapsedTimeLocked(elapsedRealtime)
+                - mElapsedTimeThreshold;
+        packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime)
+                - (idle ? mScreenOnTimeThreshold : 0) - 1000 /* just a second more */;
+    }
+
+    private boolean hasPassedThresholdsLocked(PackageHistory packageHistory, long elapsedRealtime) {
+        return (packageHistory.lastUsedScreenTime
+                    <= getScreenOnTimeLocked(elapsedRealtime) - mScreenOnTimeThreshold)
+                && (packageHistory.lastUsedElapsedTime
+                        <= getElapsedTimeLocked(elapsedRealtime) - mElapsedTimeThreshold);
+    }
+
+    private File getUserFile(int userId) {
+        return new File(new File(new File(mStorageDir, "users"),
+                Integer.toString(userId)), APP_IDLE_FILENAME);
+    }
+
+    private void readAppIdleTimesLocked(int userId, ArrayMap<String, PackageHistory> userHistory) {
+        FileInputStream fis = null;
+        try {
+            AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
+            fis = appIdleFile.openRead();
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(fis, StandardCharsets.UTF_8.name());
+
+            int type;
+            while ((type = parser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                // Skip
+            }
+
+            if (type != XmlPullParser.START_TAG) {
+                Slog.e(TAG, "Unable to read app idle file for user " + userId);
+                return;
+            }
+            if (!parser.getName().equals(TAG_PACKAGES)) {
+                return;
+            }
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+                if (type == XmlPullParser.START_TAG) {
+                    final String name = parser.getName();
+                    if (name.equals(TAG_PACKAGE)) {
+                        final String packageName = parser.getAttributeValue(null, ATTR_NAME);
+                        PackageHistory packageHistory = new PackageHistory();
+                        packageHistory.lastUsedElapsedTime =
+                                Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
+                        packageHistory.lastUsedScreenTime =
+                                Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
+                        userHistory.put(packageName, packageHistory);
+                    }
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            Slog.e(TAG, "Unable to read app idle file for user " + userId);
+        } finally {
+            IoUtils.closeQuietly(fis);
+        }
+    }
+
+    public void writeAppIdleTimesLocked(int userId) {
+        FileOutputStream fos = null;
+        AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
+        try {
+            fos = appIdleFile.startWrite();
+            final BufferedOutputStream bos = new BufferedOutputStream(fos);
+
+            FastXmlSerializer xml = new FastXmlSerializer();
+            xml.setOutput(bos, StandardCharsets.UTF_8.name());
+            xml.startDocument(null, true);
+            xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+            xml.startTag(null, TAG_PACKAGES);
+
+            ArrayMap<String,PackageHistory> userHistory = getUserHistoryLocked(userId);
+            final int N = userHistory.size();
+            for (int i = 0; i < N; i++) {
+                String packageName = userHistory.keyAt(i);
+                PackageHistory history = userHistory.valueAt(i);
+                xml.startTag(null, TAG_PACKAGE);
+                xml.attribute(null, ATTR_NAME, packageName);
+                xml.attribute(null, ATTR_ELAPSED_IDLE,
+                        Long.toString(history.lastUsedElapsedTime));
+                xml.attribute(null, ATTR_SCREEN_IDLE,
+                        Long.toString(history.lastUsedScreenTime));
+                xml.endTag(null, TAG_PACKAGE);
+            }
+
+            xml.endTag(null, TAG_PACKAGES);
+            xml.endDocument();
+            appIdleFile.finishWrite(fos);
+        } catch (Exception e) {
+            appIdleFile.failWrite(fos);
+            Slog.e(TAG, "Error writing app idle file for user " + userId);
+        }
     }
 
     public void dump(IndentingPrintWriter idpw, int userId) {
-        ArrayMap<String, byte[]> userHistory = mIdleHistory.get(userId);
+        idpw.println("Package idle stats:");
+        idpw.increaseIndent();
+        ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
+        final long totalElapsedTime = getElapsedTimeLocked(elapsedRealtime);
+        final long screenOnTime = getScreenOnTimeLocked(elapsedRealtime);
         if (userHistory == null) return;
         final int P = userHistory.size();
         for (int p = 0; p < P; p++) {
             final String packageName = userHistory.keyAt(p);
-            final byte[] history = userHistory.valueAt(p);
+            final PackageHistory packageHistory = userHistory.valueAt(p);
+            idpw.print("package=" + packageName);
+            idpw.print(" lastUsedElapsed=");
+            TimeUtils.formatDuration(totalElapsedTime - packageHistory.lastUsedElapsedTime, idpw);
+            idpw.print(" lastUsedScreenOn=");
+            TimeUtils.formatDuration(screenOnTime - packageHistory.lastUsedScreenTime, idpw);
+            idpw.print(" idle=" + (isIdleLocked(packageName, userId, elapsedRealtime) ? "y" : "n"));
+            idpw.println();
+        }
+        idpw.println();
+        idpw.print("totalElapsedTime=");
+        TimeUtils.formatDuration(getElapsedTimeLocked(elapsedRealtime), idpw);
+        idpw.println();
+        idpw.print("totalScreenOnTime=");
+        TimeUtils.formatDuration(getScreenOnTimeLocked(elapsedRealtime), idpw);
+        idpw.println();
+        idpw.decreaseIndent();
+    }
+
+    public void dumpHistory(IndentingPrintWriter idpw, int userId) {
+        ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
+        if (userHistory == null) return;
+        final int P = userHistory.size();
+        for (int p = 0; p < P; p++) {
+            final String packageName = userHistory.keyAt(p);
+            final byte[] history = userHistory.valueAt(p).recent;
             for (int i = 0; i < HISTORY_SIZE; i++) {
                 idpw.print(history[i] == 0 ? '.' : 'A');
             }
+            idpw.print(" idle=" + (isIdleLocked(packageName, userId, elapsedRealtime) ? "y" : "n"));
             idpw.print("  " + packageName);
             idpw.println();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/services/usage/java/com/android/server/usage/IntervalStats.java b/services/usage/java/com/android/server/usage/IntervalStats.java
index 7f379fe..f541f70 100644
--- a/services/usage/java/com/android/server/usage/IntervalStats.java
+++ b/services/usage/java/com/android/server/usage/IntervalStats.java
@@ -48,7 +48,6 @@
             usageStats.mPackageName = getCachedStringRef(packageName);
             usageStats.mBeginTimeStamp = beginTime;
             usageStats.mEndTimeStamp = endTime;
-            usageStats.mBeginIdleTime = 0;
             packageStats.put(usageStats.mPackageName, usageStats);
         }
         return usageStats;
@@ -113,7 +112,6 @@
         if (eventType != UsageEvents.Event.SYSTEM_INTERACTION) {
             usageStats.mLastTimeUsed = timeStamp;
         }
-        usageStats.mLastTimeSystemUsed = timeStamp;
         usageStats.mEndTimeStamp = timeStamp;
 
         if (eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
@@ -123,22 +121,6 @@
         endTime = timeStamp;
     }
 
-    /**
-     * Updates the last active time for the package. The timestamp uses a timebase that
-     * tracks the device usage time.
-     * @param packageName
-     * @param timeStamp
-     */
-    void updateBeginIdleTime(String packageName, long timeStamp) {
-        UsageStats usageStats = getOrCreateUsageStats(packageName);
-        usageStats.mBeginIdleTime = timeStamp;
-    }
-
-    void updateSystemLastUsedTime(String packageName, long lastUsedTime) {
-        UsageStats usageStats = getOrCreateUsageStats(packageName);
-        usageStats.mLastTimeSystemUsed = lastUsedTime;
-    }
-
     void updateConfigurationStats(Configuration config, long timeStamp) {
         if (activeConfiguration != null) {
             ConfigurationStats activeStats = configurations.get(activeConfiguration);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 77740387..46ad8a1 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -40,6 +40,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.UserInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.hardware.display.DisplayManager;
@@ -62,7 +63,6 @@
 import android.provider.Settings;
 import android.telephony.TelephonyManager;
 import android.util.ArraySet;
-import android.util.AtomicFile;
 import android.util.KeyValueListParser;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -77,12 +77,8 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.SystemService;
 
-import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -106,7 +102,7 @@
     private static final long FLUSH_INTERVAL = COMPRESS_TIME ? TEN_SECONDS : TWENTY_MINUTES;
     private static final long TIME_CHANGE_THRESHOLD_MILLIS = 2 * 1000; // Two seconds.
 
-    long mAppIdleDurationMillis;
+    long mAppIdleScreenThresholdMillis;
     long mCheckIdleIntervalMillis;
     long mAppIdleWallclockThresholdMillis;
     long mAppIdleParoleIntervalMillis;
@@ -147,11 +143,8 @@
 
     private volatile boolean mPendingOneTimeCheckIdleStates;
 
-    long mScreenOnTime;
-    long mLastScreenOnEventRealtime;
-
     @GuardedBy("mLock")
-    private AppIdleHistory mAppIdleHistory = new AppIdleHistory();
+    private AppIdleHistory mAppIdleHistory;
 
     private ArrayList<UsageStatsManagerInternal.AppIdleStateChangeListener>
             mPackageAccessListeners = new ArrayList<>();
@@ -191,8 +184,7 @@
 
         synchronized (mLock) {
             cleanUpRemovedUsersLocked();
-            mLastScreenOnEventRealtime = SystemClock.elapsedRealtime();
-            mScreenOnTime = readScreenOnTimeLocked();
+            mAppIdleHistory = new AppIdleHistory(SystemClock.elapsedRealtime());
         }
 
         mRealTimeSnapshot = SystemClock.elapsedRealtime();
@@ -221,7 +213,7 @@
 
             mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
             synchronized (mLock) {
-                updateDisplayLocked();
+                mAppIdleHistory.updateDisplayLocked(isDisplayOn(), SystemClock.elapsedRealtime());
             }
 
             if (mPendingOneTimeCheckIdleStates) {
@@ -232,6 +224,11 @@
         }
     }
 
+    private boolean isDisplayOn() {
+        return mDisplayManager
+                .getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_ON;
+    }
+
     private class UserActionsReceiver extends BroadcastReceiver {
 
         @Override
@@ -274,7 +271,8 @@
         @Override public void onDisplayChanged(int displayId) {
             if (displayId == Display.DEFAULT_DISPLAY) {
                 synchronized (UsageStatsService.this.mLock) {
-                    updateDisplayLocked();
+                    mAppIdleHistory.updateDisplayLocked(isDisplayOn(),
+                            SystemClock.elapsedRealtime());
                 }
             }
         }
@@ -291,8 +289,25 @@
     }
 
     @Override
-    public long getAppIdleRollingWindowDurationMillis() {
-        return mAppIdleWallclockThresholdMillis * 2;
+    public void onNewUpdate(int userId) {
+        initializeDefaultsForSystemApps(userId);
+    }
+
+    private void initializeDefaultsForSystemApps(int userId) {
+        Slog.d(TAG, "Initializing defaults for system apps on user " + userId);
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
+        List<PackageInfo> packages = getContext().getPackageManager().getInstalledPackagesAsUser(
+                PackageManager.MATCH_DISABLED_COMPONENTS
+                | PackageManager.MATCH_UNINSTALLED_PACKAGES,
+                userId);
+        final int packageCount = packages.size();
+        for (int i = 0; i < packageCount; i++) {
+            final PackageInfo pi = packages.get(i);
+            String packageName = pi.packageName;
+            if (pi.applicationInfo != null && pi.applicationInfo.isSystemApp()) {
+                mAppIdleHistory.reportUsageLocked(packageName, userId, elapsedRealtime);
+            }
+        }
     }
 
     private void cleanUpRemovedUsersLocked() {
@@ -350,7 +365,7 @@
         if (timeLeft < 0) {
             timeLeft = 0;
         }
-        mHandler.sendEmptyMessageDelayed(MSG_CHECK_PAROLE_TIMEOUT, timeLeft / 10);
+        mHandler.sendEmptyMessageDelayed(MSG_CHECK_PAROLE_TIMEOUT, timeLeft);
     }
 
     private void postParoleEndTimeout() {
@@ -400,28 +415,27 @@
             return;
         }
 
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
         for (int i = 0; i < userIds.length; i++) {
             final int userId = userIds[i];
             List<PackageInfo> packages =
                     getContext().getPackageManager().getInstalledPackagesAsUser(
-                            PackageManager.GET_DISABLED_COMPONENTS
-                                | PackageManager.GET_UNINSTALLED_PACKAGES,
+                            PackageManager.MATCH_DISABLED_COMPONENTS
+                                | PackageManager.MATCH_UNINSTALLED_PACKAGES,
                             userId);
             synchronized (mLock) {
-                final long timeNow = checkAndGetTimeLocked();
-                final long screenOnTime = getScreenOnTimeLocked();
-                UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId,
-                        timeNow);
                 final int packageCount = packages.size();
                 for (int p = 0; p < packageCount; p++) {
                     final PackageInfo pi = packages.get(p);
                     final String packageName = pi.packageName;
                     final boolean isIdle = isAppIdleFiltered(packageName,
                             UserHandle.getAppId(pi.applicationInfo.uid),
-                            userId, service, timeNow, screenOnTime);
+                            userId, elapsedRealtime);
                     mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS,
                             userId, isIdle ? 1 : 0, packageName));
-                    mAppIdleHistory.addEntry(packageName, userId, isIdle, timeNow);
+                    if (isIdle) {
+                        mAppIdleHistory.setIdle(packageName, userId, elapsedRealtime);
+                    }
                 }
             }
         }
@@ -458,62 +472,6 @@
         }
     }
 
-    void updateDisplayLocked() {
-        boolean screenOn = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).getState()
-                == Display.STATE_ON;
-
-        if (screenOn == mScreenOn) return;
-
-        mScreenOn = screenOn;
-        long now = SystemClock.elapsedRealtime();
-        if (mScreenOn) {
-            mLastScreenOnEventRealtime = now;
-        } else {
-            mScreenOnTime += now - mLastScreenOnEventRealtime;
-            writeScreenOnTimeLocked(mScreenOnTime);
-        }
-    }
-
-    long getScreenOnTimeLocked() {
-        long screenOnTime = mScreenOnTime;
-        if (mScreenOn) {
-            screenOnTime += SystemClock.elapsedRealtime() - mLastScreenOnEventRealtime;
-        }
-        return screenOnTime;
-    }
-
-    private File getScreenOnTimeFile() {
-        return new File(mUsageStatsDir, UserHandle.USER_SYSTEM + "/screen_on_time");
-    }
-
-    private long readScreenOnTimeLocked() {
-        long screenOnTime = 0;
-        File screenOnTimeFile = getScreenOnTimeFile();
-        if (screenOnTimeFile.exists()) {
-            try {
-                BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile));
-                screenOnTime = Long.parseLong(reader.readLine());
-                reader.close();
-            } catch (IOException | NumberFormatException e) {
-            }
-        } else {
-            writeScreenOnTimeLocked(screenOnTime);
-        }
-        return screenOnTime;
-    }
-
-    private void writeScreenOnTimeLocked(long screenOnTime) {
-        AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile());
-        FileOutputStream fos = null;
-        try {
-            fos = screenOnTimeFile.startWrite();
-            fos.write(Long.toString(screenOnTime).getBytes());
-            screenOnTimeFile.finishWrite(fos);
-        } catch (IOException ioe) {
-            screenOnTimeFile.failWrite(fos);
-        }
-    }
-
     void onDeviceIdleModeChanged() {
         final boolean deviceIdle = mPowerManager.isDeviceIdleMode();
         if (DEBUG) Slog.i(TAG, "DeviceIdleMode changed to " + deviceIdle);
@@ -549,7 +507,7 @@
         if (service == null) {
             service = new UserUsageStatsService(getContext(), userId,
                     new File(mUsageStatsDir, Integer.toString(userId)), this);
-            service.init(currentTimeMillis, getScreenOnTimeLocked());
+            service.init(currentTimeMillis);
             mUserState.put(userId, service);
         }
         return service;
@@ -569,8 +527,7 @@
             final int userCount = mUserState.size();
             for (int i = 0; i < userCount; i++) {
                 final UserUsageStatsService service = mUserState.valueAt(i);
-                service.onTimeChanged(expectedSystemTime, actualSystemTime, getScreenOnTimeLocked(),
-                        false);
+                service.onTimeChanged(expectedSystemTime, actualSystemTime);
             }
             mRealTimeSnapshot = actualRealtime;
             mSystemTimeSnapshot = actualSystemTime;
@@ -602,26 +559,26 @@
     void reportEvent(UsageEvents.Event event, int userId) {
         synchronized (mLock) {
             final long timeNow = checkAndGetTimeLocked();
-            final long screenOnTime = getScreenOnTimeLocked();
+            final long elapsedRealtime = SystemClock.elapsedRealtime();
             convertToSystemTimeLocked(event);
 
             final UserUsageStatsService service =
                     getUserDataAndInitializeIfNeededLocked(userId, timeNow);
-            final long beginIdleTime = service.getBeginIdleTime(event.mPackage);
-            final long lastUsedTime = service.getSystemLastUsedTime(event.mPackage);
-            final boolean previouslyIdle = hasPassedIdleTimeoutLocked(beginIdleTime,
-                    lastUsedTime, screenOnTime, timeNow);
-            service.reportEvent(event, screenOnTime);
+            // TODO: Ideally this should call isAppIdleFiltered() to avoid calling back
+            // about apps that are on some kind of whitelist anyway.
+            final boolean previouslyIdle = mAppIdleHistory.isIdleLocked(
+                    event.mPackage, userId, elapsedRealtime);
+            service.reportEvent(event);
             // Inform listeners if necessary
             if ((event.mEventType == Event.MOVE_TO_FOREGROUND
                     || event.mEventType == Event.MOVE_TO_BACKGROUND
                     || event.mEventType == Event.SYSTEM_INTERACTION
                     || event.mEventType == Event.USER_INTERACTION)) {
+                mAppIdleHistory.reportUsageLocked(event.mPackage, userId, elapsedRealtime);
                 if (previouslyIdle) {
                     mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId,
                             /* idle = */ 0, event.mPackage));
                     notifyBatteryStats(event.mPackage, userId, false);
-                    mAppIdleHistory.addEntry(event.mPackage, userId, false, timeNow);
                 }
             }
         }
@@ -655,28 +612,23 @@
      * the threshold for idle.
      */
     void forceIdleState(String packageName, int userId, boolean idle) {
+        final int appId = getAppId(packageName);
+        if (appId < 0) return;
         synchronized (mLock) {
-            final long timeNow = checkAndGetTimeLocked();
-            final long screenOnTime = getScreenOnTimeLocked();
-            final long deviceUsageTime = screenOnTime - (idle ? mAppIdleDurationMillis : 0) - 5000;
+            final long elapsedRealtime = SystemClock.elapsedRealtime();
 
-            final UserUsageStatsService service =
-                    getUserDataAndInitializeIfNeededLocked(userId, timeNow);
-            final long beginIdleTime = service.getBeginIdleTime(packageName);
-            final long lastUsedTime = service.getSystemLastUsedTime(packageName);
-            final boolean previouslyIdle = hasPassedIdleTimeoutLocked(beginIdleTime,
-                    lastUsedTime, screenOnTime, timeNow);
-            service.setBeginIdleTime(packageName, deviceUsageTime);
-            service.setSystemLastUsedTime(packageName,
-                    timeNow - (idle ? mAppIdleWallclockThresholdMillis : 0) - 5000);
+            final boolean previouslyIdle = isAppIdleFiltered(packageName, appId,
+                    userId, elapsedRealtime);
+            mAppIdleHistory.setIdleLocked(packageName, userId, idle, elapsedRealtime);
+            final boolean stillIdle = isAppIdleFiltered(packageName, appId,
+                    userId, elapsedRealtime);
             // Inform listeners if necessary
-            if (previouslyIdle != idle) {
+            if (previouslyIdle != stillIdle) {
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId,
-                        /* idle = */ idle ? 1 : 0, packageName));
-                if (!idle) {
+                        /* idle = */ stillIdle ? 1 : 0, packageName));
+                if (!stillIdle) {
                     notifyBatteryStats(packageName, userId, idle);
                 }
-                mAppIdleHistory.addEntry(packageName, userId, idle, timeNow);
             }
         }
     }
@@ -693,10 +645,11 @@
     /**
      * Called by the Binder stub.
      */
-    void removeUser(int userId) {
+    void onUserRemoved(int userId) {
         synchronized (mLock) {
             Slog.i(TAG, "Removing user " + userId + " and all data.");
             mUserState.remove(userId);
+            mAppIdleHistory.onUserRemoved(userId);
             cleanUpRemovedUsersLocked();
         }
     }
@@ -750,29 +703,12 @@
         }
     }
 
-    private boolean isAppIdleUnfiltered(String packageName, UserUsageStatsService userService,
-            long timeNow, long screenOnTime) {
+    private boolean isAppIdleUnfiltered(String packageName, int userId, long elapsedRealtime) {
         synchronized (mLock) {
-            long beginIdleTime = userService.getBeginIdleTime(packageName);
-            long lastUsedTime = userService.getSystemLastUsedTime(packageName);
-            return hasPassedIdleTimeoutLocked(beginIdleTime, lastUsedTime, screenOnTime,
-                    timeNow);
+            return mAppIdleHistory.isIdleLocked(packageName, userId, elapsedRealtime);
         }
     }
 
-    /**
-     * @param beginIdleTime when the app was last used in device usage timebase
-     * @param lastUsedTime wallclock time of when the app was last used
-     * @param screenOnTime screen-on timebase time
-     * @param currentTime current time in device usage timebase
-     * @return whether it's been used far enough in the past to be considered inactive
-     */
-    boolean hasPassedIdleTimeoutLocked(long beginIdleTime, long lastUsedTime,
-            long screenOnTime, long currentTime) {
-        return (beginIdleTime <= screenOnTime - mAppIdleDurationMillis)
-                && (lastUsedTime <= currentTime - mAppIdleWallclockThresholdMillis);
-    }
-
     void addListener(AppIdleStateChangeListener listener) {
         synchronized (mLock) {
             if (!mPackageAccessListeners.contains(listener)) {
@@ -787,32 +723,22 @@
         }
     }
 
-    boolean isAppIdleFilteredOrParoled(String packageName, int userId, long timeNow) {
+    int getAppId(String packageName) {
+        try {
+            ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(packageName,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES
+                            | PackageManager.MATCH_DISABLED_COMPONENTS);
+            return ai.uid;
+        } catch (NameNotFoundException re) {
+            return -1;
+        }
+    }
+
+    boolean isAppIdleFilteredOrParoled(String packageName, int userId, long elapsedRealtime) {
         if (mAppIdleParoled) {
             return false;
         }
-        try {
-            ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(packageName,
-                    PackageManager.GET_UNINSTALLED_PACKAGES
-                            | PackageManager.GET_DISABLED_COMPONENTS);
-            return isAppIdleFiltered(packageName, ai.uid, userId, timeNow);
-        } catch (PackageManager.NameNotFoundException e) {
-        }
-        return false;
-    }
-
-    boolean isAppIdleFiltered(String packageName, int uidForAppId, int userId, long timeNow) {
-        final UserUsageStatsService userService;
-        final long screenOnTime;
-        synchronized (mLock) {
-            if (timeNow == -1) {
-                timeNow = checkAndGetTimeLocked();
-            }
-            userService = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
-            screenOnTime = getScreenOnTimeLocked();
-        }
-        return isAppIdleFiltered(packageName, UserHandle.getAppId(uidForAppId), userId,
-                userService, timeNow, screenOnTime);
+        return isAppIdleFiltered(packageName, getAppId(packageName), userId, elapsedRealtime);
     }
 
     /**
@@ -822,7 +748,7 @@
      * Called by interface impls.
      */
     private boolean isAppIdleFiltered(String packageName, int appId, int userId,
-            UserUsageStatsService userService, long timeNow, long screenOnTime) {
+            long elapsedRealtime) {
         if (packageName == null) return false;
         // If not enabled at all, of course nobody is ever idle.
         if (!mAppIdleEnabled) {
@@ -864,7 +790,7 @@
             return false;
         }
 
-        return isAppIdleUnfiltered(packageName, userService, timeNow, screenOnTime);
+        return isAppIdleUnfiltered(packageName, userId, elapsedRealtime);
     }
 
     int[] getIdleUidsForUser(int userId) {
@@ -872,14 +798,7 @@
             return new int[0];
         }
 
-        final long timeNow;
-        final UserUsageStatsService userService;
-        final long screenOnTime;
-        synchronized (mLock) {
-            timeNow = checkAndGetTimeLocked();
-            userService = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
-            screenOnTime = getScreenOnTimeLocked();
-        }
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
 
         List<ApplicationInfo> apps;
         try {
@@ -899,12 +818,12 @@
 
         // Now resolve all app state.  Iterating over all apps, keeping track of how many
         // we find for each uid and how many of those are idle.
-        for (int i = apps.size()-1; i >= 0; i--) {
+        for (int i = apps.size() - 1; i >= 0; i--) {
             ApplicationInfo ai = apps.get(i);
 
             // Check whether this app is idle.
             boolean idle = isAppIdleFiltered(ai.packageName, UserHandle.getAppId(ai.uid),
-                    userId, userService, timeNow, screenOnTime);
+                    userId, elapsedRealtime);
 
             int index = uidStates.indexOfKey(ai.uid);
             if (index < 0) {
@@ -990,8 +909,11 @@
         for (int i = 0; i < userCount; i++) {
             UserUsageStatsService service = mUserState.valueAt(i);
             service.persistActiveStats();
+            mAppIdleHistory.writeAppIdleTimesLocked(mUserState.keyAt(i));
         }
-
+        // Persist elapsed time periodically, in case screen doesn't get toggled
+        // until the next boot
+        mAppIdleHistory.writeElapsedTimeLocked();
         mHandler.removeMessages(MSG_FLUSH_TO_DISK);
     }
 
@@ -1000,7 +922,6 @@
      */
     void dump(String[] args, PrintWriter pw) {
         synchronized (mLock) {
-            final long screenOnTime = getScreenOnTimeLocked();
             IndentingPrintWriter idpw = new IndentingPrintWriter(pw, "  ");
             ArraySet<String> argSet = new ArraySet<>();
             argSet.addAll(Arrays.asList(args));
@@ -1011,27 +932,28 @@
                 idpw.println();
                 idpw.increaseIndent();
                 if (argSet.contains("--checkin")) {
-                    mUserState.valueAt(i).checkin(idpw, screenOnTime);
+                    mUserState.valueAt(i).checkin(idpw);
                 } else {
-                    mUserState.valueAt(i).dump(idpw, screenOnTime);
+                    mUserState.valueAt(i).dump(idpw);
                     idpw.println();
-                    if (args.length > 0 && "history".equals(args[0])) {
-                        mAppIdleHistory.dump(idpw, mUserState.keyAt(i));
+                    if (args.length > 0) {
+                        if ("history".equals(args[0])) {
+                            mAppIdleHistory.dumpHistory(idpw, mUserState.keyAt(i));
+                        } else if ("flush".equals(args[0])) {
+                            UsageStatsService.this.flushToDiskLocked();
+                            pw.println("Flushed stats to disk");
+                        }
                     }
                 }
+                mAppIdleHistory.dump(idpw, mUserState.keyAt(i));
                 idpw.decreaseIndent();
             }
-            pw.print("Screen On Timebase: ");
-            pw.print(screenOnTime);
-            pw.print(" (");
-            TimeUtils.formatDuration(screenOnTime, pw);
-            pw.println(")");
 
             pw.println();
             pw.println("Settings:");
 
             pw.print("  mAppIdleDurationMillis=");
-            TimeUtils.formatDuration(mAppIdleDurationMillis, pw);
+            TimeUtils.formatDuration(mAppIdleScreenThresholdMillis, pw);
             pw.println();
 
             pw.print("  mAppIdleWallclockThresholdMillis=");
@@ -1057,11 +979,6 @@
             pw.print("mLastAppIdleParoledTime=");
             TimeUtils.formatDuration(mLastAppIdleParoledTime, pw);
             pw.println();
-            pw.print("mScreenOnTime="); TimeUtils.formatDuration(mScreenOnTime, pw);
-            pw.println();
-            pw.print("mLastScreenOnEventRealtime=");
-            TimeUtils.formatDuration(mLastScreenOnEventRealtime, pw);
-            pw.println();
         }
     }
 
@@ -1082,7 +999,7 @@
                     break;
 
                 case MSG_REMOVE_USER:
-                    removeUser(msg.arg1);
+                    onUserRemoved(msg.arg1);
                     break;
 
                 case MSG_INFORM_LISTENERS:
@@ -1179,13 +1096,13 @@
                 }
 
                 // Default: 12 hours of screen-on time sans dream-time
-                mAppIdleDurationMillis = mParser.getLong(KEY_IDLE_DURATION,
+                mAppIdleScreenThresholdMillis = mParser.getLong(KEY_IDLE_DURATION,
                        COMPRESS_TIME ? ONE_MINUTE * 4 : 12 * 60 * ONE_MINUTE);
 
                 mAppIdleWallclockThresholdMillis = mParser.getLong(KEY_WALLCLOCK_THRESHOLD,
                         COMPRESS_TIME ? ONE_MINUTE * 8 : 2L * 24 * 60 * ONE_MINUTE); // 2 days
 
-                mCheckIdleIntervalMillis = Math.min(mAppIdleDurationMillis / 4,
+                mCheckIdleIntervalMillis = Math.min(mAppIdleScreenThresholdMillis / 4,
                         COMPRESS_TIME ? ONE_MINUTE : 8 * 60 * ONE_MINUTE); // 8 hours
 
                 // Default: 24 hours between paroles
@@ -1194,6 +1111,8 @@
 
                 mAppIdleParoleDurationMillis = mParser.getLong(KEY_PAROLE_DURATION,
                         COMPRESS_TIME ? ONE_MINUTE : 10 * ONE_MINUTE); // 10 minutes
+                mAppIdleHistory.setThresholds(mAppIdleWallclockThresholdMillis,
+                        mAppIdleScreenThresholdMillis);
             }
         }
     }
@@ -1284,7 +1203,8 @@
             }
             final long token = Binder.clearCallingIdentity();
             try {
-                return UsageStatsService.this.isAppIdleFilteredOrParoled(packageName, userId, -1);
+                return UsageStatsService.this.isAppIdleFilteredOrParoled(packageName, userId,
+                        SystemClock.elapsedRealtime());
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -1304,11 +1224,9 @@
                     "No permission to change app idle state");
             final long token = Binder.clearCallingIdentity();
             try {
-                PackageInfo pi = AppGlobals.getPackageManager().getPackageInfo(packageName,
-                        PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
-                if (pi == null) return;
+                final int appId = getAppId(packageName);
+                if (appId < 0) return;
                 UsageStatsService.this.setAppIdle(packageName, idle, userId);
-            } catch (RemoteException re) {
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -1335,8 +1253,6 @@
             }
             UsageStatsService.this.dump(args, pw);
         }
-
-
     }
 
     /**
@@ -1411,7 +1327,8 @@
 
         @Override
         public boolean isAppIdle(String packageName, int uidForAppId, int userId) {
-            return UsageStatsService.this.isAppIdleFiltered(packageName, uidForAppId, userId, -1);
+            return UsageStatsService.this.isAppIdleFiltered(packageName, uidForAppId, userId,
+                    SystemClock.elapsedRealtime());
         }
 
         @Override
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
index f2ca3a4..c95ff23 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
@@ -26,7 +26,6 @@
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStats;
 import android.content.res.Configuration;
-import android.text.TextUtils;
 
 import java.io.IOException;
 import java.net.ProtocolException;
@@ -55,13 +54,11 @@
 
     // Time attributes stored as an offset of the beginTime.
     private static final String LAST_TIME_ACTIVE_ATTR = "lastTimeActive";
-    private static final String LAST_TIME_ACTIVE_SYSTEM_ATTR = "lastTimeActiveSystem";
-    private static final String BEGIN_IDLE_TIME_ATTR = "beginIdleTime";
     private static final String END_TIME_ATTR = "endTime";
     private static final String TIME_ATTR = "time";
 
     private static void loadUsageStats(XmlPullParser parser, IntervalStats statsOut)
-            throws XmlPullParserException, IOException {
+            throws IOException {
         final String pkg = parser.getAttributeValue(null, PACKAGE_ATTR);
         if (pkg == null) {
             throw new ProtocolException("no " + PACKAGE_ATTR + " attribute present");
@@ -72,20 +69,6 @@
         // Apply the offset to the beginTime to find the absolute time.
         stats.mLastTimeUsed = statsOut.beginTime + XmlUtils.readLongAttribute(
                 parser, LAST_TIME_ACTIVE_ATTR);
-
-        final String lastTimeUsedSystem = parser.getAttributeValue(null,
-                LAST_TIME_ACTIVE_SYSTEM_ATTR);
-        if (TextUtils.isEmpty(lastTimeUsedSystem)) {
-            // If the field isn't present, use the old one.
-            stats.mLastTimeSystemUsed = stats.mLastTimeUsed;
-        } else {
-            stats.mLastTimeSystemUsed = statsOut.beginTime + Long.parseLong(lastTimeUsedSystem);
-        }
-
-        final String beginIdleTime = parser.getAttributeValue(null, BEGIN_IDLE_TIME_ATTR);
-        if (!TextUtils.isEmpty(beginIdleTime)) {
-            stats.mBeginIdleTime = Long.parseLong(beginIdleTime);
-        }
         stats.mTotalTimeInForeground = XmlUtils.readLongAttribute(parser, TOTAL_TIME_ACTIVE_ATTR);
         stats.mLastEvent = XmlUtils.readIntAttribute(parser, LAST_EVENT_ATTR);
     }
@@ -141,13 +124,10 @@
         // Write the time offset.
         XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
                 usageStats.mLastTimeUsed - stats.beginTime);
-        XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_SYSTEM_ATTR,
-                usageStats.mLastTimeSystemUsed - stats.beginTime);
 
         XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
         XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
         XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
-        XmlUtils.writeLongAttribute(xml, BEGIN_IDLE_TIME_ATTR, usageStats.mBeginIdleTime);
 
         xml.endTag(null, PACKAGE_TAG);
     }
@@ -255,7 +235,6 @@
         }
         xml.endTag(null, PACKAGES_TAG);
 
-
         xml.startTag(null, CONFIGURATIONS_TAG);
         final int configCount = stats.configurations.size();
         for (int i = 0; i < configCount; i++) {
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index f2045d3..7d003f3 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -59,7 +59,6 @@
     private final Context mContext;
     private final UsageStatsDatabase mDatabase;
     private final IntervalStats[] mCurrentStats;
-    private IntervalStats mAppIdleRollingWindow;
     private boolean mStatsChanged = false;
     private final UnixCalendar mDailyExpiryDate;
     private final StatsUpdatedListener mListener;
@@ -74,7 +73,11 @@
     interface StatsUpdatedListener {
         void onStatsUpdated();
         void onStatsReloaded();
-        long getAppIdleRollingWindowDurationMillis();
+        /**
+         * Callback that a system update was detected
+         * @param mUserId user that needs to be initialized
+         */
+        void onNewUpdate(int mUserId);
     }
 
     UserUsageStatsService(Context context, int userId, File usageStatsDir,
@@ -88,7 +91,7 @@
         mUserId = userId;
     }
 
-    void init(final long currentTimeMillis, final long deviceUsageTime) {
+    void init(final long currentTimeMillis) {
         mDatabase.init(currentTimeMillis);
 
         int nullCount = 0;
@@ -112,7 +115,7 @@
 
             // By calling loadActiveStats, we will
             // generate new stats for each bucket.
-            loadActiveStats(currentTimeMillis, /*resetBeginIdleTime=*/ false);
+            loadActiveStats(currentTimeMillis);
         } else {
             // Set up the expiry date to be one day from the latest daily stat.
             // This may actually be today and we will rollover on the first event
@@ -136,54 +139,18 @@
             stat.updateConfigurationStats(null, stat.lastTimeSaved);
         }
 
-        refreshAppIdleRollingWindow(currentTimeMillis, deviceUsageTime);
-
         if (mDatabase.isNewUpdate()) {
-            initializeDefaultsForApps(currentTimeMillis, deviceUsageTime,
-                    mDatabase.isFirstUpdate());
+            notifyNewUpdate();
         }
     }
 
-    /**
-     * If any of the apps don't have a last-used entry, add one now.
-     * @param currentTimeMillis the current time
-     * @param firstUpdate if it is the first update, touch all installed apps, otherwise only
-     *        touch the system apps
-     */
-    private void initializeDefaultsForApps(long currentTimeMillis, long deviceUsageTime,
-            boolean firstUpdate) {
-        PackageManager pm = mContext.getPackageManager();
-        List<PackageInfo> packages = pm.getInstalledPackagesAsUser(0, mUserId);
-        final int packageCount = packages.size();
-        for (int i = 0; i < packageCount; i++) {
-            final PackageInfo pi = packages.get(i);
-            String packageName = pi.packageName;
-            if (pi.applicationInfo != null && (firstUpdate || pi.applicationInfo.isSystemApp())
-                    && getBeginIdleTime(packageName) == -1) {
-                for (IntervalStats stats : mCurrentStats) {
-                    stats.update(packageName, currentTimeMillis, Event.SYSTEM_INTERACTION);
-                    stats.updateBeginIdleTime(packageName, deviceUsageTime);
-                }
-
-                mAppIdleRollingWindow.update(packageName, currentTimeMillis,
-                        Event.SYSTEM_INTERACTION);
-                mAppIdleRollingWindow.updateBeginIdleTime(packageName, deviceUsageTime);
-                mStatsChanged = true;
-            }
-        }
-        // Persist the new OTA-related access stats.
-        persistActiveStats();
-    }
-
-    void onTimeChanged(long oldTime, long newTime, long deviceUsageTime,
-                       boolean resetBeginIdleTime) {
+    void onTimeChanged(long oldTime, long newTime) {
         persistActiveStats();
         mDatabase.onTimeChanged(newTime - oldTime);
-        loadActiveStats(newTime, resetBeginIdleTime);
-        refreshAppIdleRollingWindow(newTime, deviceUsageTime);
+        loadActiveStats(newTime);
     }
 
-    void reportEvent(UsageEvents.Event event, long deviceUsageTime) {
+    void reportEvent(UsageEvents.Event event) {
         if (DEBUG) {
             Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
                     + "[" + event.mTimeStamp + "]: "
@@ -192,7 +159,7 @@
 
         if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) {
             // Need to rollover
-            rolloverStats(event.mTimeStamp, deviceUsageTime);
+            rolloverStats(event.mTimeStamp);
         }
 
         final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
@@ -218,35 +185,9 @@
                 stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
             } else {
                 stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
-                stats.updateBeginIdleTime(event.mPackage, deviceUsageTime);
             }
         }
 
-        if (event.mEventType != Event.CONFIGURATION_CHANGE) {
-            mAppIdleRollingWindow.update(event.mPackage, event.mTimeStamp, event.mEventType);
-            mAppIdleRollingWindow.updateBeginIdleTime(event.mPackage, deviceUsageTime);
-        }
-
-        notifyStatsChanged();
-    }
-
-    /**
-     * Sets the beginIdleTime for each of the intervals.
-     * @param beginIdleTime
-     */
-    void setBeginIdleTime(String packageName, long beginIdleTime) {
-        for (IntervalStats stats : mCurrentStats) {
-            stats.updateBeginIdleTime(packageName, beginIdleTime);
-        }
-        mAppIdleRollingWindow.updateBeginIdleTime(packageName, beginIdleTime);
-        notifyStatsChanged();
-    }
-
-    void setSystemLastUsedTime(String packageName, long lastUsedTime) {
-        for (IntervalStats stats : mCurrentStats) {
-            stats.updateSystemLastUsedTime(packageName, lastUsedTime);
-        }
-        mAppIdleRollingWindow.updateSystemLastUsedTime(packageName, lastUsedTime);
         notifyStatsChanged();
     }
 
@@ -404,24 +345,6 @@
         return new UsageEvents(results, table);
     }
 
-    long getBeginIdleTime(String packageName) {
-        UsageStats packageUsage;
-        if ((packageUsage = mAppIdleRollingWindow.packageStats.get(packageName)) == null) {
-            return -1;
-        } else {
-            return packageUsage.getBeginIdleTime();
-        }
-    }
-
-    long getSystemLastUsedTime(String packageName) {
-        UsageStats packageUsage;
-        if ((packageUsage = mAppIdleRollingWindow.packageStats.get(packageName)) == null) {
-            return -1;
-        } else {
-            return packageUsage.getLastTimeSystemUsed();
-        }
-    }
-
     void persistActiveStats() {
         if (mStatsChanged) {
             Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
@@ -436,7 +359,7 @@
         }
     }
 
-    private void rolloverStats(final long currentTimeMillis, final long deviceUsageTime) {
+    private void rolloverStats(final long currentTimeMillis) {
         final long startTime = SystemClock.elapsedRealtime();
         Slog.i(TAG, mLogPrefix + "Rolling over usage stats");
 
@@ -463,7 +386,7 @@
 
         persistActiveStats();
         mDatabase.prune(currentTimeMillis);
-        loadActiveStats(currentTimeMillis, /*resetBeginIdleTime=*/ false);
+        loadActiveStats(currentTimeMillis);
 
         final int continueCount = continuePreviousDay.size();
         for (int i = 0; i < continueCount; i++) {
@@ -477,8 +400,6 @@
         }
         persistActiveStats();
 
-        refreshAppIdleRollingWindow(currentTimeMillis, deviceUsageTime);
-
         final long totalTime = SystemClock.elapsedRealtime() - startTime;
         Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
                 + " milliseconds");
@@ -491,7 +412,11 @@
         }
     }
 
-    private void loadActiveStats(final long currentTimeMillis, boolean resetBeginIdleTime) {
+    private void notifyNewUpdate() {
+        mListener.onNewUpdate(mUserId);
+    }
+
+    private void loadActiveStats(final long currentTimeMillis) {
         for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
             final IntervalStats stats = mDatabase.getLatestUsageStats(intervalType);
             if (stats != null && currentTimeMillis - 500 >= stats.endTime &&
@@ -514,12 +439,6 @@
                 mCurrentStats[intervalType].beginTime = currentTimeMillis;
                 mCurrentStats[intervalType].endTime = currentTimeMillis + 1;
             }
-
-            if (resetBeginIdleTime) {
-                for (UsageStats usageStats : mCurrentStats[intervalType].packageStats.values()) {
-                    usageStats.mBeginIdleTime = 0;
-                }
-            }
         }
 
         mStatsChanged = false;
@@ -538,96 +457,28 @@
                 mDailyExpiryDate.getTimeInMillis() + ")");
     }
 
-    private static void mergePackageStats(IntervalStats dst, IntervalStats src,
-                                          final long deviceUsageTime) {
-        dst.endTime = Math.max(dst.endTime, src.endTime);
-
-        final int srcPackageCount = src.packageStats.size();
-        for (int i = 0; i < srcPackageCount; i++) {
-            final String packageName = src.packageStats.keyAt(i);
-            final UsageStats srcStats = src.packageStats.valueAt(i);
-            UsageStats dstStats = dst.packageStats.get(packageName);
-            if (dstStats == null) {
-                dstStats = new UsageStats(srcStats);
-                dst.packageStats.put(packageName, dstStats);
-            } else {
-                dstStats.add(src.packageStats.valueAt(i));
-            }
-
-            // App idle times can not begin in the future. This happens if we had a time change.
-            if (dstStats.mBeginIdleTime > deviceUsageTime) {
-                dstStats.mBeginIdleTime = deviceUsageTime;
-            }
-        }
-    }
-
-    /**
-     * App idle operates on a rolling window of time. When we roll over time, we end up with a
-     * period of time where in-memory stats are empty and we don't hit the disk for older stats
-     * for performance reasons. Suddenly all apps will become idle.
-     *
-     * Instead, at times we do a deep query to find all the apps that have run in the past few
-     * days and keep the cached data up to date.
-     *
-     * @param currentTimeMillis
-     */
-    void refreshAppIdleRollingWindow(final long currentTimeMillis, final long deviceUsageTime) {
-        // Start the rolling window for AppIdle requests.
-        final long startRangeMillis = currentTimeMillis -
-                mListener.getAppIdleRollingWindowDurationMillis();
-
-        List<IntervalStats> stats = mDatabase.queryUsageStats(UsageStatsManager.INTERVAL_DAILY,
-                startRangeMillis, currentTimeMillis, new StatCombiner<IntervalStats>() {
-                    @Override
-                    public void combine(IntervalStats stats, boolean mutable,
-                                        List<IntervalStats> accumulatedResult) {
-                        IntervalStats accum;
-                        if (accumulatedResult.isEmpty()) {
-                            accum = new IntervalStats();
-                            accum.beginTime = stats.beginTime;
-                            accumulatedResult.add(accum);
-                        } else {
-                            accum = accumulatedResult.get(0);
-                        }
-
-                        mergePackageStats(accum, stats, deviceUsageTime);
-                    }
-                });
-
-        if (stats == null || stats.isEmpty()) {
-            mAppIdleRollingWindow = new IntervalStats();
-            mergePackageStats(mAppIdleRollingWindow,
-                    mCurrentStats[UsageStatsManager.INTERVAL_YEARLY], deviceUsageTime);
-        } else {
-            mAppIdleRollingWindow = stats.get(0);
-        }
-    }
-
     //
     // -- DUMP related methods --
     //
 
-    void checkin(final IndentingPrintWriter pw, final long screenOnTime) {
+    void checkin(final IndentingPrintWriter pw) {
         mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() {
             @Override
             public boolean checkin(IntervalStats stats) {
-                printIntervalStats(pw, stats, screenOnTime, false);
+                printIntervalStats(pw, stats, false);
                 return true;
             }
         });
     }
 
-    void dump(IndentingPrintWriter pw, final long screenOnTime) {
+    void dump(IndentingPrintWriter pw) {
         // This is not a check-in, only dump in-memory stats.
         for (int interval = 0; interval < mCurrentStats.length; interval++) {
             pw.print("In-memory ");
             pw.print(intervalToString(interval));
             pw.println(" stats");
-            printIntervalStats(pw, mCurrentStats[interval], screenOnTime, true);
+            printIntervalStats(pw, mCurrentStats[interval], true);
         }
-
-        pw.println("AppIdleRollingWindow cache");
-        printIntervalStats(pw, mAppIdleRollingWindow, screenOnTime, true);
     }
 
     private String formatDateTime(long dateTime, boolean pretty) {
@@ -644,7 +495,7 @@
         return Long.toString(elapsedTime);
     }
 
-    void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats, long screenOnTime,
+    void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats,
             boolean prettyDates) {
         if (prettyDates) {
             pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext,
@@ -665,10 +516,6 @@
             pw.printPair("totalTime",
                     formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates));
             pw.printPair("lastTime", formatDateTime(usageStats.mLastTimeUsed, prettyDates));
-            pw.printPair("lastTimeSystem",
-                    formatDateTime(usageStats.mLastTimeSystemUsed, prettyDates));
-            pw.printPair("inactiveTime",
-                    formatElapsedTime(screenOnTime - usageStats.mBeginIdleTime, prettyDates));
             pw.println();
         }
         pw.decreaseIndent();
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 4368b81..3ad7d34 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -591,6 +591,14 @@
     @SystemApi
     public static final String KEY_USE_RCS_PRESENCE_BOOL = "use_rcs_presence_bool";
 
+    /**
+     * The duration in seconds that platform call and message blocking is disabled after the user
+     * contacts emergency services. Platform considers values in the range 0 to 604800 (one week) as
+     * valid. See {@link android.provider.BlockedNumberContract#isBlocked(Context, String)}).
+     */
+    public static final String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT =
+            "duration_blocking_disabled_after_emergency_int";
+
     /** The default value for every variable. */
     private final static PersistableBundle sDefaults;
 
@@ -660,6 +668,7 @@
                 "max_retries=3, 5000, 5000, 5000");
         sDefaults.putLong(KEY_CARRIER_DATA_CALL_APN_DELAY_DEFAULT_LONG, 20000);
         sDefaults.putLong(KEY_CARRIER_DATA_CALL_APN_DELAY_FASTER_LONG, 3000);
+        sDefaults.putInt(KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT, 7200);
 
         sDefaults.putStringArray(KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY, null);
         sDefaults.putStringArray(KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY, null);
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index d1d6e0d..6229ed9 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -25,6 +25,7 @@
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.Typeface;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.DisplayMetrics;
@@ -338,7 +339,7 @@
     public static String givePrintableIccid(String iccId) {
         String iccIdToPrint = null;
         if (iccId != null) {
-            if (iccId.length() > 9) {
+            if (iccId.length() > 9 && !Build.IS_DEBUGGABLE) {
                 iccIdToPrint = iccId.substring(0, 9) + "XXXXXXXXXXX";
             } else {
                 iccIdToPrint = iccId;
diff --git a/telephony/java/com/android/ims/ImsCallForwardInfo.java b/telephony/java/com/android/ims/ImsCallForwardInfo.java
index 3f8fd19..eeee0fc 100644
--- a/telephony/java/com/android/ims/ImsCallForwardInfo.java
+++ b/telephony/java/com/android/ims/ImsCallForwardInfo.java
@@ -31,6 +31,8 @@
     public int mStatus;
     // 0x91: International, 0x81: Unknown
     public int mToA;
+    // Service class
+    public int mServiceClass;
     // Number (it will not include the "sip" or "tel" URI scheme)
     public String mNumber;
     // No reply timer for CF
@@ -55,13 +57,16 @@
         out.writeInt(mToA);
         out.writeString(mNumber);
         out.writeInt(mTimeSeconds);
+        out.writeInt(mServiceClass);
     }
 
     @Override
     public String toString() {
         return super.toString() + ", Condition: " + mCondition
             + ", Status: " + ((mStatus == 0) ? "disabled" : "enabled")
-            + ", ToA: " + mToA + ", Number=" + mNumber
+            + ", ToA: " + mToA
+            + ", Service Class: " + mServiceClass
+            + ", Number=" + mNumber
             + ", Time (seconds): " + mTimeSeconds;
     }
 
@@ -71,6 +76,7 @@
         mToA = in.readInt();
         mNumber = in.readString();
         mTimeSeconds = in.readInt();
+        mServiceClass = in.readInt();
     }
 
     public static final Creator<ImsCallForwardInfo> CREATOR =
diff --git a/telephony/java/com/android/ims/ImsReasonInfo.java b/telephony/java/com/android/ims/ImsReasonInfo.java
index 2769a2b..c909c6d 100644
--- a/telephony/java/com/android/ims/ImsReasonInfo.java
+++ b/telephony/java/com/android/ims/ImsReasonInfo.java
@@ -84,6 +84,8 @@
     public static final int CODE_LOCAL_CALL_VOLTE_RETRY_REQUIRED = 147;
     // IMS call is already terminated (in TERMINATED state)
     public static final int CODE_LOCAL_CALL_TERMINATED = 148;
+    // Handover not feasible
+    public static final int CODE_LOCAL_HO_NOT_FEASIBLE = 149;
 
     /**
      * TIMEOUT (IMS -> Telephony)
@@ -153,6 +155,9 @@
     public static final int CODE_SIP_USER_REJECTED = 361;
     // Others
     public static final int CODE_SIP_GLOBAL_ERROR = 362;
+    // Emergency failure
+    public static final int CODE_EMERGENCY_TEMP_FAILURE = 363;
+    public static final int CODE_EMERGENCY_PERM_FAILURE = 364;
 
     /**
      * MEDIA (IMS -> Telephony)
@@ -236,6 +241,14 @@
     public static final int CODE_ANSWERED_ELSEWHERE = 1014;
 
     /**
+     * Supplementary services (HOLD/RESUME) failure error codes.
+     * Values for Supplemetary services failure - Failed, Cancelled and Re-Invite collision.
+     */
+    public static final int CODE_SUPP_SVC_FAILED = 1201;
+    public static final int CODE_SUPP_SVC_CANCELLED = 1202;
+    public static final int CODE_SUPP_SVC_REINVITE_COLLISION = 1203;
+
+    /**
      * Network string error messages.
      * mExtraMessage may have these values.
      */
diff --git a/telephony/java/com/android/ims/ImsStreamMediaProfile.java b/telephony/java/com/android/ims/ImsStreamMediaProfile.java
index 5a99212..216cef5 100644
--- a/telephony/java/com/android/ims/ImsStreamMediaProfile.java
+++ b/telephony/java/com/android/ims/ImsStreamMediaProfile.java
@@ -51,6 +51,16 @@
     public static final int AUDIO_QUALITY_GSM_EFR = 8;
     public static final int AUDIO_QUALITY_GSM_FR = 9;
     public static final int AUDIO_QUALITY_GSM_HR = 10;
+    public static final int AUDIO_QUALITY_G711U = 11;
+    public static final int AUDIO_QUALITY_G723 = 12;
+    public static final int AUDIO_QUALITY_G711A = 13;
+    public static final int AUDIO_QUALITY_G722 = 14;
+    public static final int AUDIO_QUALITY_G711AB = 15;
+    public static final int AUDIO_QUALITY_G729 = 16;
+    public static final int AUDIO_QUALITY_EVS_NB = 17;
+    public static final int AUDIO_QUALITY_EVS_WB = 18;
+    public static final int AUDIO_QUALITY_EVS_SWB = 19;
+    public static final int AUDIO_QUALITY_EVS_FB = 20;
 
    /**
      * Video information
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index b028ce6..de7b9c2 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -356,6 +356,15 @@
         </activity>
 
         <activity
+                android:name="MovingSurfaceViewActivity"
+                android:label="SurfaceView/Animated Movement">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.android.test.hwui.TEST" />
+            </intent-filter>
+        </activity>
+
+        <activity
                 android:name="GLTextureViewActivity"
                 android:label="TextureView/OpenGL">
             <intent-filter>
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/MovingSurfaceViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/MovingSurfaceViewActivity.java
new file mode 100644
index 0000000..cd15ef1
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/MovingSurfaceViewActivity.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 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.test.hwui;
+
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SurfaceHolder;
+import android.view.SurfaceHolder.Callback;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
+
+public class MovingSurfaceViewActivity extends Activity implements Callback {
+    static final String TAG = "MovingSurfaceView";
+    SurfaceView mSurfaceView;
+    ObjectAnimator mAnimator;
+
+    class MySurfaceView extends SurfaceView {
+        boolean mSlowToggled;
+
+        public MySurfaceView(Context context) {
+            super(context);
+            setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mSlowToggled = !mSlowToggled;
+                    Log.d(TAG, "SLOW MODE: " + mSlowToggled);
+                    invalidate();
+                }
+            });
+            setWillNotDraw(false);
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            super.draw(canvas);
+            if (mSlowToggled) {
+                try {
+                    Thread.sleep(16);
+                } catch (InterruptedException e) {}
+            }
+        }
+
+        public void setMyTranslationY(float ty) {
+            setTranslationY(ty);
+            if (mSlowToggled) {
+                invalidate();
+            }
+        }
+
+        public float getMyTranslationY() {
+            return getTranslationY();
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        FrameLayout content = new FrameLayout(this);
+
+        mSurfaceView = new MySurfaceView(this);
+        mSurfaceView.getHolder().addCallback(this);
+
+        final float density = getResources().getDisplayMetrics().density;
+        int size = (int) (200 * density);
+
+        content.addView(mSurfaceView, new FrameLayout.LayoutParams(
+                size, size, Gravity.CENTER));
+        mAnimator = ObjectAnimator.ofFloat(mSurfaceView, "myTranslationY",
+                0, size);
+        mAnimator.setRepeatMode(ObjectAnimator.REVERSE);
+        mAnimator.setRepeatCount(ObjectAnimator.INFINITE);
+        mAnimator.setDuration(200);
+        mAnimator.setInterpolator(new LinearInterpolator());
+        setContentView(content);
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        Canvas canvas = holder.lockCanvas();
+        canvas.drawARGB(0xFF, 0x00, 0xFF, 0x00);
+        holder.unlockCanvasAndPost(canvas);
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mAnimator.start();
+    }
+
+    @Override
+    protected void onPause() {
+        mAnimator.pause();
+        super.onPause();
+    }
+}
diff --git a/wifi/java/android/net/wifi/nan/IWifiNanEventListener.aidl b/wifi/java/android/net/wifi/nan/IWifiNanEventListener.aidl
index 13efc36..fa666af 100644
--- a/wifi/java/android/net/wifi/nan/IWifiNanEventListener.aidl
+++ b/wifi/java/android/net/wifi/nan/IWifiNanEventListener.aidl
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2016, The Android Open Source Project
+/*
+ * Copyright (C) 2016 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
+ *      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,
@@ -26,7 +26,7 @@
 oneway interface IWifiNanEventListener
 {
     void onConfigCompleted(in ConfigRequest completedConfig);
-    void onConfigFailed(int reason);
+    void onConfigFailed(in ConfigRequest failedConfig, int reason);
     void onNanDown(int reason);
     void onIdentityChanged();
 }
diff --git a/wifi/java/android/net/wifi/nan/IWifiNanManager.aidl b/wifi/java/android/net/wifi/nan/IWifiNanManager.aidl
index ec9e462..f382d97 100644
--- a/wifi/java/android/net/wifi/nan/IWifiNanManager.aidl
+++ b/wifi/java/android/net/wifi/nan/IWifiNanManager.aidl
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2016, The Android Open Source Project
+/*
+ * Copyright (C) 2016 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
+ *      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,
diff --git a/wifi/java/android/net/wifi/nan/IWifiNanSessionListener.aidl b/wifi/java/android/net/wifi/nan/IWifiNanSessionListener.aidl
index 50c34d9..d60d8ca 100644
--- a/wifi/java/android/net/wifi/nan/IWifiNanSessionListener.aidl
+++ b/wifi/java/android/net/wifi/nan/IWifiNanSessionListener.aidl
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2016, The Android Open Source Project
+/*
+ * Copyright (C) 2016 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
+ *      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,
diff --git a/wifi/java/android/net/wifi/nan/WifiNanEventListener.java b/wifi/java/android/net/wifi/nan/WifiNanEventListener.java
index eae0a55..5c18bd7 100644
--- a/wifi/java/android/net/wifi/nan/WifiNanEventListener.java
+++ b/wifi/java/android/net/wifi/nan/WifiNanEventListener.java
@@ -47,7 +47,8 @@
 
     /**
      * Configuration failed callback event registration flag. Corresponding
-     * callback is {@link WifiNanEventListener#onConfigFailed(int)}.
+     * callback is
+     * {@link WifiNanEventListener#onConfigFailed(ConfigRequest, int)}.
      */
     public static final int LISTEN_CONFIG_FAILED = 0x1 << 1;
 
@@ -93,7 +94,7 @@
                         WifiNanEventListener.this.onConfigCompleted((ConfigRequest) msg.obj);
                         break;
                     case LISTEN_CONFIG_FAILED:
-                        WifiNanEventListener.this.onConfigFailed(msg.arg1);
+                        WifiNanEventListener.this.onConfigFailed((ConfigRequest) msg.obj, msg.arg1);
                         break;
                     case LISTEN_NAN_DOWN:
                         WifiNanEventListener.this.onNanDown(msg.arg1);
@@ -129,7 +130,7 @@
      *
      * @param reason Failure reason code, see {@code NanSessionListener.FAIL_*}.
      */
-    public void onConfigFailed(int reason) {
+    public void onConfigFailed(ConfigRequest failedConfig, int reason) {
         Log.w(TAG, "onConfigFailed: called in stub - override if interested or disable");
     }
 
@@ -173,11 +174,14 @@
         }
 
         @Override
-        public void onConfigFailed(int reason) {
-            if (VDBG) Log.v(TAG, "onConfigFailed: reason=" + reason);
+        public void onConfigFailed(ConfigRequest failedConfig, int reason) {
+            if (VDBG) {
+                Log.v(TAG, "onConfigFailed: failedConfig=" + failedConfig + ", reason=" + reason);
+            }
 
             Message msg = mHandler.obtainMessage(LISTEN_CONFIG_FAILED);
             msg.arg1 = reason;
+            msg.obj = failedConfig;
             mHandler.sendMessage(msg);
         }
 
diff --git a/wifi/java/android/net/wifi/nan/WifiNanSessionListener.java b/wifi/java/android/net/wifi/nan/WifiNanSessionListener.java
index d5e59f0..0925087 100644
--- a/wifi/java/android/net/wifi/nan/WifiNanSessionListener.java
+++ b/wifi/java/android/net/wifi/nan/WifiNanSessionListener.java
@@ -303,8 +303,8 @@
      * message). Override to implement your custom response.
      * <p>
      * Note that either this callback or
-     * {@link WifiNanSessionListener#onMessageSendFail(int)} will be received -
-     * never both.
+     * {@link WifiNanSessionListener#onMessageSendFail(int, int)} will be
+     * received - never both.
      */
     public void onMessageSendSuccess(int messageId) {
         if (VDBG) Log.v(TAG, "onMessageSendSuccess: called in stub - override if interested");
@@ -319,8 +319,8 @@
      * message). Override to implement your custom response.
      * <p>
      * Note that either this callback or
-     * {@link WifiNanSessionListener#onMessageSendSuccess()} will be received -
-     * never both
+     * {@link WifiNanSessionListener#onMessageSendSuccess(int)} will be received
+     * - never both
      *
      * @param reason The failure reason using {@code NanSessionListener.FAIL_*}
      *            codes.