Merge pi-dev-plus-aosp-without-vendor into stage-aosp-master

Bug: 79597307
Change-Id: I2bce0d629d5b551a5d562cd7691b54313216f5f9
diff --git a/Android.mk b/Android.mk
index c305684..f371660 100644
--- a/Android.mk
+++ b/Android.mk
@@ -21,6 +21,8 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
 LOCAL_CERTIFICATE := platform
 
 LOCAL_PACKAGE_NAME := SystemUpdater
@@ -28,4 +30,15 @@
 
 LOCAL_DEX_PREOPT := false
 
+LOCAL_USE_AAPT2 := true
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_DX_FLAGS := --multi-dex
+
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+    androidx.appcompat_appcompat \
+    androidx.car_car \
+    androidx.legacy_legacy-support-v4
+
 include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index fcb00fc..1a77c05 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -15,38 +15,37 @@
   limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.car.systemupdater" >
-    <uses-sdk android:minSdkVersion="21" />
+<manifest
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.car.systemupdater">
+
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
-    <uses-permission android:name="android.permission.DELETE_CACHE_FILES" />
     <uses-permission android:name="android.permission.REBOOT" />
-    <uses-permission android:name="android.permission.RECOVERY" />
-    <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
     <uses-feature android:name="android.hardware.usb.host" />
-    <uses-feature
-        android:glEsVersion="0x00020000"
-        android:required="true"/>
 
     <application
-        android:label="@string/app_name"
-        android:icon="@drawable/ic_launcher">
+            android:label="@string/title"
+            android:theme="@style/SystemUpdaterTheme">
         <activity
             android:name="com.android.car.systemupdater.SystemUpdaterActivity"
-            android:label="@string/app_name"
-            android:icon="@drawable/ic_launcher" >
+            android:label="@string/title">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
             <intent-filter>
                 <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="com.android.settings.action.EXTRA_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.title"
+                       android:resource="@string/title" />
+            <meta-data android:name="com.android.settings.icon"
+                       android:resource="@drawable/ic_system_update_alt_black_48dp" />
+            <meta-data android:name="com.android.settings.category"
+                       android:value="com.android.settings.category.system" />
         </activity>
     </application>
-
 </manifest>
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..38f9800
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,7 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+
+[Builtin Hooks]
+commit_msg_changeid_field = true
+commit_msg_test_field = true
diff --git a/res/anim/trans_fade_in.xml b/res/anim/trans_fade_in.xml
new file mode 100644
index 0000000..4f80a18
--- /dev/null
+++ b/res/anim/trans_fade_in.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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">
+
+    <alpha
+        android:fromAlpha="0.2"
+        android:toAlpha="1"
+        android:duration="@android:integer/config_mediumAnimTime" />
+</set>
diff --git a/res/anim/trans_fade_out.xml b/res/anim/trans_fade_out.xml
new file mode 100644
index 0000000..db7808a
--- /dev/null
+++ b/res/anim/trans_fade_out.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.2"
+        android:duration="@android:integer/config_mediumAnimTime" />
+</set>
diff --git a/res/color/action_bar_btn.xml b/res/color/action_bar_btn.xml
new file mode 100644
index 0000000..29d9fa2
--- /dev/null
+++ b/res/color/action_bar_btn.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="true"
+          android:color="@color/car_accent" />
+    <item android:state_enabled="false"
+          android:alpha="0.5"
+          android:color="@color/car_accent" />
+</selector>
diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index 96a442e..0000000
--- a/res/drawable-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index 359047d..0000000
--- a/res/drawable-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index 71c6d76..0000000
--- a/res/drawable-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_launcher.png b/res/drawable-xxhdpi/ic_launcher.png
deleted file mode 100644
index 4df1894..0000000
--- a/res/drawable-xxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/button_ripple_bg.xml b/res/drawable/button_ripple_bg.xml
new file mode 100644
index 0000000..a28e38c
--- /dev/null
+++ b/res/drawable/button_ripple_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2018 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.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+  android:color="@color/car_card_ripple_background"
+        android:radius="@dimen/toggle_ripple">
+    <item android:id="@android:id/mask"
+          android:drawable="@drawable/rectangle_ripple_mask" />
+</ripple>
diff --git a/res/drawable/ic_arrow_back.xml b/res/drawable/ic_arrow_back.xml
new file mode 100644
index 0000000..81da87f
--- /dev/null
+++ b/res/drawable/ic_arrow_back.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ Copyright (C) 2018 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
+  -->
+
+<!-- This Icon is used in as the back icon on ActionBar. ActionBar hard code the icon layout and
+  ~  does not provide a way to customize it. Here to center the icon in action bar, we make up
+  ~  the margin by add the extra space in the icon itself -->
+<vector
+    android:height="@dimen/car_primary_icon_size"
+    android:width="@dimen/car_primary_icon_size"
+    android:tint="@color/car_accent"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
+</vector>
diff --git a/res/drawable/ic_system_update_alt_black_48dp.xml b/res/drawable/ic_system_update_alt_black_48dp.xml
new file mode 100644
index 0000000..a66591f
--- /dev/null
+++ b/res/drawable/ic_system_update_alt_black_48dp.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2018 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="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M24,32.5l8,-8h-6v-18h-4v18h-6l8,8zM42,6.5L30,6.5v3.97h12v28.06L6,38.53L6,10.47h12L18,6.5L6,6.5c-2.21,0 -4,1.79 -4,4v28c0,2.21 1.79,4 4,4h36c2.21,0 4,-1.79 4,-4v-28c0,-2.21 -1.79,-4 -4,-4z"
+        android:fillColor="#000000"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/rectangle_ripple_mask.xml b/res/drawable/rectangle_ripple_mask.xml
new file mode 100644
index 0000000..69eaf8b
--- /dev/null
+++ b/res/drawable/rectangle_ripple_mask.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2018 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+  <corners android:radius="@dimen/car_radius_1" />
+  <solid android:color="@android:color/white" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml b/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
new file mode 100644
index 0000000..e73b470
--- /dev/null
+++ b/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<!-- Variant of vector_drawable_progress_indeterminate_horizontal in frameworks/base/core/res, which
+     draws the whole height of the progress bar instead having blank space above and below the
+     bar. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="10dp"
+    android:width="360dp"
+    android:viewportHeight="10"
+    android:viewportWidth="360" >
+    <group
+        android:name="progress_group"
+        android:translateX="180"
+        android:translateY="5" >
+        <path
+            android:name="background_track"
+            android:pathData="M -180.0,-5.0 l 360.0,0 l 0,10.0 l -360.0,0 Z"
+            android:fillColor="?android:attr/colorControlActivated"
+            android:fillAlpha="?android:attr/disabledAlpha"/>
+        <group
+            android:name="rect2_grp"
+            android:translateX="-197.60001"
+            android:scaleX="0.1" >
+            <path
+                android:name="rect2"
+                android:pathData="M -144.0,-5.0 l 288.0,0 l 0,10.0 l -288.0,0 Z"
+                android:fillColor="?android:attr/colorControlActivated" />
+        </group>
+        <group
+            android:name="rect1_grp"
+            android:translateX="-522.59998"
+            android:scaleX="0.1" >
+            <path
+                android:name="rect1"
+                android:pathData="M -144.0,-5.0 l 288.0,0 l 0,10.0 l -288.0,0 Z"
+                android:fillColor="?android:attr/colorControlActivated" />
+        </group>
+    </group>
+</vector>
diff --git a/res/layout/action_bar_with_button.xml b/res/layout/action_bar_with_button.xml
new file mode 100644
index 0000000..5a824ab
--- /dev/null
+++ b/res/layout/action_bar_with_button.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/car_app_bar_height">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginEnd="@dimen/car_keyline_1"
+        android:gravity="center_vertical"
+        android:textAppearance="@style/TextAppearance.Car.Title2"
+        android:text="@string/title"/>
+
+    <FrameLayout
+        android:id="@+id/button_container"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_alignParentEnd="true"
+        android:layout_marginEnd="@dimen/car_keyline_1"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/action_button1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            style="@style/SystemUpdate.ActionBar.Button.Borderless.Colored"
+            android:layout_marginEnd="@dimen/action_bar_end_widget_margin_end" />
+    </FrameLayout>
+
+    <ProgressBar
+        android:id="@+id/progress_bar"
+        style="@style/Widget.Car.ProgressBar.Horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:visibility="gone"
+        android:indeterminate="true"/>
+</RelativeLayout>
diff --git a/res/layout/activity_main.xml b/res/layout/activity_main.xml
index b077f52..4d58853 100644
--- a/res/layout/activity_main.xml
+++ b/res/layout/activity_main.xml
@@ -14,17 +14,25 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:paddingLeft="@dimen/activity_horizontal_margin"
-    android:paddingRight="@dimen/activity_horizontal_margin"
-    android:paddingTop="@dimen/activity_vertical_margin"
-    android:paddingBottom="@dimen/activity_vertical_margin"
-    tools:context=".MainActivity">
-    <FrameLayout
-        android:id="@+id/device_container"
+    android:orientation="vertical">
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/toolbar"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-</RelativeLayout>
+        android:layout_height="@dimen/car_app_bar_height"
+        android:theme="@style/ActionBarStyle.Car"
+        app:contentInsetStart="0dp"
+        app:contentInsetEnd="0dp"/>
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_list_divider_height"
+        android:background="@color/car_list_divider"/>
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@+id/device_container"/>
+</LinearLayout>
diff --git a/res/layout/folder_entry.xml b/res/layout/folder_entry.xml
deleted file mode 100644
index 0fec3c0..0000000
--- a/res/layout/folder_entry.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?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.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="horizontal"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <TextView
-        android:id="@+id/text"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="5dp"
-        android:layout_marginRight="20dp"
-        android:gravity="center"
-        android:textSize="32sp">
-    </TextView>
-    <TextView
-        android:id="@+id/description"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="5dp"
-        android:gravity="center"
-        android:textSize="32sp">
-    </TextView>
-</LinearLayout>
diff --git a/res/layout/folder_list.xml b/res/layout/folder_list.xml
index cb651a3..1265653 100644
--- a/res/layout/folder_list.xml
+++ b/res/layout/folder_list.xml
@@ -14,35 +14,25 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:orientation="vertical"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" >
-        <Button
-            android:id="@+id/back"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/back"
-            android:layout_gravity="left"/>
+    android:layout_height="wrap_content">
 
-        <TextView
-            android:id="@+id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="48dp"
-            android:layout_gravity="center" />
-    </FrameLayout>
-    <ListView
+    <TextView
+        android:id="@+id/current_path"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:textAppearance="@style/TextAppearance.Car.Body2"
+        android:layout_marginStart="@dimen/car_gutter_size"
+        android:layout_marginEnd="@dimen/car_margin"/>
+
+    <androidx.car.widget.PagedListView
         android:id="@+id/folder_list"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_margin="10dp"
-        android:gravity="center"
-        android:numColumns="auto_fit"
-        android:verticalSpacing="10dp"
-        android:drawSelectorOnTop="true"
-        android:stretchMode="columnWidth" />
+        android:layout_height="match_parent"
+        app:showPagedListViewDivider="true"
+        app:gutter="start"/>
 </LinearLayout>
diff --git a/res/layout/system_update_auto_content.xml b/res/layout/system_update_auto_content.xml
new file mode 100644
index 0000000..6f5ee5c
--- /dev/null
+++ b/res/layout/system_update_auto_content.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginStart="@dimen/car_margin"
+    android:layout_marginEnd="@dimen/car_margin">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="@dimen/car_padding_3"
+        android:paddingBottom="@dimen/car_padding_5"
+        android:orientation="vertical">
+
+      <TextView
+          android:textAppearance="@style/TextAppearance.Car.Body1"
+          android:id="@+id/system_update_auto_content_title"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_marginBottom="@dimen/car_padding_1"/>
+
+      <TextView
+          android:textAppearance="@style/TextAppearance.Car.Body2"
+          android:id="@+id/system_update_auto_content_info"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_marginBottom="@dimen/car_padding_5"/>
+
+      <TextView
+          android:textAppearance="@style/TextAppearance.Car.Body2"
+          android:id="@+id/system_update_auto_content_details"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:maxLines="5"
+          android:ellipsize="end"/>
+
+    </LinearLayout>
+</FrameLayout>
diff --git a/res/values-v21/styles.xml b/res/values-night/colors.xml
similarity index 66%
rename from res/values-v21/styles.xml
rename to res/values-night/colors.xml
index 1dd3c66..9d4fc43 100644
--- a/res/values-v21/styles.xml
+++ b/res/values-night/colors.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright (c) 2016, The Android Open Source Project
+  Copyright (c) 2018, 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.
@@ -15,6 +15,9 @@
   limitations under the License.
 -->
 <resources>
-    <style name="AppTheme" parent="android:Theme.Material.Light">
-    </style>
-</resources>
+    <!-- The default color for all activities in the SetupWizard. -->
+    <color name="windowBackground">@color/car_dark_blue_grey_700</color>
+
+    <!-- The color of the status bar. -->
+    <color name="colorPrimaryDark">@color/car_dark_blue_grey_800</color>
+</resources>
\ No newline at end of file
diff --git a/res/values-w820dp/dimens.xml b/res/values-w820dp/dimens.xml
deleted file mode 100644
index 0ef6ab4..0000000
--- a/res/values-w820dp/dimens.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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>
-    <!-- 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/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..5ca113b
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2018 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>
+    <!-- The default color for all activities in the Settings. -->
+    <color name="windowBackground">@color/car_grey_50</color>
+
+    <!-- The color of the status bar. -->
+    <color name="colorPrimaryDark">@color/car_grey_300</color>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 2688318..915915f 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -15,7 +15,6 @@
   limitations under the License.
 -->
 <resources>
-    <!-- Default screen margins, per the Android Design guidelines. -->
-    <dimen name="activity_horizontal_margin">16dp</dimen>
-    <dimen name="activity_vertical_margin">16dp</dimen>
+  <dimen name="action_bar_end_widget_margin_end">46dp</dimen>
+  <dimen name="toggle_ripple">90dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f820d1c..d72f65a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -15,8 +15,43 @@
   limitations under the License.
 -->
 <resources>
-    <string name="app_name">SystemUpdater</string>
-    <string name="action_settings">Settings</string>
-    <string name="title">Mounted Volumes</string>
-    <string name="back">Go Back</string>
+    <!-- Apply a system update. The name and title of the application. [CHAR LIMIT=40] -->
+    <string name="title">Local System Update</string>
+
+    <!-- Device List Fragment -->
+    <!-- An error that the selected file is not a valid update. [CHAR LIMIT=40] -->
+    <string name="invalid_file_type">This is not a valid file for updating</string>
+    <!-- A name of an invalid file. [CHAR LIMIT=40] -->
+    <string name="unknown_file">Unknown File</string>
+    <!-- An error message indicating that the file system could not be found. [CHAR LIMIT=40] -->
+    <string name="cannot_access_storage">Cannot access the storage device</string>
+
+    <!-- UpdateLayoutFragment -->
+    <string name="update_in_progress">Applying system update</string>
+    <!-- The name of the update file. [CHAR LIMIT=40] -->
+    <string name="update_file_name">File: %s"</string>
+    <!-- The size of the update file. [CHAR LIMIT=40] -->
+    <string name="update_file_size">Size: "</string>
+    <!-- A button to start installation of the update. [CHAR LIMIT=40] -->
+    <string name="install_now">Install Now</string>
+    <!-- An error message title to indicate the the update failed. [CHAR LIMIT=40] -->
+    <string name="update_failed">Update Failed</string>
+    <!-- A status that indicates that the update is being verified before installation. [CHAR LIMIT=40] -->
+    <string name="verify_in_progress">Verifying update&#8230;</string>
+    <!-- An error message indicating that verification failed. [CHAR LIMIT=40] -->
+    <string name="verify_failure">Verification Failed. Please select a valid update file.</string>
+    <!-- A status that indicates that the update is ready to be installed. [CHAR LIMIT=40] -->
+    <string name="install_ready">The update is ready to be installed.</string>
+    <!-- A status that indicates the installation process is running. [CHAR LIMIT=40] -->
+    <string name="install_in_progress">Installation in progress&#8230;</string>
+    <!-- A status that indicates that installation worked and update is complete. [CHAR LIMIT=40] -->
+    <string name="install_success">The update is successful.</string>
+    <!-- A status that indicates that installation failed. [CHAR LIMIT=40] -->
+    <string name="install_failed">System update installation failed.</string>
+    <!-- A status that indicates the system is about to reboot. [CHAR LIMIT=40] -->
+    <string name="rebooting">The update is successful. Rebooting now&#8230;</string>
+    <!-- The volumes found on the device. [CHAR LIMIT=40] -->
+    <string name="volumes">Volumes (%d)</string>
+    <!-- The path of the current directory. [CHAR LIMIT=20] -->
+    <string name="path">Path: %s</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index de155d8..ad10701 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -14,9 +14,42 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
+
 <resources>
-    <!-- Base application theme. -->
-    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
-        <!-- Customize your theme here. -->
+    <style name="Theme.SettingsBase" parent="@android:style/Theme.Material.Settings" />
+
+    <style name="SystemUpdaterTheme" parent="Theme.Car.Light.NoActionBar">
+        <item name="android:windowBackground">@color/car_card</item>
+        <item name="android:windowAnimationStyle">@style/SettingAnimationStyle</item>
+    </style>
+
+    <style name="SettingAnimationStyle">
+        <item name="android:windowEnterAnimation">@anim/trans_fade_in</item>
+        <item name="android:windowExitAnimation">@anim/trans_fade_out</item>
+    </style>
+
+    <style name="ActionBarStyle.Car" parent="Widget.Car.Toolbar">
+        <item name="actionBarSize">@dimen/car_app_bar_height</item>
+    </style>
+
+    <style name="ListIcon">
+        <item name="android:layout_width">@dimen/car_primary_icon_size</item>
+        <item name="android:layout_height">@dimen/car_primary_icon_size</item>
+        <item name="android:layout_centerVertical">true</item>
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:scaleType">fitCenter</item>
+        <item name="android:tint">@color/car_tint</item>
+    </style>
+
+    <style name="ListIcon.ActionBar" parent="ListIcon">
+        <item name="android:layout_gravity">center</item>
+        <item name="android:src">@drawable/ic_arrow_back</item>
+    </style>
+
+    <style name="SystemUpdate.ActionBar.Button.Borderless.Colored"
+           parent="Widget.Car.Button.Borderless.Colored">
+        <item name="android:minWidth">@dimen/car_button_min_width</item>
+        <item name="android:fontFamily">roboto-regular</item>
+        <item name="android:textColor">@color/car_accent</item>
     </style>
 </resources>
diff --git a/src/com/android/car/systemupdater/DeviceListFragment.java b/src/com/android/car/systemupdater/DeviceListFragment.java
index cb0d43f..857654b 100644
--- a/src/com/android/car/systemupdater/DeviceListFragment.java
+++ b/src/com/android/car/systemupdater/DeviceListFragment.java
@@ -15,88 +15,264 @@
  */
 package com.android.car.systemupdater;
 
-import android.app.Fragment;
+import android.content.Context;
+import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.storage.StorageEventListener;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.Button;
-import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.Toast;
 
-import java.io.File;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemAdapter;
+import androidx.car.widget.ListItemProvider;
+import androidx.car.widget.PagedListView;
+import androidx.car.widget.TextListItem;
+import androidx.fragment.app.Fragment;
 
-public class DeviceListFragment extends Fragment {
-    private ListView mFolderListView;
-    private SystemUpdaterActivity mActivity;
-    private File[] mFileNames = new File[0];
-    private FileAdapter mAdapter;
-    private Button mBackButton;
-    private TextView mTitle;
-    private String mTitleText;
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * Display a list of files and directories.
+ */
+public class DeviceListFragment extends Fragment implements UpFragment {
+
+    private static final String TAG = "DeviceListFragment";
+    private static final String UPDATE_FILE_SUFFIX = ".zip";
+    private static final FileFilter UPDATE_FILE_FILTER =
+            file -> !file.isHidden() && (file.isDirectory()
+                    || file.getName().toLowerCase().endsWith(UPDATE_FILE_SUFFIX));
+
+
+    private final Stack<File> mFileStack = new Stack<>();
+    private StorageManager mStorageManager;
+    private SystemUpdater mSystemUpdater;
+    private List<File> mListItems;
+    private ListItemAdapter mAdapter;
+    private FileItemProvider mItemProvider;
+    private TextView mCurrentPathView;
+
+    private final StorageEventListener mListener = new StorageEventListener() {
+        @Override
+        public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format(
+                        "onVolumeMetadataChanged %d %d %s", oldState, newState, vol.toString()));
+            }
+            mFileStack.clear();
+            showMountedVolumes();
+        }
+    };
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+
+        mSystemUpdater = (SystemUpdater) context;
+    }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mActivity = (SystemUpdaterActivity) getActivity();
-        mAdapter = new FileAdapter(mActivity, R.layout.folder_entry, mFileNames);
-    }
-    @Override
-    public View onCreateView(LayoutInflater inflater, final ViewGroup container,
-                             Bundle savedInstanceState) {
-        View v = inflater.inflate(R.layout.folder_list, container, false);
-        mTitle = (TextView) v.findViewById(R.id.title);
-        if (mTitleText != null) {
-            mTitle.setText(mTitleText);
+
+        Context context = getContext();
+        mItemProvider = new FileItemProvider(context);
+
+        mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
+        if (mStorageManager == null) {
+            if (Log.isLoggable(TAG, Log.WARN)) {
+                Log.w(TAG, "Failed to get StorageManager");
+            }
+            Toast.makeText(context, R.string.cannot_access_storage, Toast.LENGTH_LONG).show();
+            return;
         }
-        mFolderListView = (ListView) v.findViewById(R.id.folder_list);
-        mFolderListView.setAdapter(mAdapter);
-        mFolderListView.setOnItemClickListener(mItemClickListener);
-        mBackButton = (Button) v.findViewById(R.id.back);
-        mBackButton.setOnClickListener(mBackButtonListener);
-        return v;
     }
 
-    public void updateList(File[] locations) {
-        if (locations != null) {
-            mFileNames = locations;
-            if (mAdapter != null) {
-                mAdapter.setLocations(mFileNames);
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mAdapter = new ListItemAdapter(getContext(), mItemProvider);
+        return inflater.inflate(R.layout.folder_list, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+        PagedListView folderListView = (PagedListView) view.findViewById(R.id.folder_list);
+        folderListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
+        folderListView.setAdapter(mAdapter);
+
+        mCurrentPathView = (TextView) view.findViewById(R.id.current_path);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        AppCompatActivity activity = (AppCompatActivity) getActivity();
+        ActionBar actionBar = activity.getSupportActionBar();
+        actionBar.setCustomView(R.layout.action_bar_with_button);
+        actionBar.setDisplayShowCustomEnabled(true);
+        actionBar.setDisplayShowTitleEnabled(false);
+
+        showMountedVolumes();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (mStorageManager != null) {
+            mStorageManager.registerListener(mListener);
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (mStorageManager != null) {
+            mStorageManager.unregisterListener(mListener);
+        }
+    }
+
+    /** Display the mounted volumes on this device. */
+    private void showMountedVolumes() {
+        if (mStorageManager == null) {
+            return;
+        }
+        final List<VolumeInfo> vols = mStorageManager.getVolumes();
+        ArrayList<File> volumes = new ArrayList<>(vols.size());
+        for (VolumeInfo vol : vols) {
+            File path = vol.getPathForUser(getActivity().getUserId());
+            if (vol.getState() == VolumeInfo.STATE_MOUNTED
+                    && vol.getType() == VolumeInfo.TYPE_PUBLIC
+                    && path != null) {
+                volumes.add(path);
             }
         }
+
+        // Otherwise show all of the available volumes.
+        mCurrentPathView.setText(getString(R.string.volumes, volumes.size()));
+        setFileList(volumes);
     }
 
-    public void updateTitle(String title) {
-        if (mTitle != null) {
-            mTitle.setText(title);
-        } else {
-            mTitleText = title;
+    /** Set the list of files shown on the screen. */
+    private void setFileList(List<File> files) {
+        mListItems = files;
+        if (mAdapter != null) {
+            mAdapter.notifyDataSetChanged();
         }
     }
 
-    private final View.OnClickListener mBackButtonListener =
-            new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    mActivity.onBackPressed();
-                }
-            };
+    /** Handle user selection of a file. */
+    private void onFileSelected(File file) {
+        if (isUpdateFile(file)) {
+            mFileStack.clear();
+            mSystemUpdater.applyUpdate(file);
+        } else if (file.isDirectory()) {
+            showFolderContent(file);
+            mFileStack.push(file);
+        } else {
+            Toast.makeText(getContext(), R.string.invalid_file_type, Toast.LENGTH_LONG).show();
+        }
+    }
 
-    private final AdapterView.OnItemClickListener mItemClickListener =
-            new AdapterView.OnItemClickListener() {
-                @Override
-                public void onItemClick(AdapterView<?> adapterView, View view,
-                                        int position, long id) {
-                    if (mFileNames[position].getName().endsWith(".zip")) {
-                        mActivity.checkPackage(mFileNames[position]);
-                    } else if (mFileNames[position].isDirectory()) {
-                        mActivity.showFolderContent(mFileNames[position]);
-                    } else {
-                        Toast.makeText(mActivity, "This is not a valid file for updating",
-                                Toast.LENGTH_LONG).show();
-                    }
+    @Override
+    public boolean goUp() {
+        if (mFileStack.empty()) {
+            return false;
+        }
+        mFileStack.pop();
+        if (!mFileStack.empty()) {
+            // Show the list of files contained in the top of the stack.
+            showFolderContent(mFileStack.peek());
+        } else {
+            // When the stack is empty, display the volumes and reset the title.
+            showMountedVolumes();
+        }
+        return true;
+    }
+
+    /** Display the content at the provided {@code location}. */
+    private void showFolderContent(File folder) {
+        if (!folder.isDirectory()) {
+            // This should not happen.
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Cannot show contents of a file.");
+            }
+            return;
+        }
+
+        mCurrentPathView.setText(getString(R.string.path, folder.getAbsolutePath()));
+
+        // Retrieve the list of files and update the displayed list.
+        new AsyncTask<File, Void, File[]>() {
+            @Override
+            protected File[] doInBackground(File... file) {
+                return file[0].listFiles(UPDATE_FILE_FILTER);
+            }
+
+            @Override
+            protected void onPostExecute(File[] results) {
+                super.onPostExecute(results);
+                if (results == null) {
+                    results = new File[0];
+                    Toast.makeText(getContext(), R.string.cannot_access_storage,
+                            Toast.LENGTH_LONG).show();
                 }
-            };
+                setFileList(Arrays.asList(results));
+            }
+        }.execute(folder);
+    }
+
+    /** A list item provider to display the list of files on this fragment. */
+    private class FileItemProvider extends ListItemProvider {
+        private final Context mContext;
+
+        FileItemProvider(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public ListItem get(int position) {
+            if (position < 0 || position >= mListItems.size()) {
+                return null;
+            }
+            TextListItem item = new TextListItem(mContext);
+            File file = mListItems.get(position);
+            if (file != null) {
+                item.setTitle(file.getName());
+                item.setOnClickListener(v -> onFileSelected(file));
+            } else {
+                item.setTitle(getString(R.string.unknown_file));
+            }
+            return item;
+        }
+
+        @Override
+        public int size() {
+            return mListItems == null ? 0 : mListItems.size();
+        }
+    }
+
+    /** Returns true if a file is considered to contain a system update. */
+    private static boolean isUpdateFile(File file) {
+        return file.getName().endsWith(UPDATE_FILE_SUFFIX);
+    }
+
+    /** Used to request installation of an update. */
+    interface SystemUpdater {
+        /** Attempt to apply an update to the device contained in the {@code file}. */
+        void applyUpdate(File file);
+    }
 }
diff --git a/src/com/android/car/systemupdater/FileAdapter.java b/src/com/android/car/systemupdater/FileAdapter.java
deleted file mode 100644
index 1b0ecd6..0000000
--- a/src/com/android/car/systemupdater/FileAdapter.java
+++ /dev/null
@@ -1,78 +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.car.systemupdater;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.TextView;
-
-import java.io.File;
-
-
-public class FileAdapter extends ArrayAdapter<File> {
-    private final Context mContext;
-    private File[] mLocations;
-    private final int mLayoutResourceId;
-
-    public FileAdapter(Context c, int layoutResourceId, File[] locations) {
-        super(c, layoutResourceId, locations);
-        mContext = c;
-        this.mLayoutResourceId = layoutResourceId;
-        this.mLocations = locations;
-    }
-    @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        ViewHolder vh = new ViewHolder();
-        if (convertView == null) {
-            LayoutInflater inflater = LayoutInflater.from(mContext);
-            convertView = inflater.inflate(mLayoutResourceId, parent, false);
-            vh.textView = (TextView) convertView.findViewById(R.id.text);
-            vh.descriptionView = (TextView) convertView.findViewById(R.id.description);
-            convertView.setTag(vh);
-        } else {
-            vh = (ViewHolder) convertView.getTag();
-        }
-        if (mLocations[position] != null) {
-            vh.textView.setText(mLocations[position].getAbsolutePath());
-            if (mLocations[position].getAbsolutePath().endsWith(".zip")
-                    || mLocations[position].isDirectory()) {
-                vh.textView.setTextColor(Color.GREEN);
-            } else {
-                vh.textView.setTextColor(Color.GRAY);
-            }
-        }
-        return convertView;
-    }
-
-    @Override
-    public int getCount() {
-        return mLocations.length;
-    }
-
-    public void setLocations(File[] locations) {
-        mLocations = locations;
-        notifyDataSetChanged();
-    }
-
-    static class ViewHolder {
-        TextView textView;
-        TextView descriptionView;
-    }
-}
diff --git a/src/com/android/car/systemupdater/SystemUpdaterActivity.java b/src/com/android/car/systemupdater/SystemUpdaterActivity.java
index e442731..c53ea18 100644
--- a/src/com/android/car/systemupdater/SystemUpdaterActivity.java
+++ b/src/com/android/car/systemupdater/SystemUpdaterActivity.java
@@ -15,291 +15,98 @@
  */
 package com.android.car.systemupdater;
 
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.FragmentManager;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.AsyncTask;
+import static com.android.car.systemupdater.UpdateLayoutFragment.EXTRA_RESUME_UPDATE;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.RecoverySystem;
-import android.util.Log;
-import android.widget.Toast;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.GeneralSecurityException;
-import java.util.List;
-
-import android.os.storage.StorageEventListener;
-import android.os.storage.StorageManager;
-import android.os.storage.VolumeInfo;
 
 /**
- * A prototype of performing system update using an ota package on internal or external storage.
- * TODO(yaochen): Move the code to a proper location and let it extend CarActivity once available.
+ * Apply a system update using an ota package on internal or external storage.
  */
-public class SystemUpdaterActivity extends Activity {
-    private static final String TAG = "SystemUpdaterActivity";
-    private static final boolean DEBUG = true;
-    private static final String UPDATE_FILE_NAME = "update.zip";
+public class SystemUpdaterActivity extends AppCompatActivity
+        implements DeviceListFragment.SystemUpdater {
 
-    private final Handler mHandler = new Handler();
-    private StorageManager mStorageManager = null;
-    private ProgressDialog mVerifyPackageDialog = null;
-
-
-    private final StorageEventListener mListener = new StorageEventListener() {
-        @Override
-        public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
-            if (DEBUG) {
-                Log.d(TAG, "onVolumeMetadataChanged " + oldState + " " + newState
-                        + " " + vol.toString());
-            }
-            showMountedVolumes();
-        }
+    private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
+    private static final int STORAGE_PERMISSIONS_REQUEST_CODE = 0;
+    private static final String[] REQUIRED_STORAGE_PERMISSIONS = new String[]{
+            Manifest.permission.READ_EXTERNAL_STORAGE,
+            Manifest.permission.WRITE_EXTERNAL_STORAGE
     };
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                != PackageManager.PERMISSION_GRANTED) {
+            ActivityCompat.requestPermissions(this, REQUIRED_STORAGE_PERMISSIONS,
+                    STORAGE_PERMISSIONS_REQUEST_CODE);
+        }
+
         setContentView(R.layout.activity_main);
-        mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
-        if (mStorageManager == null) {
-            Log.w(TAG, "Failed to get StorageManager");
-            Toast.makeText(this, "Cannot get StorageManager!", Toast.LENGTH_LONG).show();
-        }
-    }
+        Toolbar toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
 
-    @Override
-    protected void onResume() {
-        super.onResume();
-        if (mStorageManager != null) {
-            mStorageManager.registerListener(mListener);
-            showMountedVolumes();
-        }
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        if (mStorageManager != null) {
-            mStorageManager.unregisterListener(mListener);
-        }
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (getFragmentManager().getBackStackEntryCount() > 0) {
-            getFragmentManager().popBackStackImmediate();
-        } else {
-            super.onBackPressed();
-        }
-    }
-
-    public void showMountedVolumes() {
-        if (mStorageManager == null) {
-            return;
-        }
-        final List<VolumeInfo> vols = mStorageManager.getVolumes();
-        File[] files = new File[vols.size()];
-        int i = 0;
-        for (VolumeInfo vol : vols) {
-            File path = vol.getPathForUser(getUserId());
-            if (vol.getState() != VolumeInfo.STATE_MOUNTED || path == null) {
-                continue;
-            }
-            files[i++] = path;
-        }
-        getFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
-        DeviceListFragment frag = new DeviceListFragment();
-        frag.updateList(files);
-        frag.updateTitle(getString(R.string.title));
-        getFragmentManager().beginTransaction()
-                .replace(R.id.device_container, frag).commit();
-    }
-
-    public void showFolderContent(final File location) {
-        if (!location.isDirectory()) {
-            return;
-        }
-         AsyncTask<String, Void, File[]> readFilesTask = new AsyncTask<String, Void, File[]>() {
-            @Override
-            protected File[] doInBackground(String... strings) {
-                File f = new File(strings[0]);
-                /* if we want to filter files, use
-                File[] files = f.listFiles(new FilenameFilter() {
-                    @Override
-                    public boolean accept(File dir, String filename) {
-                        return true;
-                    }
-                }); */
-                return f.listFiles();
-            }
-
-            @Override
-            protected void onPostExecute(File[] results) {
-                super.onPostExecute(results);
-                if (results == null) {
-                    results = new File[0];
-                }
-                DeviceListFragment frag = new DeviceListFragment();
-                frag.updateTitle(location.getAbsolutePath());
-                frag.updateList(results);
-                getFragmentManager().beginTransaction()
-                        .replace(R.id.device_container, frag).addToBackStack(null).commit();
-            }
-        };
-        readFilesTask.execute(location.getAbsolutePath());
-    }
-
-    public void checkPackage(File file) {
-        mVerifyPackageDialog = new ProgressDialog(this);
-        mVerifyPackageDialog.setTitle("Verifying... " + file.getAbsolutePath());
-
-        final PackageVerifier verifyPackage = new PackageVerifier();
-        verifyPackage.execute(file);
-        mVerifyPackageDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
-            @Override
-            public void onCancel(DialogInterface dialogInterface) {
-                verifyPackage.cancel(true);
-            }
-        });
-        mVerifyPackageDialog.setProgressStyle(mVerifyPackageDialog.STYLE_HORIZONTAL);
-        mVerifyPackageDialog.setMax(100);
-        mVerifyPackageDialog.setProgress(0);
-        mVerifyPackageDialog.show();
-    }
-
-    private class PackageVerifier extends AsyncTask<File, Void, Exception> {
-        File mFile;
-
-        @Override
-        protected Exception doInBackground(File... files) {
-            File file = files[0];
-            mFile = file;
-            try {
-                RecoverySystem.verifyPackage(file, mProgressListener, null);
-            } catch (GeneralSecurityException e) {
-                Log.e(TAG, "Security Exception in verifying package " + file, e);
-                return e;
-            } catch (IOException e) {
-                Log.e(TAG, "IO Exception in verifying package " + file, e);
-                return e;
-            }
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Exception result) {
-            mVerifyPackageDialog.cancel();
-            if (result == null) {
-                mVerifyPackageDialog = new ProgressDialog(SystemUpdaterActivity.this);
-                mVerifyPackageDialog.setTitle("Copying " + mFile.getName()
-                        + " to " + getCacheDir() + "/" + UPDATE_FILE_NAME);
-                mVerifyPackageDialog.setProgressStyle(mVerifyPackageDialog.STYLE_HORIZONTAL);
-                mVerifyPackageDialog.setMax((int) (mFile.length() / 1024));
-                mVerifyPackageDialog.show();
-                new CopyFile().execute(mFile);
+        if (savedInstanceState == null) {
+            Bundle intentExtras = getIntent().getExtras();
+            if (intentExtras != null && intentExtras.getBoolean(EXTRA_RESUME_UPDATE)) {
+                UpdateLayoutFragment fragment = UpdateLayoutFragment.newResumedInstance();
+                getSupportFragmentManager().beginTransaction()
+                        .replace(R.id.device_container, fragment, FRAGMENT_TAG)
+                        .commitNow();
             } else {
-                AlertDialog.Builder doneDialog =
-                        new AlertDialog.Builder(SystemUpdaterActivity.this);
-                doneDialog.setMessage("Verification failed! " + result.getMessage()).show();
+                DeviceListFragment fragment = new DeviceListFragment();
+                getSupportFragmentManager().beginTransaction()
+                        .replace(R.id.device_container, fragment, FRAGMENT_TAG)
+                        .commitNow();
             }
         }
     }
 
-
-    private class CopyFile extends AsyncTask<File, Void, Exception> {
-        @Override
-        protected Exception doInBackground(File... files) {
-            File file = files[0];
-            if (getCacheDir().getFreeSpace() < file.length()) {
-                return new IOException("Not enough cache space!");
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            UpFragment upFragment =
+                    (UpFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
+            if (!upFragment.goUp()) {
+                onBackPressed();
             }
-            File dest = new File(getCacheDir(), UPDATE_FILE_NAME);
-            try {
-              copy(file, dest);
-            } catch (IOException e) {
-                Log.e(TAG, "Error when coping file to cache", e);
-                dest.delete();
-                return new IOException(e.getMessage());
-            }
-            return null;
+            return true;
         }
-
-        @Override
-        protected void onPostExecute(Exception result) {
-            mVerifyPackageDialog.cancel();
-            AlertDialog.Builder doneDialog = new AlertDialog.Builder(SystemUpdaterActivity.this);
-
-            doneDialog.setMessage("Copy " + (result == null ? "completed!" : "failed!"
-                    + result.getMessage()));
-
-            if (result == null) {
-                doneDialog.setPositiveButton("Start system update",
-                        new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialogInterface, int i) {
-                        try {
-                            RecoverySystem.installPackage(SystemUpdaterActivity.this,
-                                    new File(getCacheDir(), UPDATE_FILE_NAME));
-                        } catch (IOException e) {
-                            Log.e(TAG, "IOException in installing ota package");
-                            Toast.makeText(SystemUpdaterActivity.this,
-                                    "IOException in installing ota package ",
-                                    Toast.LENGTH_LONG).show();
-                        }
-                    }
-                });
-            } else {
-                Log.e(TAG, "Copy failed!", result);
-            }
-            doneDialog.create().show();
-        }
+        return super.onOptionsItemSelected(item);
     }
 
-    private void copy(File src, File dst) throws IOException {
-        InputStream in = new FileInputStream(src);
-        OutputStream out = new FileOutputStream(dst);
-        try {
-            // Transfer bytes from in to out
-            byte[] buf = new byte[0x10000]; // 64k
-            int len;
-            while ((len = in.read(buf)) > 0) {
-                out.write(buf, 0, len);
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        mVerifyPackageDialog.incrementProgressBy(1);
-                    }
-                });
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String permissions[],
+            int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        if (STORAGE_PERMISSIONS_REQUEST_CODE == requestCode) {
+            if (grantResults.length == 0) {
+                finish();
             }
-        } finally {
-            in.close();
-            out.close();
-        }
-    }
-
-    private final RecoverySystem.ProgressListener mProgressListener =
-            new RecoverySystem.ProgressListener() {
-        @Override
-        public void onProgress(final int i) {
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    if (mVerifyPackageDialog != null) {
-                        mVerifyPackageDialog.setProgress(i);
-                    }
+            for (int grantResult : grantResults) {
+                if (grantResult != PackageManager.PERMISSION_GRANTED) {
+                    finish();
                 }
-            });
+            }
         }
-    };
+    }
+
+    @Override
+    public void applyUpdate(File file) {
+        UpdateLayoutFragment fragment = UpdateLayoutFragment.getInstance(file);
+        getSupportFragmentManager().beginTransaction()
+                .replace(R.id.device_container, fragment, FRAGMENT_TAG)
+                .addToBackStack(null)
+                .commit();
+    }
 }
diff --git a/src/com/android/car/systemupdater/UpFragment.java b/src/com/android/car/systemupdater/UpFragment.java
new file mode 100644
index 0000000..094814b
--- /dev/null
+++ b/src/com/android/car/systemupdater/UpFragment.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 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.car.systemupdater;
+
+/** Allow a fragment to handle an up action. */
+interface UpFragment {
+    /** Returns true if the fragment handled the up action. */
+    default boolean goUp() {
+        return false;
+    }
+}
diff --git a/src/com/android/car/systemupdater/UpdateLayoutFragment.java b/src/com/android/car/systemupdater/UpdateLayoutFragment.java
new file mode 100644
index 0000000..4ff1b58
--- /dev/null
+++ b/src/com/android/car/systemupdater/UpdateLayoutFragment.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2018 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.car.systemupdater;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.UpdateEngine;
+import android.os.UpdateEngineCallback;
+import android.text.format.Formatter;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Display update state and progress. */
+public class UpdateLayoutFragment extends Fragment implements UpFragment {
+    public static final String EXTRA_RESUME_UPDATE = "resume_update";
+
+    private static final String TAG = "UpdateLayoutFragment";
+    private static final String EXTRA_UPDATE_FILE = "extra_update_file";
+    private static final int PERCENT_MAX = 100;
+    private static final String REBOOT_REASON = "reboot-ab-update";
+    private static final String NOTIFICATION_CHANNEL_ID = "update";
+    private static final int NOTIFICATION_ID = 1;
+
+    private ProgressBar mProgressBar;
+    private TextView mContentTitle;
+    private TextView mContentInfo;
+    private TextView mContentDetails;
+    private File mUpdateFile;
+    private Button mSystemUpdateToolbarAction;
+    private PowerManager mPowerManager;
+    private NotificationManager mNotificationManager;
+    private final UpdateVerifier mPackageVerifier = new UpdateVerifier();
+    private final UpdateEngine mUpdateEngine = new UpdateEngine();
+    private boolean mInstallationInProgress = false;
+
+    private final CarUpdateEngineCallback mCarUpdateEngineCallback = new CarUpdateEngineCallback();
+
+    /** Create a {@link UpdateLayoutFragment}. */
+    public static UpdateLayoutFragment getInstance(File file) {
+        UpdateLayoutFragment fragment = new UpdateLayoutFragment();
+        Bundle bundle = new Bundle();
+        bundle.putString(EXTRA_UPDATE_FILE, file.getAbsolutePath());
+        fragment.setArguments(bundle);
+        return fragment;
+    }
+
+    /** Create a {@link UpdateLayoutFragment} showing an update in progress. */
+    public static UpdateLayoutFragment newResumedInstance() {
+        UpdateLayoutFragment fragment = new UpdateLayoutFragment();
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(EXTRA_RESUME_UPDATE, true);
+        fragment.setArguments(bundle);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (!getArguments().getBoolean(EXTRA_RESUME_UPDATE)) {
+            mUpdateFile = new File(getArguments().getString(EXTRA_UPDATE_FILE));
+        }
+        mPowerManager = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
+        mNotificationManager =
+                (NotificationManager) getContext().getSystemService(NotificationManager.class);
+        mNotificationManager.createNotificationChannel(
+                new NotificationChannel(
+                        NOTIFICATION_CHANNEL_ID,
+                        getContext().getString(R.id.system_update_auto_content_title),
+                        NotificationManager.IMPORTANCE_DEFAULT));
+    }
+
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.system_update_auto_content, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+        mContentTitle = view.findViewById(R.id.system_update_auto_content_title);
+        mContentInfo = view.findViewById(R.id.system_update_auto_content_info);
+        mContentDetails = view.findViewById(R.id.system_update_auto_content_details);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        AppCompatActivity activity = (AppCompatActivity) getActivity();
+
+        ActionBar actionBar = activity.getSupportActionBar();
+        actionBar.setCustomView(R.layout.action_bar_with_button);
+        actionBar.setDisplayShowCustomEnabled(true);
+        actionBar.setDisplayShowTitleEnabled(false);
+
+        mProgressBar = (ProgressBar) activity.findViewById(R.id.progress_bar);
+
+        mSystemUpdateToolbarAction = activity.findViewById(R.id.action_button1);
+        mProgressBar.setIndeterminate(true);
+        mProgressBar.setVisibility(View.VISIBLE);
+        showStatus(R.string.verify_in_progress);
+
+        if (getArguments().getBoolean(EXTRA_RESUME_UPDATE)) {
+            // Rejoin the update already in progress.
+            showInstallationInProgress();
+        } else {
+            // Extract the necessary information and begin the update.
+            mPackageVerifier.execute(mUpdateFile);
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mPackageVerifier != null) {
+            mPackageVerifier.cancel(true);
+        }
+    }
+
+    /** Update the status information. */
+    private void showStatus(@StringRes int status) {
+        mContentTitle.setText(status);
+        if (mInstallationInProgress) {
+            mNotificationManager.notify(NOTIFICATION_ID, createNotification(getContext(), status));
+        } else {
+            mNotificationManager.cancel(NOTIFICATION_ID);
+        }
+    }
+
+    /** Show the install now button. */
+    private void showInstallNow(UpdateParser.ParsedUpdate update) {
+        mContentTitle.setText(R.string.install_ready);
+        mContentInfo.append(getString(R.string.update_file_name, mUpdateFile.getName()));
+        mContentInfo.append(System.getProperty("line.separator"));
+        mContentInfo.append(getString(R.string.update_file_size));
+        mContentInfo.append(Formatter.formatFileSize(getContext(), mUpdateFile.length()));
+        mContentDetails.setText(null);
+        mSystemUpdateToolbarAction.setOnClickListener(v -> installUpdate(update));
+        mSystemUpdateToolbarAction.setText(R.string.install_now);
+        mSystemUpdateToolbarAction.setVisibility(View.VISIBLE);
+    }
+
+    /** Reboot the system. */
+    private void rebootNow() {
+        if (Log.isLoggable(TAG, Log.INFO)) {
+            Log.i(TAG, "Rebooting Now.");
+        }
+        mPowerManager.reboot(REBOOT_REASON);
+    }
+
+    /** Attempt to install the update that is copied to the device. */
+    private void installUpdate(UpdateParser.ParsedUpdate parsedUpdate) {
+        showInstallationInProgress();
+        mUpdateEngine.applyPayload(
+                parsedUpdate.mUrl, parsedUpdate.mOffset, parsedUpdate.mSize, parsedUpdate.mProps);
+    }
+
+    /** Set the layout to show installation progress. */
+    private void showInstallationInProgress() {
+        mInstallationInProgress = true;
+        mProgressBar.setIndeterminate(false);
+        mProgressBar.setVisibility(View.VISIBLE);
+        mProgressBar.setMax(PERCENT_MAX);
+        mSystemUpdateToolbarAction.setVisibility(View.GONE);
+        showStatus(R.string.install_in_progress);
+
+        mUpdateEngine.bind(mCarUpdateEngineCallback, new Handler(getContext().getMainLooper()));
+    }
+
+    /** Attempt to verify the update and extract information needed for installation. */
+    private class UpdateVerifier extends AsyncTask<File, Void, UpdateParser.ParsedUpdate> {
+
+        @Override
+        protected UpdateParser.ParsedUpdate doInBackground(File... files) {
+            Preconditions.checkArgument(files.length > 0, "No file specified");
+            File file = files[0];
+            try {
+                return UpdateParser.parse(file);
+            } catch (IOException e) {
+                Log.e(TAG, String.format("For file %s", file), e);
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(UpdateParser.ParsedUpdate result) {
+            mProgressBar.setVisibility(View.GONE);
+            if (result == null) {
+                showStatus(R.string.verify_failure);
+                return;
+            }
+            if (!result.isValid()) {
+                showStatus(R.string.verify_failure);
+                Log.e(TAG, String.format("Failed verification %s", result));
+                return;
+            }
+            if (Log.isLoggable(TAG, Log.INFO)) {
+                Log.i(TAG, result.toString());
+            }
+
+            showInstallNow(result);
+        }
+    }
+
+    /** Handles events from the UpdateEngine. */
+    public class CarUpdateEngineCallback extends UpdateEngineCallback {
+
+        @Override
+        public void onStatusUpdate(int status, float percent) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("onStatusUpdate %d, Percent %.2f", status, percent));
+            }
+            switch (status) {
+                case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT:
+                    rebootNow();
+                    break;
+                case UpdateEngine.UpdateStatusConstants.DOWNLOADING:
+                    mProgressBar.setProgress((int) (percent * 100));
+                    break;
+                default:
+                    // noop
+            }
+        }
+
+        @Override
+        public void onPayloadApplicationComplete(int errorCode) {
+            Log.w(TAG, String.format("onPayloadApplicationComplete %d", errorCode));
+            mInstallationInProgress = false;
+            showStatus(errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
+                    ? R.string.install_success
+                    : R.string.install_failed);
+            mProgressBar.setVisibility(View.GONE);
+            mSystemUpdateToolbarAction.setVisibility(View.GONE);
+        }
+    }
+
+    /** Build a notification to show the installation status. */
+    private static Notification createNotification(Context context, @StringRes int contents) {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(context, SystemUpdaterActivity.class));
+        intent.putExtra(EXTRA_RESUME_UPDATE, true);
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(
+                        context,
+                        /* requestCode= */ 0,
+                        intent,
+                        PendingIntent.FLAG_UPDATE_CURRENT);
+
+        return new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
+                .setVisibility(Notification.VISIBILITY_PUBLIC)
+                .setContentTitle(context.getString(contents))
+                .setSmallIcon(R.drawable.ic_system_update_alt_black_48dp)
+                .setContentIntent(pendingIntent)
+                .setShowWhen(false)
+                .setOngoing(true)
+                .setAutoCancel(false)
+                .build();
+    }
+}
diff --git a/src/com/android/car/systemupdater/UpdateParser.java b/src/com/android/car/systemupdater/UpdateParser.java
new file mode 100644
index 0000000..d48096d
--- /dev/null
+++ b/src/com/android/car/systemupdater/UpdateParser.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 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.car.systemupdater;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** Parse an A/B update zip file. */
+class UpdateParser {
+
+    private static final String TAG = "UpdateLayoutFragment";
+    private static final String PAYLOAD_BIN_FILE = "payload.bin";
+    private static final String PAYLOAD_PROPERTIES = "payload_properties.txt";
+    private static final String FILE_URL_PREFIX = "file://";
+    private static final int ZIP_FILE_HEADER = 30;
+
+    private UpdateParser() {
+    }
+
+    /**
+     * Parse a zip file containing a system update and return a non null ParsedUpdate.
+     */
+    @Nullable
+    static ParsedUpdate parse(@NonNull File file) throws IOException {
+        Preconditions.checkNotNull(file);
+
+        long payloadOffset = 0;
+        long payloadSize = 0;
+        boolean payloadFound = false;
+        String[] props = null;
+
+        try (ZipFile zipFile = new ZipFile(file)) {
+            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+                ZipEntry entry = entries.nextElement();
+                long fileSize = entry.getCompressedSize();
+                if (!payloadFound) {
+                    payloadOffset += ZIP_FILE_HEADER + entry.getName().length();
+                    if (entry.getExtra() != null) {
+                        payloadOffset += entry.getExtra().length;
+                    }
+                }
+
+                if (entry.isDirectory()) {
+                    continue;
+                } else if (entry.getName().equals(PAYLOAD_BIN_FILE)) {
+                    payloadSize = fileSize;
+                    payloadFound = true;
+                } else if (entry.getName().equals(PAYLOAD_PROPERTIES)) {
+                    try (BufferedReader buffer = new BufferedReader(
+                            new InputStreamReader(zipFile.getInputStream(entry)))) {
+                        props = buffer.lines().toArray(String[]::new);
+                    }
+                }
+                if (!payloadFound) {
+                    payloadOffset += fileSize;
+                }
+
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, String.format("Entry %s", entry.getName()));
+                }
+            }
+        }
+        return new ParsedUpdate(file, payloadOffset, payloadSize, props);
+    }
+
+    /** Information parsed from an update file. */
+    static class ParsedUpdate {
+        final String mUrl;
+        final long mOffset;
+        final long mSize;
+        final String[] mProps;
+
+        ParsedUpdate(File file, long offset, long size, String[] props) {
+            mUrl = FILE_URL_PREFIX + file.getAbsolutePath();
+            mOffset = offset;
+            mSize = size;
+            mProps = props;
+        }
+
+        /** Verify the update information is correct. */
+        boolean isValid() {
+            return mOffset >= 0 && mSize > 0 && mProps != null;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(Locale.getDefault(),
+                    "ParsedUpdate: URL=%s, offset=%d, size=%s, props=%s",
+                    mUrl, mOffset, mSize, Arrays.toString(mProps));
+        }
+    }
+}