diff options
10 files changed, 1665 insertions, 0 deletions
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp index bd84b58aa0f4..79c810ca2611 100644 --- a/packages/PackageInstaller/Android.bp +++ b/packages/PackageInstaller/Android.bp @@ -46,6 +46,7 @@ android_app { sdk_version: "system_current", rename_resources_package: false, static_libs: [ + "xz-java", "androidx.leanback_leanback", "androidx.annotation_annotation", "androidx.fragment_fragment", @@ -78,6 +79,7 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ + "xz-java", "androidx.leanback_leanback", "androidx.fragment_fragment", "androidx.lifecycle_lifecycle-livedata", @@ -110,6 +112,7 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ + "xz-java", "androidx.leanback_leanback", "androidx.annotation_annotation", "androidx.fragment_fragment", diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 05f4d6954a00..bf69d3ba7603 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -146,6 +146,17 @@ android:configChanges="mnc|mnc|touchscreen|navigation|screenLayout|screenSize|smallestScreenSize|orientation|locale|keyboard|keyboardHidden|fontScale|uiMode|layoutDirection|density" android:exported="false" /> + <!-- Wearable Components --> + <service android:name=".wear.WearPackageInstallerService" + android:permission="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" + android:foregroundServiceType="systemExempted" + android:exported="true"/> + + <provider android:name=".wear.WearPackageIconProvider" + android:authorities="com.google.android.packageinstaller.wear.provider" + android:grantUriPermissions="true" + android:exported="false" /> + <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" tools:node="remove" /> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java new file mode 100644 index 000000000000..53a460dc18ca --- /dev/null +++ b/packages/PackageInstaller/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/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java new file mode 100644 index 000000000000..3daf3d831d97 --- /dev/null +++ b/packages/PackageInstaller/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/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java new file mode 100644 index 000000000000..bdc22cf0e276 --- /dev/null +++ b/packages/PackageInstaller/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/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java new file mode 100644 index 000000000000..1e37f15f714d --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java @@ -0,0 +1,327 @@ +/* + * 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 + ": " + + e.getMessage()); + 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, + Context.RECEIVER_EXPORTED); + + // Create a matching PendingIntent and use it to generate the IntentSender + Intent broadcastIntent = new Intent(action).setPackage(mContext.getPackageName()); + PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(), + broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT + | PendingIntent.FLAG_MUTABLE); + 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; + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java new file mode 100644 index 000000000000..2c289b2a6f94 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java @@ -0,0 +1,100 @@ +/* + * 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_PACKAGE_NAME = + "com.google.android.clockwork.EXTRA_PACKAGE_NAME"; + 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"; + private static final String KEY_SKIP_IF_LOWER_VERSION = + "com.google.android.clockwork.EXTRA_SKIP_IF_LOWER_VERSION"; + + public static String getPackageName(Bundle b) { + return b.getString(KEY_PACKAGE_NAME); + } + + public static Bundle setPackageName(Bundle b, String packageName) { + b.putString(KEY_PACKAGE_NAME, packageName); + return b; + } + + public static Uri getAssetUri(Bundle b) { + return b.getParcelable(KEY_ASSET_URI); + } + + 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 boolean skipIfLowerVersion(Bundle b) { + return b.getBoolean(KEY_SKIP_IF_LOWER_VERSION, false); + } + + public static Bundle setStartId(Bundle b, int startId) { + b.putInt(KEY_START_ID, startId); + return b; + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java new file mode 100644 index 000000000000..02b9d298db0e --- /dev/null +++ b/packages/PackageInstaller/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/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java new file mode 100644 index 000000000000..ae0f4ece1c17 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java @@ -0,0 +1,621 @@ +/* + * 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.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +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 androidx.annotation.Nullable; +import com.android.packageinstaller.DeviceUtils; +import com.android.packageinstaller.PackageUtil; +import com.android.packageinstaller.R; +import com.android.packageinstaller.common.EventResultPersister; +import com.android.packageinstaller.common.UninstallEventReceiver; +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.HashMap; +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 + implements EventResultPersister.EventResultObserver { + private static final String TAG = "WearPkgInstallerService"; + + private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall"; + private static final String BROADCAST_ACTION = + "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT"; + + private final int START_INSTALL = 1; + private final int START_UNINSTALL = 2; + + private int mInstallNotificationId = 1; + private final Map<String, Integer> mNotifIdMap = new ArrayMap<>(); + private final Map<Integer, UninstallParams> mServiceIdToParams = new HashMap<>(); + + private class UninstallParams { + public String mPackageName; + public PowerManager.WakeLock mLock; + + UninstallParams(String packageName, PowerManager.WakeLock lock) { + mPackageName = packageName; + mLock = lock; + } + } + + 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."); + finishServiceEarly(startId); + return START_NOT_STICKY; + } + + if (intent == null) { + Log.w(TAG, "Got null intent."); + finishServiceEarly(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"); + finishServiceEarly(startId); + return START_NOT_STICKY; + } + + final String packageName = WearPackageUtil.getSanitizedPackageName(packageUri); + if (packageName == null) { + Log.e(TAG, "Invalid package name in URI (expected package:<pkgName>): " + packageUri); + finishServiceEarly(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); + Message msg; + String notifTitle; + if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) { + msg = mServiceHandler.obtainMessage(START_INSTALL); + notifTitle = getString(R.string.installing); + } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) { + msg = mServiceHandler.obtainMessage(START_UNINSTALL); + notifTitle = getString(R.string.uninstalling); + } else { + Log.e(TAG, "Unknown action : " + intent.getAction()); + finishServiceEarly(startId); + return START_NOT_STICKY; + } + Pair<Integer, Notification> notifPair = buildNotification(packageName, notifTitle); + startForeground(notifPair.first, notifPair.second); + 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); + 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; + 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) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Replacing package:" + packageName); + } + } + } catch (PackageManager.NameNotFoundException e) { + // Ignore this exception. We could not find the package, will treat as a new + // installation. + } + // 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; + } + PackageInfo pkgInfo = PackageUtil.getPackageInfo(this, tempFile, + PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS); + if (pkgInfo == null) { + Log.e(TAG, "Could not parse apk information for " + packageName); + return; + } + + if (!pkgInfo.packageName.equals(packageName)) { + Log.e(TAG, "Wearable Package Name has to match what is provided for " + + packageName); + return; + } + + ApplicationInfo appInfo = pkgInfo.applicationInfo; + appInfo.sourceDir = tempFile.getPath(); + appInfo.publicSourceDir = tempFile.getPath(); + getLabelAndUpdateNotification(packageName, + getString(R.string.installing_app, appInfo.loadLabel(pm))); + + List<String> wearablePerms = Arrays.asList(pkgInfo.requestedPermissions); + + // Log if the installed pkg has a higher version number. + if (existingPkgInfo != null) { + long longVersionCode = pkgInfo.getLongVersionCode(); + if (existingPkgInfo.getLongVersionCode() == longVersionCode) { + if (skipIfSameVersion) { + Log.w(TAG, "Version number (" + longVersionCode + + ") 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 (" + longVersionCode + + ") is equal to existing app for " + packageName); + } + } else if (existingPkgInfo.getLongVersionCode() > longVersionCode) { + if (skipIfLowerVersion) { + // Starting in Feldspar, we are not going to allow downgrades of any app. + Log.w(TAG, "Version number of new app (" + longVersionCode + + ") is lower than existing app ( " + + existingPkgInfo.getLongVersionCode() + + ") for " + packageName + "; not installing due to versionCheck"); + return; + } else { + Log.w(TAG, "Version number of new app (" + longVersionCode + + ") is lower than existing app ( " + + existingPkgInfo.getLongVersionCode() + ") 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; + for (FeatureInfo feature : pkgInfo.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(pkgInfo, 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()); + + UninstallParams params = new UninstallParams(packageName, lock); + mServiceIdToParams.put(startId, params); + + final PackageManager pm = getPackageManager(); + try { + PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); + getLabelAndUpdateNotification(packageName, + getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm))); + + int uninstallId = UninstallEventReceiver.addObserver(this, + EventResultPersister.GENERATE_NEW_ID, this); + + Intent broadcastIntent = new Intent(BROADCAST_ACTION); + broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId); + broadcastIntent.putExtra(EventResultPersister.EXTRA_SERVICE_ID, startId); + broadcastIntent.setPackage(getPackageName()); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId, + broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT + | PendingIntent.FLAG_MUTABLE); + + // Found package, send uninstall request. + pm.getPackageInstaller().uninstall( + new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), + PackageManager.DELETE_ALL_USERS, + pendingIntent.getIntentSender()); + + 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); + } catch (EventResultPersister.OutOfIdsException e) { + Log.e(TAG, "Fails to start uninstall", e); + finishService(lock, startId); + } + } + + @Override + public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) { + if (mServiceIdToParams.containsKey(serviceId)) { + UninstallParams params = mServiceIdToParams.get(serviceId); + try { + if (status == PackageInstaller.STATUS_SUCCESS) { + Log.i(TAG, "Package " + params.mPackageName + " was uninstalled."); + } else { + Log.e(TAG, "Package uninstall failed " + params.mPackageName + + ", returnCode " + legacyStatus); + } + } finally { + finishService(params.mLock, serviceId); + } + } + } + + private boolean checkPermissions(PackageInfo pkgInfo, int companionSdkVersion, + int companionDeviceVersion, Uri permUri, List<String> 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 (pkgInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M) { + // Install the app if Wear App is ready for the new perms model. + return true; + } + + if (!doesWearHaveUngrantedPerms(pkgInfo.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<String> 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<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(); + + 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; + } + + /** Finishes the service after fulfilling obligation to call startForeground. */ + private void finishServiceEarly(int startId) { + Pair<Integer, Notification> notifPair = buildNotification( + getApplicationContext().getPackageName(), ""); + startForeground(notifPair.first, notifPair.second); + finishService(null, startId); + } + + 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 synchronized Pair<Integer, Notification> 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<Integer, Notification> notifPair = buildNotification(packageName, title); + notificationManager.notify(notifPair.first, notifPair.second); + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java new file mode 100644 index 000000000000..6a9145db9a06 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java @@ -0,0 +1,133 @@ +/* + * 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.Context; +import android.net.Uri; +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; + +public class WearPackageUtil { + private static final String TAG = "WearablePkgInstaller"; + + private static final String COMPRESSION_LZMA = "lzma"; + private static final String COMPRESSION_XZ = "xz"; + + 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, ParsingPackageUtils 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, 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); + } + } + } + + /** + * @return com.google.com from expected formats like + * Uri: package:com.google.com, package:/com.google.com, package://com.google.com + */ + public static String getSanitizedPackageName(Uri packageUri) { + String packageName = packageUri.getEncodedSchemeSpecificPart(); + if (packageName != null) { + return packageName.replaceAll("^/+", ""); + } + return packageName; + } +} |