/* * 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.packageinstaller.wear; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.FeatureInfo; import android.content.pm.IPackageDeleteObserver; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageParser; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.Process; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import com.android.packageinstaller.DeviceUtils; import com.android.packageinstaller.PackageUtil; import com.android.packageinstaller.R; import java.io.File; import java.io.FileNotFoundException; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Service that will install/uninstall packages. It will check for permissions and features as well. * * ----------- * * Debugging information: * * Install Action example: * adb shell am startservice -a com.android.packageinstaller.wear.INSTALL_PACKAGE \ * -d package://com.google.android.gms \ * --eu com.google.android.clockwork.EXTRA_ASSET_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/wearable/com.google.android.gms/apk \ * --es android.intent.extra.INSTALLER_PACKAGE_NAME com.google.android.gms \ * --ez com.google.android.clockwork.EXTRA_CHECK_PERMS false \ * --eu com.google.android.clockwork.EXTRA_PERM_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/permissions \ * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService * * Uninstall Action example: * adb shell am startservice -a com.android.packageinstaller.wear.UNINSTALL_PACKAGE \ * -d package://com.google.android.gms \ * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService * * Retry GMS: * adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \ * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService */ public class WearPackageInstallerService extends Service { private static final String TAG = "WearPkgInstallerService"; private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall"; private final int START_INSTALL = 1; private final int START_UNINSTALL = 2; private int mInstallNotificationId = 1; private final Map mNotifIdMap = new ArrayMap<>(); private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { switch (msg.what) { case START_INSTALL: installPackage(msg.getData()); break; case START_UNINSTALL: uninstallPackage(msg.getData()); break; } } } private ServiceHandler mServiceHandler; private NotificationChannel mNotificationChannel; private static volatile PowerManager.WakeLock lockStatic = null; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); HandlerThread thread = new HandlerThread("PackageInstallerThread", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mServiceHandler = new ServiceHandler(thread.getLooper()); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (!DeviceUtils.isWear(this)) { Log.w(TAG, "Not running on wearable."); finishService(null, startId); return START_NOT_STICKY; } if (intent == null) { Log.w(TAG, "Got null intent."); finishService(null, startId); return START_NOT_STICKY; } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Got install/uninstall request " + intent); } Uri packageUri = intent.getData(); if (packageUri == null) { Log.e(TAG, "No package URI in intent"); finishService(null, startId); return START_NOT_STICKY; } final String packageName = WearPackageUtil.getSanitizedPackageName(packageUri); if (packageName == null) { Log.e(TAG, "Invalid package name in URI (expected package:): " + packageUri); finishService(null, startId); return START_NOT_STICKY; } PowerManager.WakeLock lock = getLock(this.getApplicationContext()); if (!lock.isHeld()) { lock.acquire(); } Bundle intentBundle = intent.getExtras(); if (intentBundle == null) { intentBundle = new Bundle(); } WearPackageArgs.setStartId(intentBundle, startId); WearPackageArgs.setPackageName(intentBundle, packageName); String notifTitle; if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) { Message msg = mServiceHandler.obtainMessage(START_INSTALL); msg.setData(intentBundle); mServiceHandler.sendMessage(msg); notifTitle = getString(R.string.installing); } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) { Message msg = mServiceHandler.obtainMessage(START_UNINSTALL); msg.setData(intentBundle); mServiceHandler.sendMessage(msg); notifTitle = getString(R.string.uninstalling); } else { Log.e(TAG, "Unknown action : " + intent.getAction()); finishService(null, startId); return START_NOT_STICKY; } Pair notifPair = buildNotification(packageName, notifTitle); startForeground(notifPair.first, notifPair.second); return START_NOT_STICKY; } private void installPackage(Bundle argsBundle) { int startId = WearPackageArgs.getStartId(argsBundle); final String packageName = WearPackageArgs.getPackageName(argsBundle); final Uri assetUri = WearPackageArgs.getAssetUri(argsBundle); final Uri permUri = WearPackageArgs.getPermUri(argsBundle); boolean checkPerms = WearPackageArgs.checkPerms(argsBundle); boolean skipIfSameVersion = WearPackageArgs.skipIfSameVersion(argsBundle); int companionSdkVersion = WearPackageArgs.getCompanionSdkVersion(argsBundle); int companionDeviceVersion = WearPackageArgs.getCompanionDeviceVersion(argsBundle); String compressionAlg = WearPackageArgs.getCompressionAlg(argsBundle); boolean skipIfLowerVersion = WearPackageArgs.skipIfLowerVersion(argsBundle); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Installing package: " + packageName + ", assetUri: " + assetUri + ",permUri: " + permUri + ", startId: " + startId + ", checkPerms: " + checkPerms + ", skipIfSameVersion: " + skipIfSameVersion + ", compressionAlg: " + compressionAlg + ", companionSdkVersion: " + companionSdkVersion + ", companionDeviceVersion: " + companionDeviceVersion + ", skipIfLowerVersion: " + skipIfLowerVersion); } final PackageManager pm = getPackageManager(); File tempFile = null; int installFlags = 0; PowerManager.WakeLock lock = getLock(this.getApplicationContext()); boolean messageSent = false; try { PackageInfo existingPkgInfo = null; try { existingPkgInfo = pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER | PackageManager.GET_PERMISSIONS); if (existingPkgInfo != null) { installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; } } catch (PackageManager.NameNotFoundException e) { // Ignore this exception. We could not find the package, will treat as a new // installation. } if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) { if (Log.isLoggable(TAG, Log.DEBUG)) { 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, parcelFd, packageName, compressionAlg); if (tempFile == null) { Log.e(TAG, "Could not create a temp file from FD for " + packageName); return; } PackageParser.Package pkg = PackageUtil.getPackageInfo(this, tempFile); if (pkg == null) { Log.e(TAG, "Could not parse apk information for " + packageName); return; } if (!pkg.packageName.equals(packageName)) { Log.e(TAG, "Wearable Package Name has to match what is provided for " + packageName); return; } getLabelAndUpdateNotification(packageName, getString(R.string.installing_app, pkg.applicationInfo.loadLabel(pm))); List wearablePerms = pkg.requestedPermissions; // Log if the installed pkg has a higher version number. if (existingPkgInfo != null) { if (existingPkgInfo.versionCode == pkg.mVersionCode) { if (skipIfSameVersion) { Log.w(TAG, "Version number (" + pkg.mVersionCode + ") of new app is equal to existing app for " + packageName + "; not installing due to versionCheck"); return; } else { Log.w(TAG, "Version number of new app (" + pkg.mVersionCode + ") is equal to existing app for " + packageName); } } else if (existingPkgInfo.versionCode > pkg.mVersionCode) { if (skipIfLowerVersion) { // Starting in Feldspar, we are not going to allow downgrades of any app. Log.w(TAG, "Version number of new app (" + pkg.mVersionCode + ") is lower than existing app ( " + existingPkgInfo.versionCode + ") for " + packageName + "; not installing due to versionCheck"); return; } else { Log.w(TAG, "Version number of new app (" + pkg.mVersionCode + ") is lower than existing app ( " + existingPkgInfo.versionCode + ") for " + packageName); } } // Following the Android Phone model, we should only check for permissions for any // newly defined perms. if (existingPkgInfo.requestedPermissions != null) { for (int i = 0; i < existingPkgInfo.requestedPermissions.length; ++i) { // If the permission is granted, then we will not ask to request it again. if ((existingPkgInfo.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, existingPkgInfo.requestedPermissions[i] + " is already granted for " + packageName); } wearablePerms.remove(existingPkgInfo.requestedPermissions[i]); } } } } // Check that the wearable has all the features. boolean hasAllFeatures = true; if (pkg.reqFeatures != null) { for (FeatureInfo feature : pkg.reqFeatures) { if (feature.name != null && !pm.hasSystemFeature(feature.name) && (feature.flags & FeatureInfo.FLAG_REQUIRED) != 0) { Log.e(TAG, "Wearable does not have required feature: " + feature + " for " + packageName); hasAllFeatures = false; } } } if (!hasAllFeatures) { return; } // Check permissions on both the new wearable package and also on the already installed // wearable package. // If the app is targeting API level 23, we will also start a service in ClockworkHome // which will ultimately prompt the user to accept/reject permissions. if (checkPerms && !checkPermissions(pkg, companionSdkVersion, companionDeviceVersion, permUri, wearablePerms, tempFile)) { Log.w(TAG, "Wearable does not have enough permissions."); return; } // Finally install the package. 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); } catch (FileNotFoundException e) { Log.e(TAG, "Could not find the file with URI " + assetUri, e); } finally { if (!messageSent) { // Some error happened. If the message has been sent, we can wait for the observer // which will finish the service. if (tempFile != null) { tempFile.delete(); } finishService(lock, startId); } } } // 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); PowerManager.WakeLock lock = getLock(this.getApplicationContext()); final PackageManager pm = getPackageManager(); try { PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); getLabelAndUpdateNotification(packageName, getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm))); // Found package, send uninstall request. pm.deletePackage(packageName, new PackageDeleteObserver(lock, startId), PackageManager.DELETE_ALL_USERS); Log.i(TAG, "Sent delete request for " + packageName); } catch (IllegalArgumentException | PackageManager.NameNotFoundException e) { // Couldn't find the package, no need to call uninstall. Log.w(TAG, "Could not find package, not deleting " + packageName, e); finishService(lock, startId); } } private boolean checkPermissions(PackageParser.Package pkg, int companionSdkVersion, int companionDeviceVersion, Uri permUri, List wearablePermissions, File apkFile) { // Assumption: We are running on Android O. // If the Phone App is targeting M, all permissions may not have been granted to the phone // app. If the Wear App is then not targeting M, there may be permissions that are not // granted on the Phone app (by the user) right now and we cannot just grant it for the Wear // app. if (pkg.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M) { // Install the app if Wear App is ready for the new perms model. return true; } if (!doesWearHaveUngrantedPerms(pkg.packageName, permUri, wearablePermissions)) { // All permissions requested by the watch are already granted on the phone, no need // to do anything. return true; } // Log an error if Wear is targeting < 23 and phone is targeting >= 23. if (companionSdkVersion == 0 || companionSdkVersion >= Build.VERSION_CODES.M) { Log.e(TAG, "MNC: Wear app's targetSdkVersion should be at least 23, if " + "phone app is targeting at least 23, will continue."); } return false; } /** * Given a {@string packageName} corresponding to a phone app, query the provider for all the * perms that are granted. * * @return true if the Wear App has any perms that have not been granted yet on the phone side. * @return true if there is any error cases. */ private boolean doesWearHaveUngrantedPerms(String packageName, Uri permUri, List wearablePermissions) { if (permUri == null) { Log.e(TAG, "Permission URI is null"); // Pretend there is an ungranted permission to avoid installing for error cases. return true; } Cursor permCursor = getContentResolver().query(permUri, null, null, null, null); if (permCursor == null) { Log.e(TAG, "Could not get the cursor for the permissions"); // Pretend there is an ungranted permission to avoid installing for error cases. return true; } Set grantedPerms = new HashSet<>(); Set ungrantedPerms = new HashSet<>(); while(permCursor.moveToNext()) { // Make sure that the MatrixCursor returned by the ContentProvider has 2 columns and // verify their types. if (permCursor.getColumnCount() == 2 && Cursor.FIELD_TYPE_STRING == permCursor.getType(0) && Cursor.FIELD_TYPE_INTEGER == permCursor.getType(1)) { String perm = permCursor.getString(0); Integer granted = permCursor.getInt(1); if (granted == 1) { grantedPerms.add(perm); } else { ungrantedPerms.add(perm); } } } permCursor.close(); boolean hasUngrantedPerm = false; for (String wearablePerm : wearablePermissions) { if (!grantedPerms.contains(wearablePerm)) { hasUngrantedPerm = true; if (!ungrantedPerms.contains(wearablePerm)) { // This is an error condition. This means that the wearable has permissions that // are not even declared in its host app. This is a developer error. Log.e(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm + "\" that is not defined in the host application's manifest."); } else { Log.w(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm + "\" that is not granted in the host application."); } } } return hasUngrantedPerm; } private void finishService(PowerManager.WakeLock lock, int startId) { if (lock != null && lock.isHeld()) { lock.release(); } stopSelf(startId); } private synchronized PowerManager.WakeLock getLock(Context context) { if (lockStatic == null) { PowerManager mgr = (PowerManager) context.getSystemService(Context.POWER_SERVICE); lockStatic = mgr.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, context.getClass().getSimpleName()); lockStatic.setReferenceCounted(true); } return lockStatic; } private class PackageInstallListener implements PackageInstallerImpl.InstallListener { private Context mContext; private PowerManager.WakeLock mWakeLock; private int mStartId; private String mApplicationPackageName; private PackageInstallListener(Context context, PowerManager.WakeLock wakeLock, int startId, String applicationPackageName) { mContext = context; mWakeLock = wakeLock; mStartId = startId; mApplicationPackageName = applicationPackageName; } @Override public void installBeginning() { Log.i(TAG, "Package " + mApplicationPackageName + " is being installed."); } @Override public void installSucceeded() { try { Log.i(TAG, "Package " + mApplicationPackageName + " was installed."); // Delete tempFile from the file system. File tempFile = WearPackageUtil.getTemporaryFile(mContext, mApplicationPackageName); if (tempFile != null) { tempFile.delete(); } } finally { finishService(mWakeLock, mStartId); } } @Override public void installFailed(int errorCode, String errorDesc) { Log.e(TAG, "Package install failed " + mApplicationPackageName + ", errorCode " + errorCode); finishService(mWakeLock, mStartId); } } private class PackageDeleteObserver extends IPackageDeleteObserver.Stub { private PowerManager.WakeLock mWakeLock; private int mStartId; private PackageDeleteObserver(PowerManager.WakeLock wakeLock, int startId) { mWakeLock = wakeLock; mStartId = startId; } public void packageDeleted(String packageName, int returnCode) { try { if (returnCode >= 0) { Log.i(TAG, "Package " + packageName + " was uninstalled."); } else { Log.e(TAG, "Package uninstall failed " + packageName + ", returnCode " + returnCode); } } finally { finishService(mWakeLock, mStartId); } } } private synchronized Pair buildNotification(final String packageName, final String title) { int notifId; if (mNotifIdMap.containsKey(packageName)) { notifId = mNotifIdMap.get(packageName); } else { notifId = mInstallNotificationId++; mNotifIdMap.put(packageName, notifId); } if (mNotificationChannel == null) { mNotificationChannel = new NotificationChannel(WEAR_APPS_CHANNEL, getString(R.string.wear_app_channel), NotificationManager.IMPORTANCE_MIN); NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(mNotificationChannel); } return new Pair<>(notifId, new Notification.Builder(this, WEAR_APPS_CHANNEL) .setSmallIcon(R.drawable.ic_file_download) .setContentTitle(title) .build()); } private void getLabelAndUpdateNotification(String packageName, String title) { // Update notification since we have a label now. NotificationManager notificationManager = getSystemService(NotificationManager.class); Pair notifPair = buildNotification(packageName, title); notificationManager.notify(notifPair.first, notifPair.second); } }