*/
package android.app.usage;
+import android.annotation.IntDef;
import android.content.res.Configuration;
import android.os.Parcel;
import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
*/
public final class UsageEvents implements Parcelable {
+ /** @hide */
+ public static final String INSTANT_APP_PACKAGE_NAME = "android.instant_app";
+
+ /** @hide */
+ public static final String INSTANT_APP_CLASS_NAME = "android.instant_class";
+
/**
* An event representing a state change for a component.
*/
*/
public static final int CHOOSER_ACTION = 9;
+ /** @hide */
+ public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0;
+
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ FLAG_IS_PACKAGE_INSTANT_APP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EventFlags {}
+
/**
* {@hide}
*/
*/
public String[] mContentAnnotations;
+ /** @hide */
+ @EventFlags
+ public int mFlags;
+
+ public Event() {
+ }
+
+ /** @hide */
+ public Event(Event orig) {
+ mPackage = orig.mPackage;
+ mClass = orig.mClass;
+ mTimeStamp = orig.mTimeStamp;
+ mEventType = orig.mEventType;
+ mConfiguration = orig.mConfiguration;
+ mShortcutId = orig.mShortcutId;
+ mAction = orig.mAction;
+ mContentType = orig.mContentType;
+ mContentAnnotations = orig.mContentAnnotations;
+ mFlags = orig.mFlags;
+ }
+
/**
* The package name of the source of this event.
*/
public String getShortcutId() {
return mShortcutId;
}
+
+ /** @hide */
+ public Event getObfuscatedIfInstantApp() {
+ if ((mFlags & FLAG_IS_PACKAGE_INSTANT_APP) == 0) {
+ return this;
+ }
+ final Event ret = new Event(this);
+ ret.mPackage = INSTANT_APP_PACKAGE_NAME;
+ ret.mClass = INSTANT_APP_CLASS_NAME;
+
+ // Note there are other string fields too, but they're for app shortcuts and choosers,
+ // which instant apps can't use anyway, so there's no need to hide them.
+ return ret;
+ }
}
// Only used when creating the resulting events. Not used for reading/unparceling.
mChooserCounts = stats.mChooserCounts;
}
+ /**
+ * {@hide}
+ */
+ public UsageStats getObfuscatedForInstantApp() {
+ final UsageStats ret = new UsageStats(this);
+
+ ret.mPackageName = UsageEvents.INSTANT_APP_PACKAGE_NAME;
+
+ return ret;
+ }
+
public String getPackageName() {
return mPackageName;
}
public abstract void applyRestoredPayload(int user, String key, byte[] payload);
- /* Cache Quota Service API */
+ /**
+ * Return usage stats.
+ *
+ * @param obfuscateInstantApps whether instant app package names need to be obfuscated in the
+ * result.
+ */
public abstract List<UsageStats> queryUsageStatsForUser(
- int userId, int interval, long beginTime, long endTime);
+ int userId, int interval, long beginTime, long endTime, boolean obfuscateInstantApps);
}
* Return the taget SDK version for the app with the given UID.
*/
public abstract int getUidTargetSdkVersion(int uid);
+
+ /** Whether the binder caller can access instant apps. */
+ public abstract boolean canAccessInstantApps(int callingUid);
}
return cur;
}
+ /**
+ * Returns whether or not a full application can see an instant application.
+ * <p>
+ * Currently, there are three cases in which this can occur:
+ * <ol>
+ * <li>The calling application is a "special" process. The special
+ * processes are {@link Process#SYSTEM_UID}, {@link Process#SHELL_UID}
+ * and {@code 0}</li>
+ * <li>The calling application has the permission
+ * {@link android.Manifest.permission#ACCESS_INSTANT_APPS}</li>
+ * <li>[TODO] The calling application is the default launcher on the
+ * system partition.</li>
+ * </ol>
+ */
+ private boolean canAccessInstantApps(int callingUid) {
+ final boolean isSpecialProcess =
+ callingUid == Process.SYSTEM_UID
+ || callingUid == Process.SHELL_UID
+ || callingUid == 0;
+ final boolean allowMatchInstant =
+ isSpecialProcess
+ || mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_INSTANT_APPS) == PERMISSION_GRANTED;
+ return allowMatchInstant;
+ }
private PackageInfo generatePackageInfo(PackageSetting ps, int flags, int userId) {
if (!sUserManager.exists(userId)) return null;
if (ps == null) {
if (p == null) {
return null;
}
+ final int callingUid = Binder.getCallingUid();
// Filter out ephemeral app metadata:
// * The system/shell/root can see metadata for any app
// * An installed app can see metadata for 1) other installed apps
// and 2) ephemeral apps that have explicitly interacted with it
// * Ephemeral apps can only see their own data and exposed installed apps
// * Holding a signature permission allows seeing instant apps
- final int callingAppId = UserHandle.getAppId(Binder.getCallingUid());
- if (callingAppId != Process.SYSTEM_UID
- && callingAppId != Process.SHELL_UID
- && callingAppId != Process.ROOT_UID
- && checkUidPermission(Manifest.permission.ACCESS_INSTANT_APPS,
- Binder.getCallingUid()) != PackageManager.PERMISSION_GRANTED) {
- final String instantAppPackageName = getInstantAppPackageName(Binder.getCallingUid());
+ if (!canAccessInstantApps(callingUid)) {
+ final String instantAppPackageName = getInstantAppPackageName(callingUid);
if (instantAppPackageName != null) {
// ephemeral apps can only get information on themselves or
// installed apps that are exposed.
} else {
if (ps.getInstantApp(userId)) {
// only get access to the ephemeral app if we've been granted access
+ final int callingAppId = UserHandle.getAppId(callingUid);
if (!mInstantAppRegistry.isInstantAccessGranted(
userId, callingAppId, ps.appId)) {
return null;
return getUidTargetSdkVersionLockedLPr(uid);
}
}
+
+ @Override
+ public boolean canAccessInstantApps(int callingUid) {
+ return PackageManagerService.this.canAccessInstantApps(callingUid);
+ }
}
@Override
UserInfo info = users.get(i);
List<UsageStats> stats =
mUsageStats.queryUsageStatsForUser(info.id, UsageStatsManager.INTERVAL_BEST,
- oneYearAgo, timeNow);
+ oneYearAgo, timeNow, /*obfuscateInstantApps=*/ false);
if (stats == null) {
continue;
}
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ParceledListSlice;
import android.content.pm.UserInfo;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import java.io.File;
AppOpsManager mAppOps;
UserManager mUserManager;
PackageManager mPackageManager;
+ PackageManagerInternal mPackageManagerInternal;
AppWidgetManager mAppWidgetManager;
IDeviceIdleController mDeviceIdleController;
private DisplayManager mDisplayManager;
mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
mPackageManager = getContext().getPackageManager();
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mHandler = new H(BackgroundThread.get().getLooper());
File systemDataDir = new File(Environment.getDataDirectory(), "system");
}
}
+ private boolean shouldObfuscateInstantAppsForCaller(int callingUid) {
+ return !mPackageManagerInternal.canAccessInstantApps(callingUid);
+ }
+
void clearAppIdleForPackage(String packageName, int userId) {
synchronized (mAppIdleLock) {
mAppIdleHistory.clearUsage(packageName, userId);
final long elapsedRealtime = SystemClock.elapsedRealtime();
convertToSystemTimeLocked(event);
+ if (event.getPackageName() != null
+ && mPackageManagerInternal.isPackageEphemeral(userId, event.getPackageName())) {
+ event.mFlags |= Event.FLAG_IS_PACKAGE_INSTANT_APP;
+ }
+
final UserUsageStatsService service =
getUserDataAndInitializeIfNeededLocked(userId, timeNow);
service.reportEvent(event);
/**
* Called by the Binder stub.
*/
- List<UsageStats> queryUsageStats(int userId, int bucketType, long beginTime, long endTime) {
+ List<UsageStats> queryUsageStats(int userId, int bucketType, long beginTime, long endTime,
+ boolean obfuscateInstantApps) {
synchronized (mLock) {
final long timeNow = checkAndGetTimeLocked();
if (!validRange(timeNow, beginTime, endTime)) {
final UserUsageStatsService service =
getUserDataAndInitializeIfNeededLocked(userId, timeNow);
- return service.queryUsageStats(bucketType, beginTime, endTime);
+ List<UsageStats> list = service.queryUsageStats(bucketType, beginTime, endTime);
+
+ // Mangle instant app names *using their current state (not whether they were ephemeral
+ // when the data was recorded)*.
+ if (obfuscateInstantApps) {
+ for (int i = list.size() - 1; i >= 0; i--) {
+ final UsageStats stats = list.get(i);
+ if (mPackageManagerInternal.isPackageEphemeral(userId, stats.mPackageName)) {
+ list.set(i, stats.getObfuscatedForInstantApp());
+ }
+ }
+ }
+
+ return list;
}
}
/**
* Called by the Binder stub.
*/
- UsageEvents queryEvents(int userId, long beginTime, long endTime) {
+ UsageEvents queryEvents(int userId, long beginTime, long endTime,
+ boolean shouldObfuscateInstantApps) {
synchronized (mLock) {
final long timeNow = checkAndGetTimeLocked();
if (!validRange(timeNow, beginTime, endTime)) {
final UserUsageStatsService service =
getUserDataAndInitializeIfNeededLocked(userId, timeNow);
- return service.queryEvents(beginTime, endTime);
+ return service.queryEvents(beginTime, endTime, shouldObfuscateInstantApps);
}
}
}
}
- boolean isAppIdleFilteredOrParoled(String packageName, int userId, long elapsedRealtime) {
+ boolean isAppIdleFilteredOrParoled(String packageName, int userId, long elapsedRealtime,
+ boolean shouldObfuscateInstantApps) {
if (isParoledOrCharging()) {
return false;
}
+ if (shouldObfuscateInstantApps &&
+ mPackageManagerInternal.isPackageEphemeral(userId, packageName)) {
+ return false;
+ }
return isAppIdleFiltered(packageName, getAppId(packageName), userId, elapsedRealtime);
}
return null;
}
+ final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller(
+ Binder.getCallingUid());
+
final int userId = UserHandle.getCallingUserId();
final long token = Binder.clearCallingIdentity();
try {
final List<UsageStats> results = UsageStatsService.this.queryUsageStats(
- userId, bucketType, beginTime, endTime);
+ userId, bucketType, beginTime, endTime, obfuscateInstantApps);
if (results != null) {
return new ParceledListSlice<>(results);
}
return null;
}
+ final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller(
+ Binder.getCallingUid());
+
final int userId = UserHandle.getCallingUserId();
final long token = Binder.clearCallingIdentity();
try {
- return UsageStatsService.this.queryEvents(userId, beginTime, endTime);
+ return UsageStatsService.this.queryEvents(userId, beginTime, endTime,
+ obfuscateInstantApps);
} finally {
Binder.restoreCallingIdentity(token);
}
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
+ final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller(
+ Binder.getCallingUid());
final long token = Binder.clearCallingIdentity();
try {
return UsageStatsService.this.isAppIdleFilteredOrParoled(packageName, userId,
- SystemClock.elapsedRealtime());
+ SystemClock.elapsedRealtime(), obfuscateInstantApps);
} finally {
Binder.restoreCallingIdentity(token);
}
@Override
public List<UsageStats> queryUsageStatsForUser(
- int userId, int intervalType, long beginTime, long endTime) {
+ int userId, int intervalType, long beginTime, long endTime,
+ boolean obfuscateInstantApps) {
return UsageStatsService.this.queryUsageStats(
- userId, intervalType, beginTime, endTime);
+ userId, intervalType, beginTime, endTime, obfuscateInstantApps);
}
}
}
import android.app.usage.UsageEvents;
import android.app.usage.UsageStats;
import android.content.res.Configuration;
+import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.Log;
+import android.util.LogWriter;
import java.io.IOException;
import java.net.ProtocolException;
* UsageStats reader/writer for version 1 of the XML format.
*/
final class UsageStatsXmlV1 {
+ private static final String TAG = "UsageStatsXmlV1";
+
private static final String PACKAGES_TAG = "packages";
private static final String PACKAGE_TAG = "package";
// Attributes
private static final String PACKAGE_ATTR = "package";
+ private static final String FLAGS_ATTR = "flags";
private static final String CLASS_ATTR = "class";
private static final String TOTAL_TIME_ACTIVE_ATTR = "timeActive";
private static final String COUNT_ATTR = "count";
if (pkg == null) {
throw new ProtocolException("no " + PACKAGE_ATTR + " attribute present");
}
-
final UsageStats stats = statsOut.getOrCreateUsageStats(pkg);
// Apply the offset to the beginTime to find the absolute time.
if (packageName == null) {
throw new ProtocolException("no " + PACKAGE_ATTR + " attribute present");
}
-
final String className = XmlUtils.readStringAttribute(parser, CLASS_ATTR);
final UsageEvents.Event event = statsOut.buildEvent(packageName, className);
+ event.mFlags = XmlUtils.readIntAttribute(parser, FLAGS_ATTR, 0);
+
// Apply the offset to the beginTime to find the absolute time of this event.
event.mTimeStamp = statsOut.beginTime + XmlUtils.readLongAttribute(parser, TIME_ATTR);
if (event.mClass != null) {
XmlUtils.writeStringAttribute(xml, CLASS_ATTR, event.mClass);
}
+ XmlUtils.writeIntAttribute(xml, FLAGS_ATTR, event.mFlags);
XmlUtils.writeIntAttribute(xml, TYPE_ATTR, event.mEventType);
switch (event.mEventType) {
import android.app.usage.ConfigurationStats;
import android.app.usage.TimeSparseArray;
import android.app.usage.UsageEvents;
-import android.app.usage.UsageEvents.Event;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.SystemClock;
import android.content.Context;
return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
}
- UsageEvents queryEvents(final long beginTime, final long endTime) {
+ UsageEvents queryEvents(final long beginTime, final long endTime,
+ boolean obfuscateInstantApps) {
final ArraySet<String> names = new ArraySet<>();
List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
return;
}
- final UsageEvents.Event event = stats.events.valueAt(i);
+ UsageEvents.Event event = stats.events.valueAt(i);
+ if (obfuscateInstantApps) {
+ event = event.getObfuscatedIfInstantApp();
+ }
names.add(event.mPackage);
if (event.mClass != null) {
names.add(event.mClass);
if (event.mShortcutId != null) {
pw.printPair("shortcutId", event.mShortcutId);
}
+ pw.printHexPair("flags", event.mFlags);
pw.println();
}
pw.decreaseIndent();
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+LOCAL_CERTIFICATE := platform
+
LOCAL_PACKAGE_NAME := UsageStatsTest
include $(BUILD_PACKAGE)
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Note: Add android:sharedUserId="android.uid.system" to the root element to simulate the system UID
+ caller case.
+-->
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.tests.usagestats">
+ package="com.android.tests.usagestats"
+ >
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/log"
android:title="View Log"/>
+ <item android:id="@+id/call_is_app_inactive"
+ android:title="Call isAppInactive()"/>
</menu>
package com.android.tests.usagestats;
+import android.app.AlertDialog;
import android.app.ListActivity;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
+import android.text.InputType;
+import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
+import android.widget.EditText;
import android.widget.TextView;
import java.util.ArrayList;
case R.id.log:
startActivity(new Intent(this, UsageLogActivity.class));
return true;
+ case R.id.call_is_app_inactive:
+ callIsAppInactive();
+ return true;
default:
return super.onOptionsItemSelected(item);
updateAdapter();
}
+ private void callIsAppInactive() {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Enter package name");
+ final EditText input = new EditText(this);
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ input.setHint("com.android.tests.usagestats");
+ builder.setView(input);
+
+ builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final String packageName = input.getText().toString().trim();
+ if (!TextUtils.isEmpty(packageName)) {
+ showInactive(packageName);
+ }
+ }
+ });
+ builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+
+ builder.show();
+ }
+
+ private void showInactive(String packageName) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(
+ "isAppInactive(\"" + packageName + "\") = "
+ + (mUsageStatsManager.isAppInactive(packageName) ? "true" : "false"));
+ builder.show();
+ }
+
private void updateAdapter() {
long now = System.currentTimeMillis();
long beginTime = now - USAGE_STATS_PERIOD;