Merge "Implement dumpsys --checkin for shortcut manager" into nyc-mr1-dev
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);
+        });
+    }
 }