Merge "Add HiddenApiLogging sample"
diff --git a/HiddenApiLoggingApp/Android.bp b/HiddenApiLoggingApp/Android.bp
new file mode 100644
index 0000000..c29e7b0
--- /dev/null
+++ b/HiddenApiLoggingApp/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2019 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.
+
+android_app {
+    name: "HiddenApiLoggingApp",
+    certificate: "platform",
+    sdk_version: "system_current",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.core_core",
+        "androidx.fragment_fragment",
+    ],
+}
+
+java_binary_host {
+  name: "hidden_api_log_tracking",
+  manifest: "tool/hidden_api_log_tracking.mf",
+  srcs: [
+    "tool/src/com/hiddenapi/HiddenApiLogTracking.java",
+  ],
+  static_libs: [
+    "platformprotos",
+  ]
+}
\ No newline at end of file
diff --git a/HiddenApiLoggingApp/AndroidManifest.xml b/HiddenApiLoggingApp/AndroidManifest.xml
new file mode 100644
index 0000000..87e3081
--- /dev/null
+++ b/HiddenApiLoggingApp/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 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.
+ -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.hiddenapiapp">
+    <!-- Add permission for changing DeviceConfig -->
+    <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
+    <!-- Add permissions for StatsManager.addConfig -->
+    <uses-permission android:name="android.permission.DUMP" />
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <application
+        android:label="Hidden Api Logging Manager">
+        <receiver
+          android:name=".Receiver"
+          android:exported="true">
+      </receiver>
+    </application>
+</manifest>
diff --git a/HiddenApiLoggingApp/README b/HiddenApiLoggingApp/README
new file mode 100644
index 0000000..fe6af71
--- /dev/null
+++ b/HiddenApiLoggingApp/README
@@ -0,0 +1,12 @@
+HiddenApiLoggingApp.apk:
+
+Demonstrates how to obtain the hidden api logging data.
+Usage:
+1. mmma packages/experimental/HiddenApiLoggingApp -j
+2. adb install -r ${OUT}/system/app/HiddenApiLoggingApp/HiddenApiLoggingApp.apk
+3 ./out/host/linux-x86/bin/hidden_api_log_tracking
+4. <wait for the 'Waiting for hidden api accesses... Press Ctrl-c to read logs' prompt>
+5. <trigger hidden api usage>
+6. <press Ctrl-c to stop execution and read accumulated hidden api usage logs>
+
+Owner: Andrei Onea <andreionea@google.com>
diff --git a/HiddenApiLoggingApp/src/com/android/hiddenapiapp/Receiver.java b/HiddenApiLoggingApp/src/com/android/hiddenapiapp/Receiver.java
new file mode 100644
index 0000000..96e1172
--- /dev/null
+++ b/HiddenApiLoggingApp/src/com/android/hiddenapiapp/Receiver.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 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.hiddenapiapp;
+
+import android.app.StatsManager;
+import android.app.StatsManager.StatsUnavailableException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.DeviceConfig;
+import android.util.Log;
+import java.util.Base64;
+
+public class Receiver extends BroadcastReceiver {
+  public static final String SET_RATE = "com.android.hiddenapiapp.SET_RATE";
+  public static final String SET_CONFIG = "com.android.hiddenapiapp.SET_CONFIG";
+  public static final String GET_DATA = "com.android.hiddenapiapp.GET_DATA";
+  public static final String REMOVE_CONFIG = "com.android.hiddenapiapp.REMOVE_CONFIG";
+
+  // ID of the config to be sent to statsd; this is an arbitrary value - but, obviously,
+  // every config must have its own id (can be assigned either client or server side,
+  // depending on use case)
+  private static final long CONFIG_ID = 54321;
+  private static final String TAG = "HiddenApiLogging";
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    StatsManager statsManager = (StatsManager) context.getSystemService(StatsManager.class);
+    switch (intent.getAction()) {
+      case SET_RATE:
+        setLogRate(intent.getIntExtra("rate", 0));
+        setResult(1, null, null);
+        break;
+      case SET_CONFIG:
+        String config = intent.getStringExtra("config");
+        if (setupConfig(Base64.getDecoder().decode(config), statsManager)) {
+          setResult(1, null, null);
+        } else {
+          setResult(2, null, null);
+        }
+      case GET_DATA:
+        // This is for demo purposes only; in real applications, this data would be accumulated
+        // periodically in a buffer, and sent when the buffer fills up or otherwise convenient
+        setResult(1, Base64.getEncoder().encodeToString(getData(statsManager)), null);
+        break;
+      case REMOVE_CONFIG:
+        if (removeConfig(statsManager)) {
+          setResult(1, null, null);
+        } else {
+          setResult(2, null, null);
+        }
+      default:
+        setResult(2, null, null);
+    }
+  }
+
+  private void setLogRate(int rate) {
+    DeviceConfig.setProperty(
+        "app_compat",
+        "hidden_api_access_statslog_sampling_rate",
+        new Integer(rate).toString(),
+        false);
+  }
+
+  private boolean setupConfig(byte[] config, StatsManager statsManager) {
+    if (statsManager == null) {
+      return false;
+    }
+    try {
+      statsManager.addConfig(CONFIG_ID, config);
+      return true;
+    } catch (StatsUnavailableException e) {
+      Log.d(TAG, "Failed to send config to statsd");
+      return false;
+    }
+  }
+
+  private byte[] getData(StatsManager statsManager) {
+    if (statsManager == null) {
+      return null;
+    }
+    try {
+      return statsManager.getReports(CONFIG_ID);
+    } catch (StatsUnavailableException e) {
+      return null;
+    }
+  }
+
+  private boolean removeConfig(StatsManager statsManager) {
+    if (statsManager == null) {
+      return false;
+    }
+    try {
+      statsManager.removeConfig(CONFIG_ID);
+      statsManager = null;
+      return true;
+    } catch (StatsUnavailableException e) {
+      Log.d(TAG, "Failed to remove config from statsd");
+      return false;
+    }
+  }
+}
diff --git a/HiddenApiLoggingApp/tool/hidden_api_log_tracking.mf b/HiddenApiLoggingApp/tool/hidden_api_log_tracking.mf
new file mode 100644
index 0000000..6249801
--- /dev/null
+++ b/HiddenApiLoggingApp/tool/hidden_api_log_tracking.mf
@@ -0,0 +1 @@
+Main-Class: com.hiddenapi.HiddenApiLogTracking
diff --git a/HiddenApiLoggingApp/tool/src/com/hiddenapi/HiddenApiLogTracking.java b/HiddenApiLoggingApp/tool/src/com/hiddenapi/HiddenApiLogTracking.java
new file mode 100644
index 0000000..75f0cd2
--- /dev/null
+++ b/HiddenApiLoggingApp/tool/src/com/hiddenapi/HiddenApiLogTracking.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 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.hiddenapi;
+
+import com.android.internal.os.StatsdConfigProto.AtomMatcher;
+import com.android.internal.os.StatsdConfigProto.EventMetric;
+import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.ConfigMetricsReportList;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Base64;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+// This tool represents an approximation of what a server would be doing
+// - except the directionality of the flow of data is reversed at times;
+// The device should send its data directly to the server when its internal
+// buffers are full or when otherwise convenient (e.g. connecting to Wi-fi)
+public class HiddenApiLogTracking {
+  private static String runOnDevice(String cmd) {
+    String[] finalCommand = ("adb shell " + cmd).split(" ");
+    System.out.println(String.join(" ", finalCommand));
+    ProcessBuilder pb = new ProcessBuilder(finalCommand);
+    Process process;
+    try {
+      process = pb.start();
+    } catch (IOException e) {
+      throw new RuntimeException("Could not start new process.", e);
+    }
+    StringBuilder output = new StringBuilder();
+    try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+      for (String line = br.readLine(); line != null; line = br.readLine()) {
+        output.append(line).append('\n');
+      }
+      process.waitFor();
+      return output.toString();
+    } catch (Exception e) {
+      process.destroy();
+      throw new RuntimeException("Failed to read command output", e);
+    }
+  }
+
+  private static String createConfig() {
+    StatsdConfig.Builder builder = StatsdConfig.newBuilder().setId(54321);
+    final String atomName = "Atom" + System.nanoTime();
+    final String eventName = "Event" + System.nanoTime();
+
+    SimpleAtomMatcher.Builder sam =
+        SimpleAtomMatcher.newBuilder().setAtomId(Atom.HIDDEN_API_USED_FIELD_NUMBER);
+    builder.addAtomMatcher(
+        AtomMatcher.newBuilder().setId(atomName.hashCode()).setSimpleAtomMatcher(sam));
+    builder.addEventMetric(
+        EventMetric.newBuilder().setId(eventName.hashCode()).setWhat(atomName.hashCode()));
+    builder.addAllowedLogSource("AID_STATSD");
+    return Base64.getEncoder().encodeToString(builder.build().toByteArray());
+  }
+
+  private static void processData(String dataAsString) {
+    dataAsString = dataAsString.substring(1, dataAsString.length() - 1);
+    byte[] data = Base64.getDecoder().decode(dataAsString);
+    try {
+      ConfigMetricsReportList cmrl = ConfigMetricsReportList.parser().parseFrom(data);
+      String output =
+          cmrl.getReportsList().stream()
+              .flatMap(report -> report.getMetricsList().stream())
+              .flatMap(metric -> metric.getEventMetrics().getDataList().stream())
+              .map(eventMetric -> eventMetric.getAtom())
+              .map(atom -> atom.getHiddenApiUsed().getSignature())
+              .collect(Collectors.joining("\n"));
+      System.out.println("Used hidden apis");
+      System.out.println(output);
+    } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+      throw new RuntimeException("Cannot parse protocol buffer.", e);
+    }
+  }
+
+  public static void main(String[] args) {
+    System.out.println("Setting rate to max rate");
+    runOnDevice(
+        "am broadcast -n com.android.hiddenapiapp/.Receiver "
+            + "-a com.android.hiddenapiapp.SET_RATE --ei rate 65535");
+    System.out.println("Sending config");
+    String config = createConfig();
+    runOnDevice(
+        "am broadcast -n com.android.hiddenapiapp/.Receiver "
+            + "-a com.android.hiddenapiapp.SET_CONFIG --es config "
+            + config);
+
+    System.out.println("Waiting for hidden api accesses... Press enter to read logs");
+
+    try {
+      System.in.read();
+    } catch (IOException e) {
+      System.err.println("There was an IO error");
+      e.printStackTrace(System.err);
+      System.err.println("Proceeding normally...");
+    }
+
+    String output =
+        runOnDevice(
+            "am broadcast -n com.android.hiddenapiapp/.Receiver "
+                + "-a com.android.hiddenapiapp.GET_DATA");
+    Pattern p = Pattern.compile("\"([^\"]+)\"");
+    Matcher m = p.matcher(output);
+    if (m.find()) {
+      processData(m.group(0));
+    } else {
+      System.out.println("No config received!");
+    }
+
+    runOnDevice(
+        "am broadcast -n com.android.hiddenapiapp/.Receiver "
+            + "-a com.android.hiddenapiapp.REMOVE_CONFIG");
+  }
+}