/* ** ** Copyright 2013, 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; import android.content.Context; import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.provider.Settings; import android.util.EventLog; import android.util.Log; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import libcore.io.IoUtils; /** * Analytics about an attempt to install a package via {@link PackageInstallerActivity}. * *

An instance of this class is created at the beginning of the install flow and gradually filled * as the user progresses through the flow. When the flow terminates (regardless of the reason), * {@link #setFlowFinished(byte)} is invoked which reports the installation attempt as an event * to the Event Log. */ public class InstallFlowAnalytics implements Parcelable { private static final String TAG = "InstallFlowAnalytics"; /** Installation has not yet terminated. */ static final byte RESULT_NOT_YET_AVAILABLE = -1; /** Package successfully installed. */ static final byte RESULT_SUCCESS = 0; /** Installation failed because scheme unsupported. */ static final byte RESULT_FAILED_UNSUPPORTED_SCHEME = 1; /** * Installation of an APK failed because of a failure to obtain information from the provided * APK. */ static final byte RESULT_FAILED_TO_GET_PACKAGE_INFO = 2; /** * Installation of an already installed package into the current user profile failed because the * specified package is not installed. */ static final byte RESULT_FAILED_PACKAGE_MISSING = 3; /** * Installation failed because installation from unknown sources is prohibited by the Unknown * Sources setting. */ static final byte RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING = 4; /** Installation cancelled by the user. */ static final byte RESULT_CANCELLED_BY_USER = 5; /** * Installation failed due to {@code PackageManager} failure. PackageManager error code is * provided in {@link #mPackageManagerInstallResult}). */ static final byte RESULT_PACKAGE_MANAGER_INSTALL_FAILED = 6; private static final int FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED = 1 << 0; private static final int FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE = 1 << 1; private static final int FLAG_VERIFY_APPS_ENABLED = 1 << 2; private static final int FLAG_APP_VERIFIER_INSTALLED = 1 << 3; private static final int FLAG_FILE_URI = 1 << 4; private static final int FLAG_REPLACE = 1 << 5; private static final int FLAG_SYSTEM_APP = 1 << 6; private static final int FLAG_PACKAGE_INFO_OBTAINED = 1 << 7; private static final int FLAG_INSTALL_BUTTON_CLICKED = 1 << 8; private static final int FLAG_NEW_PERMISSIONS_FOUND = 1 << 9; private static final int FLAG_PERMISSIONS_DISPLAYED = 1 << 10; private static final int FLAG_NEW_PERMISSIONS_DISPLAYED = 1 << 11; private static final int FLAG_ALL_PERMISSIONS_DISPLAYED = 1 << 12; /** * Information about this flow expressed as a collection of flags. See {@code FLAG_...} * constants. */ private int mFlags; /** Outcome of the flow. See {@code RESULT_...} constants. */ private byte mResult = RESULT_NOT_YET_AVAILABLE; /** * Result code returned by {@code PackageManager} to install the package or {@code 0} if * {@code PackageManager} has not yet been invoked to install the package. */ private int mPackageManagerInstallResult; /** * Time instant when the installation request arrived, measured in elapsed realtime * milliseconds. See {@link SystemClock#elapsedRealtime()}. */ private long mStartTimestampMillis; /** * Time instant when the information about the package being installed was obtained, measured in * elapsed realtime milliseconds. See {@link SystemClock#elapsedRealtime()}. */ private long mPackageInfoObtainedTimestampMillis; /** * Time instant when the user clicked the Install button, measured in elapsed realtime * milliseconds. See {@link SystemClock#elapsedRealtime()}. This field is only valid if the * Install button has been clicked, as signaled by {@link #FLAG_INSTALL_BUTTON_CLICKED}. */ private long mInstallButtonClickTimestampMillis; /** * Time instant when this flow terminated, measured in elapsed realtime milliseconds. See * {@link SystemClock#elapsedRealtime()}. */ private long mEndTimestampMillis; /** URI of the package being installed. */ private String mPackageUri; /** Whether this attempt has been logged to the Event Log. */ private boolean mLogged; private Context mContext; public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public InstallFlowAnalytics createFromParcel(Parcel in) { return new InstallFlowAnalytics(in); } @Override public InstallFlowAnalytics[] newArray(int size) { return new InstallFlowAnalytics[size]; } }; public InstallFlowAnalytics() {} public InstallFlowAnalytics(Parcel in) { mFlags = in.readInt(); mResult = in.readByte(); mPackageManagerInstallResult = in.readInt(); mStartTimestampMillis = in.readLong(); mPackageInfoObtainedTimestampMillis = in.readLong(); mInstallButtonClickTimestampMillis = in.readLong(); mEndTimestampMillis = in.readLong(); mPackageUri = in.readString(); mLogged = readBoolean(in); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mFlags); dest.writeByte(mResult); dest.writeInt(mPackageManagerInstallResult); dest.writeLong(mStartTimestampMillis); dest.writeLong(mPackageInfoObtainedTimestampMillis); dest.writeLong(mInstallButtonClickTimestampMillis); dest.writeLong(mEndTimestampMillis); dest.writeString(mPackageUri); writeBoolean(dest, mLogged); } private static void writeBoolean(Parcel dest, boolean value) { dest.writeByte((byte) (value ? 1 : 0)); } private static boolean readBoolean(Parcel dest) { return dest.readByte() != 0; } @Override public int describeContents() { return 0; } void setContext(Context context) { mContext = context; } /** Sets whether the Unknown Sources setting is checked. */ void setInstallsFromUnknownSourcesPermitted(boolean permitted) { setFlagState(FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED, permitted); } /** Gets whether the Unknown Sources setting is checked. */ private boolean isInstallsFromUnknownSourcesPermitted() { return isFlagSet(FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED); } /** Sets whether this install attempt is from an unknown source. */ void setInstallRequestFromUnknownSource(boolean unknownSource) { setFlagState(FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE, unknownSource); } /** Gets whether this install attempt is from an unknown source. */ private boolean isInstallRequestFromUnknownSource() { return isFlagSet(FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE); } /** Sets whether app verification is enabled. */ void setVerifyAppsEnabled(boolean enabled) { setFlagState(FLAG_VERIFY_APPS_ENABLED, enabled); } /** Gets whether app verification is enabled. */ private boolean isVerifyAppsEnabled() { return isFlagSet(FLAG_VERIFY_APPS_ENABLED); } /** Sets whether at least one app verifier is installed. */ void setAppVerifierInstalled(boolean installed) { setFlagState(FLAG_APP_VERIFIER_INSTALLED, installed); } /** Gets whether at least one app verifier is installed. */ private boolean isAppVerifierInstalled() { return isFlagSet(FLAG_APP_VERIFIER_INSTALLED); } /** * Sets whether an APK file is being installed. * * @param fileUri {@code true} if an APK file is being installed, {@code false} if an already * installed package is being installed to this user profile. */ void setFileUri(boolean fileUri) { setFlagState(FLAG_FILE_URI, fileUri); } /** * Sets the URI of the package being installed. */ void setPackageUri(String packageUri) { mPackageUri = packageUri; } /** * Gets whether an APK file is being installed. * * @return {@code true} if an APK file is being installed, {@code false} if an already * installed package is being installed to this user profile. */ private boolean isFileUri() { return isFlagSet(FLAG_FILE_URI); } /** Sets whether this is an attempt to replace an existing package. */ void setReplace(boolean replace) { setFlagState(FLAG_REPLACE, replace); } /** Gets whether this is an attempt to replace an existing package. */ private boolean isReplace() { return isFlagSet(FLAG_REPLACE); } /** Sets whether the package being updated is a system package. */ void setSystemApp(boolean systemApp) { setFlagState(FLAG_SYSTEM_APP, systemApp); } /** Gets whether the package being updated is a system package. */ private boolean isSystemApp() { return isFlagSet(FLAG_SYSTEM_APP); } /** * Sets whether the package being installed is requesting more permissions than the already * installed version of the package. */ void setNewPermissionsFound(boolean found) { setFlagState(FLAG_NEW_PERMISSIONS_FOUND, found); } /** * Gets whether the package being installed is requesting more permissions than the already * installed version of the package. */ private boolean isNewPermissionsFound() { return isFlagSet(FLAG_NEW_PERMISSIONS_FOUND); } /** Sets whether permissions were displayed to the user. */ void setPermissionsDisplayed(boolean displayed) { setFlagState(FLAG_PERMISSIONS_DISPLAYED, displayed); } /** Gets whether permissions were displayed to the user. */ private boolean isPermissionsDisplayed() { return isFlagSet(FLAG_PERMISSIONS_DISPLAYED); } /** * Sets whether new permissions were displayed to the user (if permissions were displayed at * all). */ void setNewPermissionsDisplayed(boolean displayed) { setFlagState(FLAG_NEW_PERMISSIONS_DISPLAYED, displayed); } /** * Gets whether new permissions were displayed to the user (if permissions were displayed at * all). */ private boolean isNewPermissionsDisplayed() { return isFlagSet(FLAG_NEW_PERMISSIONS_DISPLAYED); } /** * Sets whether all permissions were displayed to the user (if permissions were displayed at * all). */ void setAllPermissionsDisplayed(boolean displayed) { setFlagState(FLAG_ALL_PERMISSIONS_DISPLAYED, displayed); } /** * Gets whether all permissions were displayed to the user (if permissions were displayed at * all). */ private boolean isAllPermissionsDisplayed() { return isFlagSet(FLAG_ALL_PERMISSIONS_DISPLAYED); } /** * Sets the time instant when the installation request arrived, measured in elapsed realtime * milliseconds. See {@link SystemClock#elapsedRealtime()}. */ void setStartTimestampMillis(long timestampMillis) { mStartTimestampMillis = timestampMillis; } /** * Records that the information about the package info has been obtained or that there has been * a failure to obtain the information. */ void setPackageInfoObtained() { setFlagState(FLAG_PACKAGE_INFO_OBTAINED, true); mPackageInfoObtainedTimestampMillis = SystemClock.elapsedRealtime(); } /** * Checks whether the information about the package info has been obtained or that there has * been a failure to obtain the information. */ private boolean isPackageInfoObtained() { return isFlagSet(FLAG_PACKAGE_INFO_OBTAINED); } /** * Records that the Install button has been clicked. */ void setInstallButtonClicked() { setFlagState(FLAG_INSTALL_BUTTON_CLICKED, true); mInstallButtonClickTimestampMillis = SystemClock.elapsedRealtime(); } /** * Checks whether the Install button has been clicked. */ private boolean isInstallButtonClicked() { return isFlagSet(FLAG_INSTALL_BUTTON_CLICKED); } /** * Marks this flow as finished due to {@code PackageManager} succeeding or failing to install * the package and reports this to the Event Log. */ void setFlowFinishedWithPackageManagerResult(int packageManagerResult) { mPackageManagerInstallResult = packageManagerResult; if (packageManagerResult == PackageManager.INSTALL_SUCCEEDED) { setFlowFinished( InstallFlowAnalytics.RESULT_SUCCESS); } else { setFlowFinished( InstallFlowAnalytics.RESULT_PACKAGE_MANAGER_INSTALL_FAILED); } } /** * Marks this flow as finished and reports this to the Event Log. */ void setFlowFinished(byte result) { if (mLogged) { return; } mResult = result; mEndTimestampMillis = SystemClock.elapsedRealtime(); writeToEventLog(); } private void writeToEventLog() { byte packageManagerInstallResultByte = 0; if (mResult == RESULT_PACKAGE_MANAGER_INSTALL_FAILED) { // PackageManager install error codes are negative, starting from -1 and going to // -111 (at the moment). We thus store them in negated form. packageManagerInstallResultByte = clipUnsignedValueToUnsignedByte( -mPackageManagerInstallResult); } final int resultAndFlags = (mResult & 0xff) | ((packageManagerInstallResultByte & 0xff) << 8) | ((mFlags & 0xffff) << 16); // Total elapsed time from start to end, in milliseconds. final int totalElapsedTime = clipUnsignedLongToUnsignedInt(mEndTimestampMillis - mStartTimestampMillis); // Total elapsed time from start till information about the package being installed was // obtained, in milliseconds. final int elapsedTimeTillPackageInfoObtained = (isPackageInfoObtained()) ? clipUnsignedLongToUnsignedInt( mPackageInfoObtainedTimestampMillis - mStartTimestampMillis) : 0; // Total elapsed time from start till Install button clicked, in milliseconds // milliseconds. final int elapsedTimeTillInstallButtonClick = (isInstallButtonClicked()) ? clipUnsignedLongToUnsignedInt( mInstallButtonClickTimestampMillis - mStartTimestampMillis) : 0; // If this user has consented to app verification, augment the logged event with the hash of // the contents of the APK. if (((mFlags & FLAG_FILE_URI) != 0) && ((mFlags & FLAG_VERIFY_APPS_ENABLED) != 0) && (isUserConsentToVerifyAppsGranted())) { // Log the hash of the APK's contents. // Reading the APK may take a while -- perform in background. AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { @Override public void run() { byte[] digest = null; try { digest = getPackageContentsDigest(); } catch (IOException e) { Log.w(TAG, "Failed to hash APK contents", e); } finally { String digestHex = (digest != null) ? IntegralToString.bytesToHexString(digest, false) : ""; EventLogTags.writeInstallPackageAttempt( resultAndFlags, totalElapsedTime, elapsedTimeTillPackageInfoObtained, elapsedTimeTillInstallButtonClick, digestHex); } } }); } else { // Do not log the hash of the APK's contents EventLogTags.writeInstallPackageAttempt( resultAndFlags, totalElapsedTime, elapsedTimeTillPackageInfoObtained, elapsedTimeTillInstallButtonClick, ""); } mLogged = true; if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Analytics:" + "\n\tinstallsFromUnknownSourcesPermitted: " + isInstallsFromUnknownSourcesPermitted() + "\n\tinstallRequestFromUnknownSource: " + isInstallRequestFromUnknownSource() + "\n\tverifyAppsEnabled: " + isVerifyAppsEnabled() + "\n\tappVerifierInstalled: " + isAppVerifierInstalled() + "\n\tfileUri: " + isFileUri() + "\n\treplace: " + isReplace() + "\n\tsystemApp: " + isSystemApp() + "\n\tpackageInfoObtained: " + isPackageInfoObtained() + "\n\tinstallButtonClicked: " + isInstallButtonClicked() + "\n\tpermissionsDisplayed: " + isPermissionsDisplayed() + "\n\tnewPermissionsDisplayed: " + isNewPermissionsDisplayed() + "\n\tallPermissionsDisplayed: " + isAllPermissionsDisplayed() + "\n\tnewPermissionsFound: " + isNewPermissionsFound() + "\n\tresult: " + mResult + "\n\tpackageManagerInstallResult: " + mPackageManagerInstallResult + "\n\ttotalDuration: " + (mEndTimestampMillis - mStartTimestampMillis) + " ms" + "\n\ttimeTillPackageInfoObtained: " + ((isPackageInfoObtained()) ? ((mPackageInfoObtainedTimestampMillis - mStartTimestampMillis) + " ms") : "n/a") + "\n\ttimeTillInstallButtonClick: " + ((isInstallButtonClicked()) ? ((mInstallButtonClickTimestampMillis - mStartTimestampMillis) + " ms") : "n/a")); Log.v(TAG, "Wrote to Event Log: 0x" + Long.toString(resultAndFlags & 0xffffffffL, 16) + ", " + totalElapsedTime + ", " + elapsedTimeTillPackageInfoObtained + ", " + elapsedTimeTillInstallButtonClick); } } private static final byte clipUnsignedValueToUnsignedByte(long value) { if (value < 0) { return 0; } else if (value > 0xff) { return (byte) 0xff; } else { return (byte) value; } } private static final int clipUnsignedLongToUnsignedInt(long value) { if (value < 0) { return 0; } else if (value > 0xffffffffL) { return 0xffffffff; } else { return (int) value; } } /** * Sets or clears the specified flag in the {@link #mFlags} field. */ private void setFlagState(int flag, boolean set) { if (set) { mFlags |= flag; } else { mFlags &= ~flag; } } /** * Checks whether the specified flag is set in the {@link #mFlags} field. */ private boolean isFlagSet(int flag) { return (mFlags & flag) == flag; } /** * Checks whether the user has consented to app verification. */ private boolean isUserConsentToVerifyAppsGranted() { return Settings.Secure.getInt( mContext.getContentResolver(), Settings.Secure.PACKAGE_VERIFIER_USER_CONSENT, 0) != 0; } /** * Gets the digest of the contents of the package being installed. */ private byte[] getPackageContentsDigest() throws IOException { File file = new File(Uri.parse(mPackageUri).getPath()); return getSha256ContentsDigest(file); } /** * Gets the SHA-256 digest of the contents of the specified file. */ private static byte[] getSha256ContentsDigest(File file) throws IOException { MessageDigest digest; try { digest = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("SHA-256 not available", e); } byte[] buf = new byte[8192]; InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(file), buf.length); int chunkSize; while ((chunkSize = in.read(buf)) != -1) { digest.update(buf, 0, chunkSize); } } finally { IoUtils.closeQuietly(in); } return digest.digest(); } }