Implement dumpsys --checkin for shortcut manager
Dump the very basic stuff for now.
We need to update GMS-core to actually collect the information.
Bug 28535604
Change-Id: I6ce17ee2014786a0ef97f3dc973b8a01c2d2a814
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index e667838..df51923 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -25,6 +25,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.ShortcutUser.PackageWithUser;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -288,6 +290,15 @@
}
}
+ @Override
+ public JSONObject dumpCheckin(boolean clear) throws JSONException {
+ final JSONObject result = super.dumpCheckin(clear);
+
+ // Nothing really interesting to dump.
+
+ return result;
+ }
+
@VisibleForTesting
ArraySet<String> getAllPinnedShortcutsForTest(String packageName, int packageUserId) {
return new ArraySet<>(mPinnedShortcuts.get(PackageWithUser.of(packageUserId, packageName)));
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 3c18198..7d57f33 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -36,6 +36,8 @@
import com.android.server.pm.ShortcutService.ShortcutOperation;
import com.android.server.pm.ShortcutService.Stats;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -93,6 +95,12 @@
private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
private static final String ATTR_NAME_XMLUTILS = "name";
+ private static final String KEY_DYNAMIC = "dynamic";
+ private static final String KEY_MANIFEST = "manifest";
+ private static final String KEY_PINNED = "pinned";
+ private static final String KEY_BITMAPS = "bitmaps";
+ private static final String KEY_BITMAP_BYTES = "bitmapBytes";
+
/**
* All the shortcuts from the package, keyed on IDs.
*/
@@ -1199,6 +1207,42 @@
}
@Override
+ public JSONObject dumpCheckin(boolean clear) throws JSONException {
+ final JSONObject result = super.dumpCheckin(clear);
+
+ int numDynamic = 0;
+ int numPinned = 0;
+ int numManifest = 0;
+ int numBitmaps = 0;
+ long totalBitmapSize = 0;
+
+ final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
+ final int size = shortcuts.size();
+ for (int i = 0; i < size; i++) {
+ final ShortcutInfo si = shortcuts.valueAt(i);
+
+ if (si.isDynamic()) numDynamic++;
+ if (si.isDeclaredInManifest()) numManifest++;
+ if (si.isPinned()) numPinned++;
+
+ if (si.getBitmapPath() != null) {
+ numBitmaps++;
+ totalBitmapSize += new File(si.getBitmapPath()).length();
+ }
+ }
+
+ result.put(KEY_DYNAMIC, numDynamic);
+ result.put(KEY_MANIFEST, numManifest);
+ result.put(KEY_PINNED, numPinned);
+ result.put(KEY_BITMAPS, numBitmaps);
+ result.put(KEY_BITMAP_BYTES, totalBitmapSize);
+
+ // TODO Log update frequency too.
+
+ return result;
+ }
+
+ @Override
public void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
throws IOException, XmlPullParserException {
final int size = mShortcuts.size();
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
index 26b52e9..79b5c4e 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
@@ -21,6 +21,8 @@
import com.android.internal.util.Preconditions;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -31,6 +33,7 @@
*/
abstract class ShortcutPackageItem {
private static final String TAG = ShortcutService.TAG;
+ private static final String KEY_NAME = "name";
private final int mPackageUserId;
private final String mPackageName;
@@ -137,6 +140,12 @@
public abstract void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
throws IOException, XmlPullParserException;
+ public JSONObject dumpCheckin(boolean clear) throws JSONException {
+ final JSONObject result = new JSONObject();
+ result.put(KEY_NAME, mPackageName);
+ return result;
+ }
+
/**
* Verify various internal states.
*/
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 10f1b4b..a91e284 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -95,6 +95,9 @@
import libcore.io.IoUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -184,6 +187,10 @@
private static final String LAUNCHER_INTENT_CATEGORY = Intent.CATEGORY_LAUNCHER;
+ private static final String KEY_SHORTCUT = "shortcut";
+ private static final String KEY_LOW_RAM = "lowRam";
+ private static final String KEY_ICON_SIZE = "iconSize";
+
@VisibleForTesting
interface ConfigConstants {
/**
@@ -1352,10 +1359,18 @@
if (isCallerSystem()) {
return;
}
- injectEnforceCallingPermission(
+ enforceCallingOrSelfPermission(
android.Manifest.permission.RESET_SHORTCUT_MANAGER_THROTTLING, null);
}
+ private void enforceCallingOrSelfPermission(
+ @NonNull String permission, @Nullable String message) {
+ if (isCallerSystem()) {
+ return;
+ }
+ injectEnforceCallingPermission(permission, message);
+ }
+
/**
* Somehow overriding ServiceContext.enforceCallingPermission() in the unit tests would confuse
* mockito. So instead we extracted it here and override it in the tests.
@@ -2981,20 +2996,29 @@
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
- != PackageManager.PERMISSION_GRANTED) {
- pw.println("Permission Denial: can't dump UserManager from from pid="
- + Binder.getCallingPid()
- + ", uid=" + Binder.getCallingUid()
- + " without permission "
- + android.Manifest.permission.DUMP);
- return;
+ enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
+ "can't dump by this caller");
+ boolean checkin = false;
+ boolean clear = false;
+ if (args != null) {
+ for (String arg : args) {
+ if ("-c".equals(arg)) {
+ checkin = true;
+ } else if ("--checkin".equals(arg)) {
+ checkin = true;
+ clear = true;
+ }
+ }
}
- dumpInner(pw, args);
+
+ if (checkin) {
+ dumpCheckin(pw, clear);
+ } else {
+ dumpInner(pw);
+ }
}
- @VisibleForTesting
- void dumpInner(PrintWriter pw, String[] args) {
+ private void dumpInner(PrintWriter pw) {
synchronized (mLock) {
final long now = injectCurrentTimeMillis();
pw.print("Now: [");
@@ -3106,6 +3130,34 @@
(count == 0 ? 0 : ((double) dur) / count)));
}
+ /**
+ * Dumpsys for checkin.
+ *
+ * @param clear if true, clear the history information. Some other system services have this
+ * behavior but shortcut service doesn't for now.
+ */
+ private void dumpCheckin(PrintWriter pw, boolean clear) {
+ synchronized (mLock) {
+ try {
+ final JSONArray users = new JSONArray();
+
+ for (int i = 0; i < mUsers.size(); i++) {
+ users.put(mUsers.valueAt(i).dumpCheckin(clear));
+ }
+
+ final JSONObject result = new JSONObject();
+
+ result.put(KEY_SHORTCUT, users);
+ result.put(KEY_LOW_RAM, injectIsLowRamDevice());
+ result.put(KEY_ICON_SIZE, mMaxIconDimension);
+
+ pw.println(result.toString(1));
+ } catch (JSONException e) {
+ Slog.e(TAG, "Unable to write in json", e);
+ }
+ }
+ }
+
// === Shell support ===
@Override
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index 3a43ece..21e4165 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -32,6 +32,9 @@
import libcore.util.Objects;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -55,6 +58,9 @@
private static final String ATTR_VALUE = "value";
private static final String ATTR_KNOWN_LOCALES = "locales";
private static final String ATTR_LAST_APP_SCAN_TIME = "last-app-scan-time";
+ private static final String KEY_USER_ID = "userId";
+ private static final String KEY_LAUNCHERS = "launchers";
+ private static final String KEY_PACKAGES = "packages";
static final class PackageWithUser {
final int userId;
@@ -503,4 +509,28 @@
pw.print(Formatter.formatFileSize(mService.mContext, size));
pw.println(")");
}
+
+ public JSONObject dumpCheckin(boolean clear) throws JSONException {
+ final JSONObject result = new JSONObject();
+
+ result.put(KEY_USER_ID, mUserId);
+
+ {
+ final JSONArray launchers = new JSONArray();
+ for (int i = 0; i < mLaunchers.size(); i++) {
+ launchers.put(mLaunchers.valueAt(i).dumpCheckin(clear));
+ }
+ result.put(KEY_LAUNCHERS, launchers);
+ }
+
+ {
+ final JSONArray packages = new JSONArray();
+ for (int i = 0; i < mPackages.size(); i++) {
+ packages.put(mPackages.valueAt(i).dumpCheckin(clear));
+ }
+ result.put(KEY_PACKAGES, packages);
+ }
+
+ return result;
+ }
}
diff --git a/services/tests/servicestests/assets/shortcut/dumpsys_expected.txt b/services/tests/servicestests/assets/shortcut/dumpsys_expected.txt
new file mode 100644
index 0000000..eed2087
--- /dev/null
+++ b/services/tests/servicestests/assets/shortcut/dumpsys_expected.txt
@@ -0,0 +1,105 @@
+{
+ "shortcut": [
+ {
+ "userId": 0,
+ "launchers": [
+ {
+ "name": "com.android.launcher.1"
+ },
+ {
+ "name": "com.android.launcher.2"
+ },
+ {
+ "name": "com.android.launcher.3"
+ },
+ {
+ "name": "com.android.launcher.4"
+ },
+ {
+ "name": "com.android.launcher.1"
+ }
+ ],
+ "packages": [
+ {
+ "name": "com.android.test.1",
+ "dynamic": 3,
+ "manifest": 0,
+ "pinned": 4,
+ "bitmaps": 0,
+ "bitmapBytes": 0
+ },
+ {
+ "name": "com.android.test.2",
+ "dynamic": 4,
+ "manifest": 0,
+ "pinned": 5,
+ "bitmaps": 2,
+ "bitmapBytes": ***BITMAP_SIZE***
+ },
+ {
+ "name": "com.android.test.3",
+ "dynamic": 3,
+ "manifest": 0,
+ "pinned": 6,
+ "bitmaps": 0,
+ "bitmapBytes": 0
+ },
+ {
+ "name": "com.android.test.4",
+ "dynamic": 0,
+ "manifest": 0,
+ "pinned": 0,
+ "bitmaps": 0,
+ "bitmapBytes": 0
+ }
+ ]
+ },
+ {
+ "userId": 10,
+ "launchers": [
+ {
+ "name": "com.android.launcher.1"
+ }
+ ],
+ "packages": [
+ {
+ "name": "com.android.test.1",
+ "dynamic": 3,
+ "manifest": 0,
+ "pinned": 2,
+ "bitmaps": 0,
+ "bitmapBytes": 0
+ }
+ ]
+ },
+ {
+ "userId": 20,
+ "launchers": [
+ {
+ "name": "com.android.launcher.1"
+ },
+ {
+ "name": "com.android.launcher.2"
+ },
+ {
+ "name": "com.android.launcher.3"
+ },
+ {
+ "name": "com.android.launcher.1"
+ }
+ ],
+ "packages": [
+ {
+ "name": "com.android.test.1",
+ "dynamic": 3,
+ "manifest": 0,
+ "pinned": 6,
+ "bitmaps": 0,
+ "bitmapBytes": 0
+ }
+ ]
+ }
+ ],
+ "lowRam": false,
+ "iconSize": 128
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 037b24e..5864255 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -93,6 +93,8 @@
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
@@ -1106,17 +1108,32 @@
protected void dumpsysOnLogcat(String message, boolean force) {
if (force || !ENABLE_DUMP) return;
- final ByteArrayOutputStream out = new ByteArrayOutputStream();
- final PrintWriter pw = new PrintWriter(out);
- mService.dumpInner(pw, null);
- pw.close();
-
Log.v(TAG, "Dumping ShortcutService: " + message);
- for (String line : out.toString().split("\n")) {
+ for (String line : dumpsys(null).split("\n")) {
Log.v(TAG, line);
}
}
+ protected String dumpCheckin() {
+ return dumpsys(new String[]{"--checkin"});
+ }
+
+ private String dumpsys(String[] args) {
+ final ArrayList<String> origPermissions = new ArrayList<>(mCallerPermissions);
+ mCallerPermissions.add(android.Manifest.permission.DUMP);
+ try {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final PrintWriter pw = new PrintWriter(out);
+ mService.dump(/* fd */ null, pw, args);
+ pw.close();
+
+ return out.toString();
+ } finally {
+ mCallerPermissions.clear();
+ mCallerPermissions.addAll(origPermissions);
+ }
+ }
+
/**
* For debugging, dump arbitrary file on logcat.
*/
@@ -1793,4 +1810,18 @@
}
return actualShortcuts;
}
+
+ public String readTestAsset(String assetPath) throws IOException {
+ final StringBuilder sb = new StringBuilder();
+ try (BufferedReader br = new BufferedReader(
+ new InputStreamReader(
+ getTestContext().getResources().getAssets().open(assetPath)))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ sb.append(System.lineSeparator());
+ }
+ }
+ return sb.toString();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index bd413be..dc70583 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -47,6 +47,10 @@
import com.android.frameworks.servicestests.R;
import com.android.server.pm.ShortcutService.ConfigConstants;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.Locale;
/**
@@ -1852,4 +1856,46 @@
ShortcutInfo.lookUpResourceId(res, "drawable/black_16x64", null,
getTestContext().getPackageName()));
}
+
+ public void testDumpCheckin() throws IOException {
+ prepareCrossProfileDataSet();
+
+ // prepareCrossProfileDataSet() doesn't set any icons, so do set here.
+ final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
+ final Icon res64x64 = Icon.createWithResource(getTestContext(), R.drawable.black_64x64);
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+ final Icon bmp64x64 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_64x64));
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("res32x32", res32x32),
+ makeShortcutWithIcon("res64x64", res64x64),
+ makeShortcutWithIcon("bmp32x32", bmp32x32),
+ makeShortcutWithIcon("bmp64x64", bmp64x64))));
+ });
+ // We can't predict the compressed bitmap sizes, so get the real sizes here.
+ final long bitmapTotal =
+ new File(getPackageShortcut(CALLING_PACKAGE_2, "bmp32x32", USER_0)
+ .getBitmapPath()).length() +
+ new File(getPackageShortcut(CALLING_PACKAGE_2, "bmp64x64", USER_0)
+ .getBitmapPath()).length();
+
+ // Read the expected output and inject the bitmap size.
+ final String expected = readTestAsset("shortcut/dumpsys_expected.txt")
+ .replace("***BITMAP_SIZE***", String.valueOf(bitmapTotal));
+
+ assertEquals(expected, dumpCheckin());
+ }
+
+ public void testDumpsysNoPermission() {
+ assertExpectException(SecurityException.class, "android.permission.DUMP",
+ () -> mService.dump(null, new PrintWriter(new StringWriter()), null));
+
+ // System can call it without the permission.
+ runWithSystemUid(() -> {
+ mService.dump(null, new PrintWriter(new StringWriter()), null);
+ });
+ }
}