blob: 2fc6db3705e7e3da5221eb73a099c11f4bf60248 [file] [log] [blame]
/*
**
** 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}.
*
* <p>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<InstallFlowAnalytics> CREATOR =
new Parcelable.Creator<InstallFlowAnalytics>() {
@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();
}
}