diff options
Diffstat (limited to 'src/com/android/packageinstaller/wear/WearPackageInstallerService.java')
-rw-r--r-- | src/com/android/packageinstaller/wear/WearPackageInstallerService.java | 602 |
1 files changed, 602 insertions, 0 deletions
diff --git a/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/src/com/android/packageinstaller/wear/WearPackageInstallerService.java new file mode 100644 index 00000000..3874c0a4 --- /dev/null +++ b/src/com/android/packageinstaller/wear/WearPackageInstallerService.java @@ -0,0 +1,602 @@ +/* + * 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.Service; +import android.content.ComponentName; +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; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +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.text.TextUtils; +import android.util.Log; + +import com.android.packageinstaller.DeviceUtils; +import com.android.packageinstaller.PackageUtil; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +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 \ + * -t vnd.android.cursor.item/wearable_apk \ + * -d 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 + * + * 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 KEY_PACKAGE_NAME = + "com.google.android.clockwork.EXTRA_PACKAGE_NAME"; + private static final String KEY_APP_LABEL = "com.google.android.clockwork.EXTRA_APP_LABEL"; + private static final String KEY_APP_ICON_URI = + "com.google.android.clockwork.EXTRA_APP_ICON_URI"; + private static final String KEY_PERMS_LIST = "com.google.android.clockwork.EXTRA_PERMS_LIST"; + private static final String KEY_HAS_LAUNCHER = + "com.google.android.clockwork.EXTRA_HAS_LAUNCHER"; + + private static final String HOME_APP_PACKAGE_NAME = "com.google.android.wearable.app"; + 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; + + 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 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"); + return START_NOT_STICKY; + } + PowerManager.WakeLock lock = getLock(this.getApplicationContext()); + if (!lock.isHeld()) { + lock.acquire(); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Got install/uninstall request " + intent); + } + if (intent != null) { + Bundle intentBundle = intent.getExtras(); + WearPackageArgs.setStartId(intentBundle, startId); + if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) { + final Message msg = mServiceHandler.obtainMessage(START_INSTALL); + WearPackageArgs.setAssetUri(intentBundle, intent.getData()); + msg.setData(intentBundle); + mServiceHandler.sendMessage(msg); + } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) { + Message msg = mServiceHandler.obtainMessage(START_UNINSTALL); + msg.setData(intentBundle); + mServiceHandler.sendMessage(msg); + } + } + 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); + + 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); + } + 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.GET_UNINSTALLED_PACKAGES | 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); + } + } + 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(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; + } + + List<String> 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) { + 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 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; + } + + // 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; + } + + // Finally install the package. + pm.installPackage(Uri.fromFile(tempFile), + new PackageInstallObserver(this, lock, startId, packageName), + installFlags, 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); + } + } + } + + private void uninstallPackage(Bundle argsBundle) { + int startId = WearPackageArgs.getStartId(argsBundle); + final String packageName = WearPackageArgs.getPackageName(argsBundle); + + final PackageManager pm = getPackageManager(); + PowerManager.WakeLock lock = getLock(this.getApplicationContext()); + pm.deletePackage(packageName, new PackageDeleteObserver(lock, startId), + PackageManager.DELETE_ALL_USERS); + startPermsServiceForUninstall(packageName); + Log.i(TAG, "Sent delete request for " + packageName); + } + + private boolean checkPermissions(PackageParser.Package pkg, int companionSdkVersion, + int companionDeviceVersion, Uri permUri, List<String> wearablePermissions, + File apkFile) { + // If the Wear App is targeted for M-release, since the permission model has been changed, + // permissions may not be granted on the phone yet. We need a different flow for user to + // accept these permissions. + // + // Assumption: Code is running on E-release, so Wear is always running M. + // - Case 1: If the Wear App(WA) is targeting 23, always choose the M model (4 cases) + // - Case 2: Else if the Phone App(PA) is targeting 23 and Phone App(P) is running on M, + // show a Dialog so that the user can accept all perms (1 case) + // - Also show a warning to the developer if the watch is targeting M + // - Case 3: If Case 2 is false, then the behavior on the phone is pre-M. Stick to pre-M + // behavior on watch (as long as we don't hit case 1). + // - 3a: WA(22) PA(22) P(22) -> watch app is not targeting 23 + // - 3b: WA(22) PA(22) P(23) -> watch app is not targeting 23 + // - 3c: WA(22) PA(23) P(22) -> watch app is not targeting 23 + // - Case 4: We did not get Companion App's/Device's version, always show dialog to user to + // accept permissions. (This happens if the AndroidWear Companion App is really old). + boolean isWearTargetingM = + pkg.applicationInfo.targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1; + if (isWearTargetingM) { // Case 1 + // Install the app if Wear App is ready for the new perms model. + return true; + } + + List<String> unavailableWearablePerms = getWearPermsNotGrantedOnPhone(pkg.packageName, + permUri, wearablePermissions); + if (unavailableWearablePerms == null) { + return false; + } + + if (unavailableWearablePerms.size() == 0) { + // All permissions requested by the watch are already granted on the phone, no need + // to do anything. + return true; + } + + // Cases 2 and 4. + boolean isCompanionTargetingM = companionSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1; + boolean isCompanionRunningM = companionDeviceVersion > Build.VERSION_CODES.LOLLIPOP_MR1; + if (isCompanionTargetingM) { // Case 2 Warning + Log.w(TAG, "MNC: Wear app's targetSdkVersion should be at least 23, if " + + "phone app is targeting at least 23, will continue."); + } + if ((isCompanionTargetingM && isCompanionRunningM) || // Case 2 + companionSdkVersion == 0 || companionDeviceVersion == 0) { // Case 4 + startPermsServiceForInstall(pkg, apkFile, unavailableWearablePerms); + } + + // Case 3a-3c. + return false; + } + + /** + * Given a {@string packageName} corresponding to a phone app, query the provider for all the + * perms that are granted. + * @return null if there is an error retrieving this info + * else, a list of all the wearable perms that are not in the list of granted perms of + * the phone. + */ + private List<String> getWearPermsNotGrantedOnPhone(String packageName, Uri permUri, + List<String> wearablePermissions) { + if (permUri == null) { + Log.e(TAG, "Permission URI is null"); + return null; + } + Cursor permCursor = getContentResolver().query(permUri, null, null, null, null); + if (permCursor == null) { + Log.e(TAG, "Could not get the cursor for the permissions"); + return null; + } + + Set<String> grantedPerms = new HashSet<>(); + Set<String> 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(); + + ArrayList<String> unavailableWearablePerms = new ArrayList<>(); + for (String wearablePerm : wearablePermissions) { + if (!grantedPerms.contains(wearablePerm)) { + unavailableWearablePerms.add(wearablePerm); + 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 unavailableWearablePerms; + } + + private void finishService(PowerManager.WakeLock lock, int startId) { + if (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 void startPermsServiceForInstall(final PackageParser.Package pkg, final File apkFile, + List<String> unavailableWearablePerms) { + final String packageName = pkg.packageName; + + Intent showPermsIntent = new Intent() + .setComponent(new ComponentName(HOME_APP_PACKAGE_NAME, SHOW_PERMS_SERVICE_CLASS)) + .setAction(Intent.ACTION_INSTALL_PACKAGE); + final PackageManager pm = getPackageManager(); + pkg.applicationInfo.publicSourceDir = apkFile.getPath(); + final CharSequence label = pkg.applicationInfo.loadLabel(pm); + final Uri iconUri = getIconFileUri(packageName, pkg.applicationInfo.loadIcon(pm)); + if (TextUtils.isEmpty(label) || iconUri == null) { + Log.e(TAG, "MNC: Could not launch service since either label " + label + + ", or icon Uri " + iconUri + " is invalid."); + } else { + showPermsIntent.putExtra(KEY_APP_LABEL, label); + showPermsIntent.putExtra(KEY_APP_ICON_URI, iconUri); + showPermsIntent.putExtra(KEY_PACKAGE_NAME, packageName); + showPermsIntent.putExtra(KEY_PERMS_LIST, + unavailableWearablePerms.toArray(new String[0])); + showPermsIntent.putExtra(KEY_HAS_LAUNCHER, WearPackageUtil.hasLauncherActivity(pkg)); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "MNC: Launching Intent " + showPermsIntent + " for " + packageName + + " with name " + label); + } + startService(showPermsIntent); + } + } + + private void startPermsServiceForUninstall(final String packageName) { + Intent showPermsIntent = new Intent() + .setComponent(new ComponentName(HOME_APP_PACKAGE_NAME, SHOW_PERMS_SERVICE_CLASS)) + .setAction(Intent.ACTION_UNINSTALL_PACKAGE); + showPermsIntent.putExtra(KEY_PACKAGE_NAME, packageName); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Launching Intent " + showPermsIntent + " for " + packageName); + } + startService(showPermsIntent); + } + + private Uri getIconFileUri(final String packageName, final Drawable d) { + if (d == null || !(d instanceof BitmapDrawable)) { + Log.e(TAG, "Drawable is not a BitmapDrawable for " + packageName); + return null; + } + File iconFile = WearPackageUtil.getIconFile(this, packageName); + + if (iconFile == null) { + Log.e(TAG, "Could not get icon file for " + packageName); + return null; + } + + FileOutputStream fos = null; + try { + // Convert bitmap to byte array + Bitmap bitmap = ((BitmapDrawable) d).getBitmap(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos); + + // Write the bytes into the file + fos = new FileOutputStream(iconFile); + fos.write(bos.toByteArray()); + fos.flush(); + + return WearPackageIconProvider.getUriForPackage(packageName); + } catch (IOException e) { + Log.e(TAG, "Could not convert drawable to icon file for package " + packageName, e); + return null; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + private class PackageInstallObserver extends IPackageInstallObserver.Stub { + private Context mContext; + private PowerManager.WakeLock mWakeLock; + private int mStartId; + private String mApplicationPackageName; + private PackageInstallObserver(Context context, PowerManager.WakeLock wakeLock, + int startId, String applicationPackageName) { + mContext = context; + mWakeLock = wakeLock; + mStartId = startId; + 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; + } + + Log.i(TAG, "Package " + packageName + " was installed."); + + // Delete tempFile from the file system. + File tempFile = WearPackageUtil.getTemporaryFile(mContext, packageName); + 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); + } + } + } + + 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); + } + } + } +} |