summaryrefslogtreecommitdiffstats
path: root/src/com/android/packageinstaller/wear
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/packageinstaller/wear')
-rw-r--r--src/com/android/packageinstaller/wear/WearPackageArgs.java92
-rw-r--r--src/com/android/packageinstaller/wear/WearPackageIconProvider.java202
-rw-r--r--src/com/android/packageinstaller/wear/WearPackageInstallerService.java602
-rw-r--r--src/com/android/packageinstaller/wear/WearPackageUtil.java167
4 files changed, 1063 insertions, 0 deletions
diff --git a/src/com/android/packageinstaller/wear/WearPackageArgs.java b/src/com/android/packageinstaller/wear/WearPackageArgs.java
new file mode 100644
index 00000000..67051da0
--- /dev/null
+++ b/src/com/android/packageinstaller/wear/WearPackageArgs.java
@@ -0,0 +1,92 @@
+/*
+ * 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.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * Installation Util that contains a list of parameters that are needed for
+ * installing/uninstalling.
+ */
+public class WearPackageArgs {
+ private static final String KEY_ASSET_URI =
+ "com.google.android.clockwork.EXTRA_ASSET_URI";
+ private static final String KEY_START_ID =
+ "com.google.android.clockwork.EXTRA_START_ID";
+ private static final String KEY_PERM_URI =
+ "com.google.android.clockwork.EXTRA_PERM_URI";
+ private static final String KEY_CHECK_PERMS =
+ "com.google.android.clockwork.EXTRA_CHECK_PERMS";
+ private static final String KEY_SKIP_IF_SAME_VERSION =
+ "com.google.android.clockwork.EXTRA_SKIP_IF_SAME_VERSION";
+ private static final String KEY_COMPRESSION_ALG =
+ "com.google.android.clockwork.EXTRA_KEY_COMPRESSION_ALG";
+ private static final String KEY_COMPANION_SDK_VERSION =
+ "com.google.android.clockwork.EXTRA_KEY_COMPANION_SDK_VERSION";
+ private static final String KEY_COMPANION_DEVICE_VERSION =
+ "com.google.android.clockwork.EXTRA_KEY_COMPANION_DEVICE_VERSION";
+ private static final String KEY_SHOULD_CHECK_GMS_DEPENDENCY =
+ "com.google.android.clockwork.EXTRA_KEY_SHOULD_CHECK_GMS_DEPENDENCY";
+
+ public static String getPackageName(Bundle b) {
+ return b.getString(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
+ }
+
+ public static Uri getAssetUri(Bundle b) {
+ return b.getParcelable(KEY_ASSET_URI);
+ }
+
+ public static Bundle setAssetUri(Bundle b, Uri assetUri) {
+ b.putParcelable(KEY_ASSET_URI, assetUri);
+ return b;
+ }
+
+ public static Uri getPermUri(Bundle b) {
+ return b.getParcelable(KEY_PERM_URI);
+ }
+
+ public static boolean checkPerms(Bundle b) {
+ return b.getBoolean(KEY_CHECK_PERMS);
+ }
+
+ public static boolean skipIfSameVersion(Bundle b) {
+ return b.getBoolean(KEY_SKIP_IF_SAME_VERSION);
+ }
+
+ public static int getCompanionSdkVersion(Bundle b) {
+ return b.getInt(KEY_COMPANION_SDK_VERSION);
+ }
+
+ public static int getCompanionDeviceVersion(Bundle b) {
+ return b.getInt(KEY_COMPANION_DEVICE_VERSION);
+ }
+
+ public static String getCompressionAlg(Bundle b) {
+ return b.getString(KEY_COMPRESSION_ALG);
+ }
+
+ public static int getStartId(Bundle b) {
+ return b.getInt(KEY_START_ID);
+ }
+
+ public static Bundle setStartId(Bundle b, int startId) {
+ b.putInt(KEY_START_ID, startId);
+ return b;
+ }
+}
diff --git a/src/com/android/packageinstaller/wear/WearPackageIconProvider.java b/src/com/android/packageinstaller/wear/WearPackageIconProvider.java
new file mode 100644
index 00000000..02b9d298
--- /dev/null
+++ b/src/com/android/packageinstaller/wear/WearPackageIconProvider.java
@@ -0,0 +1,202 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.List;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+public class WearPackageIconProvider extends ContentProvider {
+ private static final String TAG = "WearPackageIconProvider";
+ public static final String AUTHORITY = "com.google.android.packageinstaller.wear.provider";
+
+ private static final String REQUIRED_PERMISSION =
+ "com.google.android.permission.INSTALL_WEARABLE_PACKAGES";
+
+ /** MIME types. */
+ public static final String ICON_TYPE = "vnd.android.cursor.item/cw_package_icon";
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ throw new UnsupportedOperationException("Query is not supported.");
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ if (uri == null) {
+ throw new IllegalArgumentException("URI passed in is null.");
+ }
+
+ if (AUTHORITY.equals(uri.getEncodedAuthority())) {
+ return ICON_TYPE;
+ }
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException("Insert is not supported.");
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ if (uri == null) {
+ throw new IllegalArgumentException("URI passed in is null.");
+ }
+
+ enforcePermissions(uri);
+
+ if (ICON_TYPE.equals(getType(uri))) {
+ final File file = WearPackageUtil.getIconFile(
+ this.getContext().getApplicationContext(), getPackageNameFromUri(uri));
+ if (file != null) {
+ file.delete();
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("Update is not supported.");
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(
+ Uri uri, @SuppressWarnings("unused") String mode) throws FileNotFoundException {
+ if (uri == null) {
+ throw new IllegalArgumentException("URI passed in is null.");
+ }
+
+ enforcePermissions(uri);
+
+ if (ICON_TYPE.equals(getType(uri))) {
+ final File file = WearPackageUtil.getIconFile(
+ this.getContext().getApplicationContext(), getPackageNameFromUri(uri));
+ if (file != null) {
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+ }
+ return null;
+ }
+
+ public static Uri getUriForPackage(final String packageName) {
+ return Uri.parse("content://" + AUTHORITY + "/icons/" + packageName + ".icon");
+ }
+
+ private String getPackageNameFromUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ List<String> pathSegments = uri.getPathSegments();
+ String packageName = pathSegments.get(pathSegments.size() - 1);
+
+ if (packageName.endsWith(".icon")) {
+ packageName = packageName.substring(0, packageName.lastIndexOf("."));
+ }
+ return packageName;
+ }
+
+ /**
+ * Make sure the calling app is either a system app or the same app or has the right permission.
+ * @throws SecurityException if the caller has insufficient permissions.
+ */
+ @TargetApi(Build.VERSION_CODES.BASE_1_1)
+ private void enforcePermissions(Uri uri) {
+ // Redo some of the permission check in {@link ContentProvider}. Just add an extra check to
+ // allow System process to access this provider.
+ Context context = getContext();
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final int myUid = android.os.Process.myUid();
+
+ if (uid == myUid || isSystemApp(context, pid)) {
+ return;
+ }
+
+ if (context.checkPermission(REQUIRED_PERMISSION, pid, uid) == PERMISSION_GRANTED) {
+ return;
+ }
+
+ // last chance, check against any uri grants
+ if (context.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ == PERMISSION_GRANTED) {
+ return;
+ }
+
+ throw new SecurityException("Permission Denial: reading "
+ + getClass().getName() + " uri " + uri + " from pid=" + pid
+ + ", uid=" + uid);
+ }
+
+ /**
+ * From the pid of the calling process, figure out whether this is a system app or not. We do
+ * this by checking the application information corresponding to the pid and then checking if
+ * FLAG_SYSTEM is set.
+ */
+ @TargetApi(Build.VERSION_CODES.CUPCAKE)
+ private boolean isSystemApp(Context context, int pid) {
+ // Get the Activity Manager Object
+ ActivityManager aManager =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ // Get the list of running Applications
+ List<ActivityManager.RunningAppProcessInfo> rapInfoList =
+ aManager.getRunningAppProcesses();
+ for (ActivityManager.RunningAppProcessInfo rapInfo : rapInfoList) {
+ if (rapInfo.pid == pid) {
+ try {
+ PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(
+ rapInfo.pkgList[0], 0);
+ if (pkgInfo != null && pkgInfo.applicationInfo != null &&
+ (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ Log.d(TAG, pid + " is a system app.");
+ return true;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Could not find package information.", e);
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+}
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);
+ }
+ }
+ }
+}
diff --git a/src/com/android/packageinstaller/wear/WearPackageUtil.java b/src/com/android/packageinstaller/wear/WearPackageUtil.java
new file mode 100644
index 00000000..688d6167
--- /dev/null
+++ b/src/com/android/packageinstaller/wear/WearPackageUtil.java
@@ -0,0 +1,167 @@
+/*
+ * 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.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageParser;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.tukaani.xz.LZMAInputStream;
+import org.tukaani.xz.XZInputStream;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+public class WearPackageUtil {
+ private static final String TAG = "WearablePkgInstaller";
+
+ private static final String COMPRESSION_LZMA = "lzma";
+ private static final String COMPRESSION_XZ = "xz";
+
+ private static final String SHOW_PERMS_SERVICE_PKG_NAME = "com.google.android.wearable.app";
+ private static final String SHOW_PERMS_SERVICE_CLASS_NAME =
+ "com.google.android.clockwork.packagemanager.ShowPermsService";
+ private static final String EXTRA_PACKAGE_NAME
+ = "com.google.android.clockwork.EXTRA_PACKAGE_NAME";
+
+ public static File getTemporaryFile(Context context, String packageName) {
+ try {
+ File newFileDir = new File(context.getFilesDir(), "tmp");
+ newFileDir.mkdirs();
+ Os.chmod(newFileDir.getAbsolutePath(), 0771);
+ File newFile = new File(newFileDir, packageName + ".apk");
+ return newFile;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to open.", e);
+ return null;
+ }
+ }
+
+ public static File getIconFile(final Context context, final String packageName) {
+ try {
+ File newFileDir = new File(context.getFilesDir(), "images/icons");
+ newFileDir.mkdirs();
+ Os.chmod(newFileDir.getAbsolutePath(), 0771);
+ return new File(newFileDir, packageName + ".icon");
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to open.", e);
+ return null;
+ }
+ }
+
+ /**
+ * In order to make sure that the Wearable Asset Manager has a reasonable apk that can be used
+ * by the PackageManager, we will parse it before sending it to the PackageManager.
+ * Unfortunately, PackageParser needs a file to parse. So, we have to temporarily convert the fd
+ * to a File.
+ *
+ * @param context
+ * @param fd FileDescriptor to convert to File
+ * @param packageName Name of package, will define the name of the file
+ * @param compressionAlg Can be null. For ALT mode the APK will be compressed. We will
+ * decompress it here
+ */
+ public static File getFileFromFd(Context context, ParcelFileDescriptor fd,
+ String packageName, @Nullable String compressionAlg) {
+ File newFile = getTemporaryFile(context, packageName);
+ if (fd == null || fd.getFileDescriptor() == null) {
+ return null;
+ }
+ InputStream fr = new ParcelFileDescriptor.AutoCloseInputStream(fd);
+ try {
+ if (TextUtils.equals(compressionAlg, COMPRESSION_XZ)) {
+ fr = new XZInputStream(fr);
+ } else if (TextUtils.equals(compressionAlg, COMPRESSION_LZMA)) {
+ fr = new LZMAInputStream(fr);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Compression was set to " + compressionAlg + ", but could not decode ", e);
+ return null;
+ }
+
+ int nRead;
+ byte[] data = new byte[1024];
+ try {
+ final FileOutputStream fo = new FileOutputStream(newFile);
+ while ((nRead = fr.read(data, 0, data.length)) != -1) {
+ fo.write(data, 0, nRead);
+ }
+ fo.flush();
+ fo.close();
+ Os.chmod(newFile.getAbsolutePath(), 0644);
+ return newFile;
+ } catch (IOException e) {
+ Log.e(TAG, "Reading from Asset FD or writing to temp file failed ", e);
+ return null;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Could not set permissions on file ", e);
+ return null;
+ } finally {
+ try {
+ fr.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to close the file from FD ", e);
+ }
+ }
+ }
+
+ public static boolean hasLauncherActivity(PackageParser.Package pkg) {
+ if (pkg == null || pkg.activities == null) {
+ return false;
+ }
+
+ final int activityCount = pkg.activities.size();
+ for (int i = 0; i < activityCount; ++i) {
+ if (pkg.activities.get(i).intents != null) {
+ ArrayList<PackageParser.ActivityIntentInfo> intents =
+ pkg.activities.get(i).intents;
+ final int intentsCount = intents.size();
+ for (int j = 0; j < intentsCount; ++j) {
+ final PackageParser.ActivityIntentInfo intentInfo = intents.get(j);
+ if (intentInfo.hasAction(Intent.ACTION_MAIN)) {
+ if (intentInfo.hasCategory(Intent.CATEGORY_INFO) ||
+ intentInfo .hasCategory(Intent.CATEGORY_LAUNCHER)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public static void removeFromPermStore(Context context, String wearablePackageName) {
+ Intent newIntent = new Intent()
+ .setComponent(new ComponentName(
+ SHOW_PERMS_SERVICE_PKG_NAME, SHOW_PERMS_SERVICE_CLASS_NAME))
+ .setAction(Intent.ACTION_UNINSTALL_PACKAGE);
+ newIntent.putExtra(EXTRA_PACKAGE_NAME, wearablePackageName);
+ Log.i(TAG, "Sending removeFromPermStore to ShowPermsService " + newIntent
+ + " for " + wearablePackageName);
+ context.startService(newIntent);
+ }
+}