diff options
-rw-r--r-- | res/values-b+sr+Latn-television/strings.xml | 5 | ||||
-rw-r--r-- | res/values-b+sr+Latn/strings.xml | 5 | ||||
-rw-r--r-- | res/values-be-rBY-television/strings.xml | 5 | ||||
-rw-r--r-- | res/values-be-rBY/strings.xml | 9 | ||||
-rw-r--r-- | res/values-bs-rBA-television/strings.xml | 5 | ||||
-rw-r--r-- | res/values-bs-rBA/strings.xml | 7 | ||||
-rw-r--r-- | res/values-es-watch/strings.xml | 2 | ||||
-rw-r--r-- | res/values-eu-rES-television/strings.xml | 2 | ||||
-rw-r--r-- | res/values-fa-watch/strings.xml | 2 | ||||
-rw-r--r-- | res/values-gu-rIN-television/strings.xml | 2 | ||||
-rw-r--r-- | res/values-gu-rIN-watch/strings.xml | 2 | ||||
-rw-r--r-- | src/com/android/packageinstaller/wear/InstallTask.java | 173 | ||||
-rw-r--r-- | src/com/android/packageinstaller/wear/InstallerConstants.java | 59 | ||||
-rw-r--r-- | src/com/android/packageinstaller/wear/PackageInstallerFactory.java | 36 | ||||
-rw-r--r-- | src/com/android/packageinstaller/wear/PackageInstallerImpl.java | 324 | ||||
-rw-r--r-- | src/com/android/packageinstaller/wear/WearPackageInstallerService.java | 64 |
16 files changed, 638 insertions, 64 deletions
diff --git a/res/values-b+sr+Latn-television/strings.xml b/res/values-b+sr+Latn-television/strings.xml index 5dce759d..c91f97b5 100644 --- a/res/values-b+sr+Latn-television/strings.xml +++ b/res/values-b+sr+Latn-television/strings.xml @@ -20,9 +20,4 @@ <string name="grant_dialog_how_to_change" msgid="615414835189256888">"Ovo možete da promenite kasnije u Podešavanjima > Aplikacije"</string> <string name="current_permission_template" msgid="4793247012451594523">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g>/<xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> <string name="preference_show_system_apps" msgid="7330308025768596149">"Prikaži sistemske aplikacije"</string> - <string name="app_permissions_decor_title" msgid="1461057434211920209">"Dozvole za aplikacije"</string> - <string name="manage_permissions_decor_title" msgid="4823785025722958092">"Dozvole za aplikacije"</string> - <string name="permission_apps_decor_title" msgid="3644363529649579576">"Dozvole za aplikaciju <xliff:g id="PERMISSION">%1$s</xliff:g>"</string> - <string name="additional_permissions_decor_title" msgid="7000432624396037882">"Dodatne dozvole"</string> - <string name="system_apps_decor_title" msgid="5292119639812561805">"Dozvole za aplikaciju <xliff:g id="PERMISSION">%1$s</xliff:g>"</string> </resources> diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml index 0425bb5c..2c201a26 100644 --- a/res/values-b+sr+Latn/strings.xml +++ b/res/values-b+sr+Latn/strings.xml @@ -70,8 +70,7 @@ <string name="uninstall_failed" msgid="631122574306299512">"Deinstaliranje nije uspelo."</string> <string name="uninstall_failed_device_policy_manager" msgid="3493789239037852035">"Nije moguće deinstalirati zato što je ovaj paket aktivan administrator uređaja."</string> <string name="uninstall_failed_device_policy_manager_of_user" msgid="4466062391552204291">"Nije moguće deinst. jer je paket aktivni administrator uređaja za korisnika <xliff:g id="USERNAME">%1$s</xliff:g>."</string> - <string name="uninstall_all_blocked_profile_owner" msgid="3544933038594382346">"Ova aplikacija je potrebna za neke korisnike ili profile, a deinstalirana je za druge"</string> - <string name="uninstall_blocked_profile_owner" msgid="6912141045528994954">"Ova aplikacija je potrebna za vaš profil i ne može da se deinstalira."</string> + <string name="uninstall_blocked_profile_owner" msgid="5154358281537910006">"Ova aplikacija je potrebna za neke korisnike ili profile, a deinstalirana je za druge"</string> <string name="uninstall_blocked_device_owner" msgid="7074175526413453063">"Ova aplikacija je potrebna administratoru uređaja i ne može da se deinstalira."</string> <string name="manage_device_administrators" msgid="891392489300312370">"Upravljaj administratorima uređaja"</string> <string name="manage_users" msgid="3125018886835668847">"Upravljaj korisnicima"</string> @@ -86,7 +85,7 @@ <string name="grant_dialog_button_deny" msgid="2176510645406614340">"Odbij"</string> <string name="grant_dialog_button_deny_anyway" msgid="847960499284125250">"Ipak odbij"</string> <string name="current_permission_template" msgid="6378304249516652817">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g>. od <xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> - <string name="permission_warning_template" msgid="7332275268559121742">"Želite li da dozvolite da <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> obavi sledeću radnju: <xliff:g id="ACTION">%2$s</xliff:g>?"</string> + <string name="permission_warning_template" msgid="5209102765005869454">"Želite li da dozvolite da usluga <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="ACTION">%2$s</xliff:g>?"</string> <string name="grant_dialog_button_allow" msgid="4616529495342337095">"Dozvoli"</string> <string name="app_permissions_breadcrumb" msgid="3390836200791539264">"Aplikacije"</string> <string name="app_permissions" msgid="3146758905824597178">"Dozvole za aplikacije"</string> diff --git a/res/values-be-rBY-television/strings.xml b/res/values-be-rBY-television/strings.xml index 5735d627..01023339 100644 --- a/res/values-be-rBY-television/strings.xml +++ b/res/values-be-rBY-television/strings.xml @@ -20,9 +20,4 @@ <string name="grant_dialog_how_to_change" msgid="615414835189256888">"Пазней гэта можна змянiць у раздзеле «Налады > Праграмы»"</string> <string name="current_permission_template" msgid="4793247012451594523">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g> / <xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> <string name="preference_show_system_apps" msgid="7330308025768596149">"Паказваць сістэмныя праграмы"</string> - <string name="app_permissions_decor_title" msgid="1461057434211920209">"Дазволы праграмы"</string> - <string name="manage_permissions_decor_title" msgid="4823785025722958092">"Дазволы праграмы"</string> - <string name="permission_apps_decor_title" msgid="3644363529649579576">"Дазволы праграмы <xliff:g id="PERMISSION">%1$s</xliff:g>"</string> - <string name="additional_permissions_decor_title" msgid="7000432624396037882">"Дадатковыя дазволы"</string> - <string name="system_apps_decor_title" msgid="5292119639812561805">"Дазволы праграмы <xliff:g id="PERMISSION">%1$s</xliff:g>"</string> </resources> diff --git a/res/values-be-rBY/strings.xml b/res/values-be-rBY/strings.xml index c4dba525..d81023e1 100644 --- a/res/values-be-rBY/strings.xml +++ b/res/values-be-rBY/strings.xml @@ -21,7 +21,7 @@ <string name="install" msgid="5896438203900042068">"Усталяваць"</string> <string name="done" msgid="3889387558374211719">"Гатова"</string> <string name="security_settings_desc" msgid="2706691034244052604">"Дазволіць гэтаму прыкладанню выконваць наступныя дзеяннi:"</string> - <string name="cancel" msgid="8360346460165114585">"Скасаваць"</string> + <string name="cancel" msgid="8360346460165114585">"Адмяніць"</string> <string name="unknown" msgid="4742479012767208045">"Невядомы"</string> <string name="installing" msgid="8613631001631998372">"Усталяванне..."</string> <string name="install_done" msgid="3682715442154357097">"Прыкладанне ўсталявана."</string> @@ -70,8 +70,7 @@ <string name="uninstall_failed" msgid="631122574306299512">"Няўдалае выдаленне."</string> <string name="uninstall_failed_device_policy_manager" msgid="3493789239037852035">"Немагчыма выдаліць, паколькі гэты пакет з\'яўляецца актыўным адміністратарам прылады."</string> <string name="uninstall_failed_device_policy_manager_of_user" msgid="4466062391552204291">"Немагчыма выдаліць, бо гэты пакет з\'яўляецца актыўным адміністратарам прылады для карыстальніка <xliff:g id="USERNAME">%1$s</xliff:g>."</string> - <string name="uninstall_all_blocked_profile_owner" msgid="3544933038594382346">"Гэта праграма патрабуецца для некаторых карыстальнікаў або профіляў і была выдалена для іншых"</string> - <string name="uninstall_blocked_profile_owner" msgid="6912141045528994954">"Гэта праграма неабходная для вашага профілю і не можа быць выдалена."</string> + <string name="uninstall_blocked_profile_owner" msgid="5154358281537910006">"Гэта праграма патраб. для некат. карыст. або профіляў і была выдалена для іншых"</string> <string name="uninstall_blocked_device_owner" msgid="7074175526413453063">"Гэта праграма патрабуецца адміністратару вашай прылады і не можа быць выдалена."</string> <string name="manage_device_administrators" msgid="891392489300312370">"Кіраваць адміністратарамі прылады"</string> <string name="manage_users" msgid="3125018886835668847">"Кіраванне карыстальнікамі"</string> @@ -83,10 +82,10 @@ <string name="devicePerms" msgid="6733560207731294504">"Доступ да прылады"</string> <string name="no_new_perms" msgid="6657813692169565975">"Гэтае абнаўленне не патрабуе ніякіх новых дазволаў."</string> <string name="grant_confirm_question" msgid="4690289297029223742">"Даць наступны дазвол? Ён будзе атрымліваць доступ да:"</string> - <string name="grant_dialog_button_deny" msgid="2176510645406614340">"Адмовіць"</string> + <string name="grant_dialog_button_deny" msgid="2176510645406614340">"Адхіліць"</string> <string name="grant_dialog_button_deny_anyway" msgid="847960499284125250">"Усё роўна адмовіць"</string> <string name="current_permission_template" msgid="6378304249516652817">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g> з <xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> - <string name="permission_warning_template" msgid="7332275268559121742">"Дазволіць <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> <xliff:g id="ACTION">%2$s</xliff:g>?"</string> + <string name="permission_warning_template" msgid="5209102765005869454">"Дазволіць <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="ACTION">%2$s</xliff:g>?"</string> <string name="grant_dialog_button_allow" msgid="4616529495342337095">"Дазволіць"</string> <string name="app_permissions_breadcrumb" msgid="3390836200791539264">"Праграмы"</string> <string name="app_permissions" msgid="3146758905824597178">"Дазволы праграмы"</string> diff --git a/res/values-bs-rBA-television/strings.xml b/res/values-bs-rBA-television/strings.xml index 8fba2cc7..1924d741 100644 --- a/res/values-bs-rBA-television/strings.xml +++ b/res/values-bs-rBA-television/strings.xml @@ -20,9 +20,4 @@ <string name="grant_dialog_how_to_change" msgid="615414835189256888">"Ovo možete kasnije promijeniti u odjeljku Postavke > Aplikacije"</string> <string name="current_permission_template" msgid="4793247012451594523">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g>/<xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> <string name="preference_show_system_apps" msgid="7330308025768596149">"Prikaži sistemske aplikacije"</string> - <string name="app_permissions_decor_title" msgid="1461057434211920209">"Odobrenja za aplikacije"</string> - <string name="manage_permissions_decor_title" msgid="4823785025722958092">"Odobrenja za aplikacije"</string> - <string name="permission_apps_decor_title" msgid="3644363529649579576">"Odobrenja za aplikaciju: <xliff:g id="PERMISSION">%1$s</xliff:g>"</string> - <string name="additional_permissions_decor_title" msgid="7000432624396037882">"Dodatna odobrenja"</string> - <string name="system_apps_decor_title" msgid="5292119639812561805">"Odobrenja za aplikaciju: <xliff:g id="PERMISSION">%1$s</xliff:g>"</string> </resources> diff --git a/res/values-bs-rBA/strings.xml b/res/values-bs-rBA/strings.xml index e1cde366..f025402b 100644 --- a/res/values-bs-rBA/strings.xml +++ b/res/values-bs-rBA/strings.xml @@ -21,7 +21,7 @@ <string name="install" msgid="5896438203900042068">"Instaliraj"</string> <string name="done" msgid="3889387558374211719">"Gotovo"</string> <string name="security_settings_desc" msgid="2706691034244052604">"Dozvolite ovoj aplikaciji da:"</string> - <string name="cancel" msgid="8360346460165114585">"Otkaži"</string> + <string name="cancel" msgid="8360346460165114585">"Prekini"</string> <string name="unknown" msgid="4742479012767208045">"Nepoznato"</string> <string name="installing" msgid="8613631001631998372">"Instalacija u toku..."</string> <string name="install_done" msgid="3682715442154357097">"Aplikacija je instalirana."</string> @@ -70,8 +70,7 @@ <string name="uninstall_failed" msgid="631122574306299512">"Uklanjanje nije uspjelo."</string> <string name="uninstall_failed_device_policy_manager" msgid="3493789239037852035">"Ovaj paket ne možete ukloniti jer je aktivni administrator uređaja."</string> <string name="uninstall_failed_device_policy_manager_of_user" msgid="4466062391552204291">"Ne može se ukloniti jer ovaj paket funkcionira kao aktivni administrator uređaja za korisnika <xliff:g id="USERNAME">%1$s</xliff:g>."</string> - <string name="uninstall_all_blocked_profile_owner" msgid="3544933038594382346">"Ova aplikacija je neophodna nekim korisnicima ili profilima, a kod ostalih je deinstalirana"</string> - <string name="uninstall_blocked_profile_owner" msgid="6912141045528994954">"Ova aplikacija je potrebna za vaš profil i ne može se deinstalirati."</string> + <string name="uninstall_blocked_profile_owner" msgid="5154358281537910006">"Ova aplikacija je neophodna za neke korisnike ili profile, a za druge je deinstalirana"</string> <string name="uninstall_blocked_device_owner" msgid="7074175526413453063">"Ova aplikacija je potrebna administratoru vašeg uređaja i ne može se ukloniti."</string> <string name="manage_device_administrators" msgid="891392489300312370">"Upravljanje administratorima uređaja"</string> <string name="manage_users" msgid="3125018886835668847">"Upravljanje korisnicima"</string> @@ -86,7 +85,7 @@ <string name="grant_dialog_button_deny" msgid="2176510645406614340">"Odbij"</string> <string name="grant_dialog_button_deny_anyway" msgid="847960499284125250">"Odbij svakako"</string> <string name="current_permission_template" msgid="6378304249516652817">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g> od <xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> - <string name="permission_warning_template" msgid="7332275268559121742">"Želite li dozvoliti aplikaciji <b><xliff:g id="APP_NAME">%1$s</xliff:g></b> da <xliff:g id="ACTION">%2$s</xliff:g>?"</string> + <string name="permission_warning_template" msgid="5209102765005869454">"Želite li dozvoliti da aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="ACTION">%2$s</xliff:g>?"</string> <string name="grant_dialog_button_allow" msgid="4616529495342337095">"Dozvoli"</string> <string name="app_permissions_breadcrumb" msgid="3390836200791539264">"Aplikacije"</string> <string name="app_permissions" msgid="3146758905824597178">"Odobrenja za aplikacije"</string> diff --git a/res/values-es-watch/strings.xml b/res/values-es-watch/strings.xml index 198c266f..6d0c8af8 100644 --- a/res/values-es-watch/strings.xml +++ b/res/values-es-watch/strings.xml @@ -21,5 +21,5 @@ <string name="preference_show_system_apps" msgid="7042886929865431207">"Mostrar aplicaciones del sistema"</string> <string name="permission_summary_enforced_by_policy" msgid="9002523259681588936">"No se puede cambiar"</string> <string name="generic_enabled" msgid="5221039415230005888">"HABILITADO"</string> - <string name="generic_disabled" msgid="576339310027394867">"INHABILITADO"</string> + <string name="generic_disabled" msgid="576339310027394867">"INHABILITADA"</string> </resources> diff --git a/res/values-eu-rES-television/strings.xml b/res/values-eu-rES-television/strings.xml index 907cf7d8..6b8a22d3 100644 --- a/res/values-eu-rES-television/strings.xml +++ b/res/values-eu-rES-television/strings.xml @@ -16,7 +16,7 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="grant_dialog_button_deny_dont_ask_again" msgid="5694574989758145558">"Ukatu eta ez galdetu berriro"</string> + <string name="grant_dialog_button_deny_dont_ask_again" msgid="5694574989758145558">"Baztertu eta ez galdetu berriro"</string> <string name="grant_dialog_how_to_change" msgid="615414835189256888">"Hori geroago alda dezakezu Ezarpenak > Aplikazioak atalean"</string> <string name="current_permission_template" msgid="4793247012451594523">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g>/<xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> <string name="preference_show_system_apps" msgid="7330308025768596149">"Erakutsi sistema-aplikazioak"</string> diff --git a/res/values-fa-watch/strings.xml b/res/values-fa-watch/strings.xml index 5dc5a3f6..15029959 100644 --- a/res/values-fa-watch/strings.xml +++ b/res/values-fa-watch/strings.xml @@ -21,5 +21,5 @@ <string name="preference_show_system_apps" msgid="7042886929865431207">"نمایش برنامههای سیستم"</string> <string name="permission_summary_enforced_by_policy" msgid="9002523259681588936">"نمیتواند تغییر کند"</string> <string name="generic_enabled" msgid="5221039415230005888">"فعال شد"</string> - <string name="generic_disabled" msgid="576339310027394867">"غیرفعال"</string> + <string name="generic_disabled" msgid="576339310027394867">"غیرفعال شد"</string> </resources> diff --git a/res/values-gu-rIN-television/strings.xml b/res/values-gu-rIN-television/strings.xml index b0a40b6c..cab714d4 100644 --- a/res/values-gu-rIN-television/strings.xml +++ b/res/values-gu-rIN-television/strings.xml @@ -19,7 +19,7 @@ <string name="grant_dialog_button_deny_dont_ask_again" msgid="5694574989758145558">"નકારો અને ફરીથી પૂછશો નહીં"</string> <string name="grant_dialog_how_to_change" msgid="615414835189256888">"તમે પછીથી આને સેટિંગ્સ > એપ્લિકેશન્સમાં બદલી શકો છો"</string> <string name="current_permission_template" msgid="4793247012451594523">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g> / <xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> - <string name="preference_show_system_apps" msgid="7330308025768596149">"સિસ્ટમ ઍપ્લિકેશનો બતાવો"</string> + <string name="preference_show_system_apps" msgid="7330308025768596149">"સિસ્ટમ એપ્લિકેશનો બતાવો"</string> <string name="app_permissions_decor_title" msgid="1461057434211920209">"ઍપ્લિકેશન પરવાનગીઓ"</string> <string name="manage_permissions_decor_title" msgid="4823785025722958092">"ઍપ્લિકેશન પરવાનગીઓ"</string> <string name="permission_apps_decor_title" msgid="3644363529649579576">"<xliff:g id="PERMISSION">%1$s</xliff:g> પરવાનગીઓ"</string> diff --git a/res/values-gu-rIN-watch/strings.xml b/res/values-gu-rIN-watch/strings.xml index 10ee4306..3530b6a9 100644 --- a/res/values-gu-rIN-watch/strings.xml +++ b/res/values-gu-rIN-watch/strings.xml @@ -18,7 +18,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="grant_dialog_button_deny_dont_ask_again" msgid="5828565432145544298">"નકારો, ફરીથી પૂછશો નહીં"</string> <string name="current_permission_template" msgid="6691830243038105737">"<xliff:g id="CURRENT_PERMISSION_INDEX">%1$s</xliff:g> / <xliff:g id="PERMISSION_COUNT">%2$s</xliff:g>"</string> - <string name="preference_show_system_apps" msgid="7042886929865431207">"સિસ્ટમ ઍપ્લિકેશનો બતાવો"</string> + <string name="preference_show_system_apps" msgid="7042886929865431207">"સિસ્ટમ એપ્લિકેશનો બતાવો"</string> <string name="permission_summary_enforced_by_policy" msgid="9002523259681588936">"બદલી શકતાં નથી"</string> <string name="generic_enabled" msgid="5221039415230005888">"સક્ષમ કરેલ"</string> <string name="generic_disabled" msgid="576339310027394867">"અક્ષમ કરેલ"</string> diff --git a/src/com/android/packageinstaller/wear/InstallTask.java b/src/com/android/packageinstaller/wear/InstallTask.java new file mode 100644 index 00000000..53a460dc --- /dev/null +++ b/src/com/android/packageinstaller/wear/InstallTask.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.packageinstaller.wear; + +import android.content.Context; +import android.content.IntentSender; +import android.content.pm.PackageInstaller; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.text.TextUtils; +import android.util.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Task that installs an APK. This must not be called on the main thread. + * This code is based off the Finsky/Wearsky implementation + */ +public class InstallTask { + private static final String TAG = "InstallTask"; + + private static final int DEFAULT_BUFFER_SIZE = 8192; + + private final Context mContext; + private String mPackageName; + private ParcelFileDescriptor mParcelFileDescriptor; + private PackageInstallerImpl.InstallListener mCallback; + private PackageInstaller.Session mSession; + private IntentSender mCommitCallback; + + private Exception mException = null; + private int mErrorCode = 0; + private String mErrorDesc = null; + + public InstallTask(Context context, String packageName, + ParcelFileDescriptor parcelFileDescriptor, + PackageInstallerImpl.InstallListener callback, PackageInstaller.Session session, + IntentSender commitCallback) { + mContext = context; + mPackageName = packageName; + mParcelFileDescriptor = parcelFileDescriptor; + mCallback = callback; + mSession = session; + mCommitCallback = commitCallback; + } + + public boolean isError() { + return mErrorCode != InstallerConstants.STATUS_SUCCESS || !TextUtils.isEmpty(mErrorDesc); + } + + public void execute() { + if (Looper.myLooper() == Looper.getMainLooper()) { + throw new IllegalStateException("This method cannot be called from the UI thread."); + } + + OutputStream sessionStream = null; + try { + sessionStream = mSession.openWrite(mPackageName, 0, -1); + + // 2b: Stream the asset to the installer. Note: + // Note: writeToOutputStreamFromAsset() always safely closes the input stream + writeToOutputStreamFromAsset(sessionStream); + mSession.fsync(sessionStream); + } catch (Exception e) { + mException = e; + mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM; + mErrorDesc = "Could not write to stream"; + } finally { + if (sessionStream != null) { + // 2c: close output stream + try { + sessionStream.close(); + } catch (Exception e) { + // Ignore otherwise + if (mException == null) { + mException = e; + mErrorCode = InstallerConstants.ERROR_INSTALL_CLOSE_STREAM; + mErrorDesc = "Could not close session stream"; + } + } + } + } + + if (mErrorCode != InstallerConstants.STATUS_SUCCESS) { + // An error occurred, we're done + Log.e(TAG, "Exception while installing " + mPackageName + ": " + mErrorCode + ", " + + mErrorDesc + ", " + mException); + mSession.close(); + mCallback.installFailed(mErrorCode, "[" + mPackageName + "]" + mErrorDesc); + } else { + // 3. Commit the session (this actually installs it.) Session map + // will be cleaned up in the callback. + mCallback.installBeginning(); + mSession.commit(mCommitCallback); + mSession.close(); + } + } + + /** + * {@code PackageInstaller} works with streams. Get the {@code FileDescriptor} + * corresponding to the {@code Asset} and then write the contents into an + * {@code OutputStream} that is passed in. + * <br> + * The {@code FileDescriptor} is closed but the {@code OutputStream} is not closed. + */ + private boolean writeToOutputStreamFromAsset(OutputStream outputStream) { + if (outputStream == null) { + mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM_EXCEPTION; + mErrorDesc = "Got a null OutputStream."; + return false; + } + + if (mParcelFileDescriptor == null || mParcelFileDescriptor.getFileDescriptor() == null) { + mErrorCode = InstallerConstants.ERROR_COULD_NOT_GET_FD; + mErrorDesc = "Could not get FD"; + return false; + } + + InputStream inputStream = null; + try { + byte[] inputBuf = new byte[DEFAULT_BUFFER_SIZE]; + int bytesRead; + inputStream = new ParcelFileDescriptor.AutoCloseInputStream(mParcelFileDescriptor); + + while ((bytesRead = inputStream.read(inputBuf)) > -1) { + if (bytesRead > 0) { + outputStream.write(inputBuf, 0, bytesRead); + } + } + + outputStream.flush(); + } catch (IOException e) { + mErrorCode = InstallerConstants.ERROR_INSTALL_APK_COPY_FAILURE; + mErrorDesc = "Reading from Asset FD or writing to temp file failed: " + e; + return false; + } finally { + safeClose(inputStream); + } + + return true; + } + + /** + * Quietly close a closeable resource (e.g. a stream or file). The input may already + * be closed and it may even be null. + */ + public static void safeClose(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } catch (IOException ioe) { + // Catch and discard the error + } + } + } +}
\ No newline at end of file diff --git a/src/com/android/packageinstaller/wear/InstallerConstants.java b/src/com/android/packageinstaller/wear/InstallerConstants.java new file mode 100644 index 00000000..3daf3d83 --- /dev/null +++ b/src/com/android/packageinstaller/wear/InstallerConstants.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.packageinstaller.wear; + +/** + * Constants for Installation / Uninstallation requests. + * Using the same values as Finsky/Wearsky code for consistency in user analytics of failures + */ +public class InstallerConstants { + /** Request succeeded */ + public static final int STATUS_SUCCESS = 0; + + /** + * The new PackageInstaller also returns a small set of less granular error codes, which + * we'll remap to the range -500 and below to keep away from existing installer codes + * (which run from -1 to -110). + */ + public final static int ERROR_PACKAGEINSTALLER_BASE = -500; + + public static final int ERROR_COULD_NOT_GET_FD = -603; + /** This node is not targeted by this request. */ + + /** The install did not complete because could not create PackageInstaller session */ + public final static int ERROR_INSTALL_CREATE_SESSION = -612; + /** The install did not complete because could not open PackageInstaller session */ + public final static int ERROR_INSTALL_OPEN_SESSION = -613; + /** The install did not complete because could not open PackageInstaller output stream */ + public final static int ERROR_INSTALL_OPEN_STREAM = -614; + /** The install did not complete because of an exception while streaming bytes */ + public final static int ERROR_INSTALL_COPY_STREAM_EXCEPTION = -615; + /** The install did not complete because of an unexpected exception from PackageInstaller */ + public final static int ERROR_INSTALL_SESSION_EXCEPTION = -616; + /** The install did not complete because of an unexpected userActionRequired callback */ + public final static int ERROR_INSTALL_USER_ACTION_REQUIRED = -617; + /** The install did not complete because of an unexpected broadcast (missing fields) */ + public final static int ERROR_INSTALL_MALFORMED_BROADCAST = -618; + /** The install did not complete because of an error while copying from downloaded file */ + public final static int ERROR_INSTALL_APK_COPY_FAILURE = -619; + /** The install did not complete because of an error while copying to the PackageInstaller + * output stream */ + public final static int ERROR_INSTALL_COPY_STREAM = -620; + /** The install did not complete because of an error while closing the PackageInstaller + * output stream */ + public final static int ERROR_INSTALL_CLOSE_STREAM = -621; +}
\ No newline at end of file diff --git a/src/com/android/packageinstaller/wear/PackageInstallerFactory.java b/src/com/android/packageinstaller/wear/PackageInstallerFactory.java new file mode 100644 index 00000000..bdc22cf0 --- /dev/null +++ b/src/com/android/packageinstaller/wear/PackageInstallerFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.packageinstaller.wear; + +import android.content.Context; + +/** + * Factory that creates a Package Installer. + */ +public class PackageInstallerFactory { + private static PackageInstallerImpl sPackageInstaller; + + /** + * Return the PackageInstaller shared object. {@code init} should have already been called. + */ + public synchronized static PackageInstallerImpl getPackageInstaller(Context context) { + if (sPackageInstaller == null) { + sPackageInstaller = new PackageInstallerImpl(context); + } + return sPackageInstaller; + } +}
\ No newline at end of file diff --git a/src/com/android/packageinstaller/wear/PackageInstallerImpl.java b/src/com/android/packageinstaller/wear/PackageInstallerImpl.java new file mode 100644 index 00000000..3dee7817 --- /dev/null +++ b/src/com/android/packageinstaller/wear/PackageInstallerImpl.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.packageinstaller.wear; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.pm.PackageInstaller; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of package manager installation using modern PackageInstaller api. + * + * Heavily copied from Wearsky/Finsky implementation + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class PackageInstallerImpl { + private static final String TAG = "PackageInstallerImpl"; + + /** Intent actions used for broadcasts from PackageInstaller back to the local receiver */ + private static final String ACTION_INSTALL_COMMIT = + "com.android.vending.INTENT_PACKAGE_INSTALL_COMMIT"; + + private final Context mContext; + private final PackageInstaller mPackageInstaller; + private final Map<String, PackageInstaller.SessionInfo> mSessionInfoMap; + private final Map<String, PackageInstaller.Session> mOpenSessionMap; + + public PackageInstallerImpl(Context context) { + mContext = context.getApplicationContext(); + mPackageInstaller = mContext.getPackageManager().getPackageInstaller(); + + // Capture a map of known sessions + // This list will be pruned a bit later (stale sessions will be canceled) + mSessionInfoMap = new HashMap<String, PackageInstaller.SessionInfo>(); + List<PackageInstaller.SessionInfo> mySessions = mPackageInstaller.getMySessions(); + for (int i = 0; i < mySessions.size(); i++) { + PackageInstaller.SessionInfo sessionInfo = mySessions.get(i); + String packageName = sessionInfo.getAppPackageName(); + PackageInstaller.SessionInfo oldInfo = mSessionInfoMap.put(packageName, sessionInfo); + + // Checking for old info is strictly for logging purposes + if (oldInfo != null) { + Log.w(TAG, "Multiple sessions for " + packageName + " found. Removing " + oldInfo + .getSessionId() + " & keeping " + mySessions.get(i).getSessionId()); + } + } + mOpenSessionMap = new HashMap<String, PackageInstaller.Session>(); + } + + /** + * This callback will be made after an installation attempt succeeds or fails. + */ + public interface InstallListener { + /** + * This callback signals that preflight checks have succeeded and installation + * is beginning. + */ + void installBeginning(); + + /** + * This callback signals that installation has completed. + */ + void installSucceeded(); + + /** + * This callback signals that installation has failed. + */ + void installFailed(int errorCode, String errorDesc); + } + + /** + * This is a placeholder implementation that bundles an entire "session" into a single + * call. This will be replaced by more granular versions that allow longer session lifetimes, + * download progress tracking, etc. + * + * This must not be called on main thread. + */ + public void install(final String packageName, ParcelFileDescriptor parcelFileDescriptor, + final InstallListener callback) { + // 0. Generic try/catch block because I am not really sure what exceptions (other than + // IOException) might be thrown by PackageInstaller and I want to handle them + // at least slightly gracefully. + try { + // 1. Create or recover a session, and open it + // Try recovery first + PackageInstaller.Session session = null; + PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); + if (sessionInfo != null) { + // See if it's openable, or already held open + session = getSession(packageName); + } + // If open failed, or there was no session, create a new one and open it. + // If we cannot create or open here, the failure is terminal. + if (session == null) { + try { + innerCreateSession(packageName); + } catch (IOException ioe) { + Log.e(TAG, "Can't create session for " + packageName + ": " + ioe.getMessage()); + callback.installFailed(InstallerConstants.ERROR_INSTALL_CREATE_SESSION, + "Could not create session"); + mSessionInfoMap.remove(packageName); + return; + } + sessionInfo = mSessionInfoMap.get(packageName); + try { + session = mPackageInstaller.openSession(sessionInfo.getSessionId()); + mOpenSessionMap.put(packageName, session); + } catch (SecurityException se) { + Log.e(TAG, "Can't open session for " + packageName + ": " + se.getMessage()); + callback.installFailed(InstallerConstants.ERROR_INSTALL_OPEN_SESSION, + "Can't open session"); + mSessionInfoMap.remove(packageName); + return; + } + } + + // 2. Launch task to handle file operations. + InstallTask task = new InstallTask( mContext, packageName, parcelFileDescriptor, + callback, session, + getCommitCallback(packageName, sessionInfo.getSessionId(), callback)); + task.execute(); + if (task.isError()) { + cancelSession(sessionInfo.getSessionId(), packageName); + } + } catch (Exception e) { + Log.e(TAG, "Unexpected exception while installing " + packageName); + callback.installFailed(InstallerConstants.ERROR_INSTALL_SESSION_EXCEPTION, + "Unexpected exception while installing " + packageName); + } + } + + /** + * Retrieve an existing session. Will open if needed, but does not attempt to create. + */ + private PackageInstaller.Session getSession(String packageName) { + // Check for already-open session + PackageInstaller.Session session = mOpenSessionMap.get(packageName); + if (session != null) { + try { + // Probe the session to ensure that it's still open. This may or may not + // throw (if non-open), but it may serve as a canary for stale sessions. + session.getNames(); + return session; + } catch (IOException ioe) { + Log.e(TAG, "Stale open session for " + packageName + ": " + ioe.getMessage()); + mOpenSessionMap.remove(packageName); + } catch (SecurityException se) { + Log.e(TAG, "Stale open session for " + packageName + ": " + se.getMessage()); + mOpenSessionMap.remove(packageName); + } + } + // Check to see if this is a known session + PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); + if (sessionInfo == null) { + return null; + } + // Try to open it. If we fail here, assume that the SessionInfo was stale. + try { + session = mPackageInstaller.openSession(sessionInfo.getSessionId()); + } catch (SecurityException se) { + Log.w(TAG, "SessionInfo was stale for " + packageName + " - deleting info"); + mSessionInfoMap.remove(packageName); + return null; + } catch (IOException ioe) { + Log.w(TAG, "IOException opening old session for " + ioe.getMessage() + + " - deleting info"); + mSessionInfoMap.remove(packageName); + return null; + } + mOpenSessionMap.put(packageName, session); + return session; + } + + /** This version throws an IOException when the session cannot be created */ + private void innerCreateSession(String packageName) throws IOException { + if (mSessionInfoMap.containsKey(packageName)) { + Log.w(TAG, "Creating session for " + packageName + " when one already exists"); + return; + } + PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + params.setAppPackageName(packageName); + + // IOException may be thrown at this point + int sessionId = mPackageInstaller.createSession(params); + PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); + mSessionInfoMap.put(packageName, sessionInfo); + } + + /** + * Cancel a session based on its sessionId. Package name is for logging only. + */ + private void cancelSession(int sessionId, String packageName) { + // Close if currently held open + closeSession(packageName); + // Remove local record + mSessionInfoMap.remove(packageName); + try { + mPackageInstaller.abandonSession(sessionId); + } catch (SecurityException se) { + // The session no longer exists, so we can exit quietly. + return; + } + } + + /** + * Close a session if it happens to be held open. + */ + private void closeSession(String packageName) { + PackageInstaller.Session session = mOpenSessionMap.remove(packageName); + if (session != null) { + // Unfortunately close() is not idempotent. Try our best to make this safe. + try { + session.close(); + } catch (Exception e) { + Log.w(TAG, "Unexpected error closing session for " + packageName + ": " + + e.getMessage()); + } + } + } + + /** + * Creates a commit callback for the package install that's underway. This will be called + * some time after calling session.commit() (above). + */ + private IntentSender getCommitCallback(final String packageName, final int sessionId, + final InstallListener callback) { + // Create a single-use broadcast receiver + BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mContext.unregisterReceiver(this); + handleCommitCallback(intent, packageName, sessionId, callback); + } + }; + // Create a matching intent-filter and register the receiver + String action = ACTION_INSTALL_COMMIT + "." + packageName; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(action); + mContext.registerReceiver(broadcastReceiver, intentFilter); + + // Create a matching PendingIntent and use it to generate the IntentSender + Intent broadcastIntent = new Intent(action); + PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(), + broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); + return pendingIntent.getIntentSender(); + } + + /** + * Examine the extras to determine information about the package update/install, decode + * the result, and call the appropriate callback. + * + * @param intent The intent, which the PackageInstaller will have added Extras to + * @param packageName The package name we created the receiver for + * @param sessionId The session Id we created the receiver for + * @param callback The callback to report success/failure to + */ + private void handleCommitCallback(Intent intent, String packageName, int sessionId, + InstallListener callback) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Installation of " + packageName + " finished with extras " + + intent.getExtras()); + } + String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); + int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE); + if (status == PackageInstaller.STATUS_SUCCESS) { + cancelSession(sessionId, packageName); + callback.installSucceeded(); + } else if (status == -1 /*PackageInstaller.STATUS_USER_ACTION_REQUIRED*/) { + // TODO - use the constant when the correct/final name is in the SDK + // TODO This is unexpected, so we are treating as failure for now + cancelSession(sessionId, packageName); + callback.installFailed(InstallerConstants.ERROR_INSTALL_USER_ACTION_REQUIRED, + "Unexpected: user action required"); + } else { + cancelSession(sessionId, packageName); + int errorCode = getPackageManagerErrorCode(status); + Log.e(TAG, "Error " + errorCode + " while installing " + packageName + ": " + + statusMessage); + callback.installFailed(errorCode, null); + } + } + + private int getPackageManagerErrorCode(int status) { + // This is a hack: because PackageInstaller now reports error codes + // with small positive values, we need to remap them into a space + // that is more compatible with the existing package manager error codes. + // See https://sites.google.com/a/google.com/universal-store/documentation + // /android-client/download-error-codes + int errorCode; + if (status == Integer.MIN_VALUE) { + errorCode = InstallerConstants.ERROR_INSTALL_MALFORMED_BROADCAST; + } else { + errorCode = InstallerConstants.ERROR_PACKAGEINSTALLER_BASE - status; + } + return errorCode; + } +}
\ No newline at end of file diff --git a/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/src/com/android/packageinstaller/wear/WearPackageInstallerService.java index 7387ed2a..22e8fc68 100644 --- a/src/com/android/packageinstaller/wear/WearPackageInstallerService.java +++ b/src/com/android/packageinstaller/wear/WearPackageInstallerService.java @@ -22,7 +22,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.FeatureInfo; import android.content.pm.IPackageDeleteObserver; -import android.content.pm.IPackageInstallObserver; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageParser; @@ -98,13 +97,6 @@ public class WearPackageInstallerService extends Service { private static final String SHOW_PERMS_SERVICE_CLASS = "com.google.android.clockwork.packagemanager.ShowPermsService"; - /** - * Normally sent by the Play store (See http://go/playstore-gms_updated), we instead - * broadcast, ourselves. http://b/17387718 - */ - private static final String GMS_UPDATED_BROADCAST = "com.google.android.gms.GMS_UPDATED"; - public static final String GMS_PACKAGE_NAME = "com.google.android.gms"; - private final int START_INSTALL = 1; private final int START_UNINSTALL = 2; @@ -233,6 +225,12 @@ public class WearPackageInstallerService extends Service { Log.d(TAG, "Replacing package:" + packageName); } } + // TODO(28021618): This was left as a temp file due to the fact that this code is being + // deprecated and that we need the bare minimum to continue working moving forward + // If this code is used as reference, this permission logic might want to be + // reworked to use a stream instead of a file so that we don't need to write a + // file at all. Note that there might be some trickiness with opening a stream + // for multiple users. ParcelFileDescriptor parcelFd = getContentResolver() .openFileDescriptor(assetUri, "r"); tempFile = WearPackageUtil.getFileFromFd(WearPackageInstallerService.this, @@ -318,9 +316,9 @@ public class WearPackageInstallerService extends Service { } // Finally install the package. - pm.installPackage(Uri.fromFile(tempFile), - new PackageInstallObserver(this, lock, startId, packageName), - installFlags, packageName); + ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(assetUri, "r"); + PackageInstallerFactory.getPackageInstaller(this).install(packageName, fd, + new PackageInstallListener(this, lock, startId, packageName)); messageSent = true; Log.i(TAG, "Sent installation request for " + packageName); @@ -338,6 +336,10 @@ public class WearPackageInstallerService extends Service { } } + // TODO: This was left using the old PackageManager API due to the fact that this code is being + // deprecated and that we need the bare minimum to continue working moving forward + // If this code is used as reference, this logic should be reworked to use the new + // PackageInstaller APIs similar to how installPackage was reworked private void uninstallPackage(Bundle argsBundle) { int startId = WearPackageArgs.getStartId(argsBundle); final String packageName = WearPackageArgs.getPackageName(argsBundle); @@ -558,12 +560,12 @@ public class WearPackageInstallerService extends Service { } } - private class PackageInstallObserver extends IPackageInstallObserver.Stub { + private class PackageInstallListener implements PackageInstallerImpl.InstallListener { private Context mContext; private PowerManager.WakeLock mWakeLock; private int mStartId; private String mApplicationPackageName; - private PackageInstallObserver(Context context, PowerManager.WakeLock wakeLock, + private PackageInstallListener(Context context, PowerManager.WakeLock wakeLock, int startId, String applicationPackageName) { mContext = context; mWakeLock = wakeLock; @@ -571,35 +573,33 @@ public class WearPackageInstallerService extends Service { mApplicationPackageName = applicationPackageName; } - public void packageInstalled(String packageName, int returnCode) { - try { - // If installation failed, bail out and remove the ShowPermsStore entry - if (returnCode < 0) { - Log.e(TAG, "Package install failed " + mApplicationPackageName - + ", returnCode " + returnCode); - WearPackageUtil.removeFromPermStore(mContext, mApplicationPackageName); - return; - } + @Override + public void installBeginning() { + Log.i(TAG, "Package " + mApplicationPackageName + " is being installed."); + } - Log.i(TAG, "Package " + packageName + " was installed."); + @Override + public void installSucceeded() { + try { + Log.i(TAG, "Package " + mApplicationPackageName + " was installed."); // Delete tempFile from the file system. - File tempFile = WearPackageUtil.getTemporaryFile(mContext, packageName); + File tempFile = WearPackageUtil.getTemporaryFile(mContext, mApplicationPackageName); if (tempFile != null) { tempFile.delete(); } - - // Broadcast the "UPDATED" gmscore intent, normally sent by play store. - // TODO: Remove this broadcast if/when we get the play store to do this for us. - if (GMS_PACKAGE_NAME.equals(packageName)) { - Intent gmsInstalledIntent = new Intent(GMS_UPDATED_BROADCAST); - gmsInstalledIntent.setPackage(GMS_PACKAGE_NAME); - mContext.sendBroadcast(gmsInstalledIntent); - } } finally { finishService(mWakeLock, mStartId); } } + + @Override + public void installFailed(int errorCode, String errorDesc) { + Log.e(TAG, "Package install failed " + mApplicationPackageName + + ", errorCode " + errorCode); + WearPackageUtil.removeFromPermStore(mContext, mApplicationPackageName); + finishService(mWakeLock, mStartId); + } } private class PackageDeleteObserver extends IPackageDeleteObserver.Stub { |