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…</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…</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…</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));
+ }
+ }
+}