summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSangyun Yun <sangyun@google.com>2023-02-06 19:25:32 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2023-02-06 19:25:32 +0000
commitf066efb37af0faacd79ec870f5f338ac6ff6a3ef (patch)
tree0efdbc994be787f5d6f1eb6cb3e0546eec952643
parent93498df0ec500468682ba38eedf84ebd393fe30d (diff)
parent2b027ec0d3db0eaafb35c23279580ccdf3945e04 (diff)
downloadTelephony-f066efb37af0faacd79ec870f5f338ac6ff6a3ef.tar.gz
Merge "TelephonyStatsLib, support handling atoms and metrics"
-rw-r--r--apex/Android.bp7
-rw-r--r--libs/TelephonyStatsLib/Android.bp55
-rw-r--r--libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPulled.java57
-rw-r--r--libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPushed.java39
-rw-r--r--libs/TelephonyStatsLib/src/com/android/telephony/statslib/PulledCallback.java30
-rw-r--r--libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLib.java200
-rw-r--r--libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibPulledAtomCallback.java156
-rw-r--r--libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibStorage.java271
-rw-r--r--libs/TelephonyStatsLib/tests/AndroidManifest.xml31
-rw-r--r--libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPulledTestInfo.java120
-rw-r--r--libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPushedTestInfo.java97
-rw-r--r--libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsSerializablePulledTestInfo.java143
-rw-r--r--libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibStorageTest.java252
-rw-r--r--libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibTest.java152
14 files changed, 1609 insertions, 1 deletions
diff --git a/apex/Android.bp b/apex/Android.bp
index 40840bd..41fa969 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -35,5 +35,10 @@ apex {
certificate: ":com.android.telephonymodules.certificate",
updatable: false,
system_ext_specific: true,
- apps: ["QualifiedNetworksService"],
+ apps: [
+ "QualifiedNetworksService",
+ ],
+ java_libs: [
+ "TelephonyStatsLib",
+ ],
}
diff --git a/libs/TelephonyStatsLib/Android.bp b/libs/TelephonyStatsLib/Android.bp
new file mode 100644
index 0000000..353075e
--- /dev/null
+++ b/libs/TelephonyStatsLib/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "TelephonyStatsLib",
+ srcs: [
+ "src/**/*.java",
+ ],
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.telephonymodules",
+ ],
+}
+
+android_test {
+ name: "TelephonyStatsLibTests",
+ manifest: "tests/AndroidManifest.xml",
+ srcs: [
+ "tests/**/*.java",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.mock",
+ "android.test.base"
+ ],
+ static_libs: [
+ "androidx.test.rules",
+ "androidx.test.core",
+ "mockito-target-extended-minus-junit4",
+ "platform-test-annotations",
+ "TelephonyStatsLib",
+ ],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ instrumentation_for: "TelephonyStatsLib",
+ test_suites: ["device-tests"],
+}
diff --git a/libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPulled.java b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPulled.java
new file mode 100644
index 0000000..e303b5c
--- /dev/null
+++ b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPulled.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.util.StatsEvent;
+
+/** AtomsPulled class */
+public abstract class AtomsPulled {
+
+ /** Constructor of AtomsPulled */
+ public AtomsPulled() {}
+
+ /**
+ * Write the atom information to be recorded to the builder according to the type in order.
+ *
+ * @param builder Builder class for StatsEvent Builder object.
+ */
+ public abstract void build(StatsEvent.Builder builder);
+
+ /** Return atom id defined in proto. */
+ public abstract int getStatsId();
+
+ /** Return copy of the AtomsPulled */
+ public abstract AtomsPulled copy();
+
+ /**
+ * Return dimension string for pulled atoms.
+ *
+ * <p>Used for Pulled Atoms only. Pulled atoms should report the accumulated data at the time of
+ * the callback. The same type of information should be reported at once. (e.g. one per
+ * NetCapability for CountHandoverFailure)
+ *
+ * @return key string of atoms dimension
+ */
+ public abstract String getDimension();
+
+ /**
+ * Accumulate info to this. Used for Pulled Atoms only.
+ *
+ * @param info atoms to be accumulated to
+ */
+ public abstract void accumulate(AtomsPulled info);
+}
diff --git a/libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPushed.java b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPushed.java
new file mode 100644
index 0000000..ab50441
--- /dev/null
+++ b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/AtomsPushed.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.util.StatsEvent;
+
+/** AtomsPushed class */
+public abstract class AtomsPushed {
+
+ /** Constructor of AtomsPushed */
+ public AtomsPushed() {}
+
+ /**
+ * Write the atom information to be recorded to the builder according to the type in order.
+ *
+ * @param builder Builder class for StatsEvent Builder object.
+ */
+ public abstract void build(StatsEvent.Builder builder);
+
+ /** Return atom id defined in proto. */
+ public abstract int getStatsId();
+
+ /** Return copy of the AtomsPushed */
+ public abstract AtomsPushed copy();
+}
diff --git a/libs/TelephonyStatsLib/src/com/android/telephony/statslib/PulledCallback.java b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/PulledCallback.java
new file mode 100644
index 0000000..f236f70
--- /dev/null
+++ b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/PulledCallback.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import java.util.List;
+
+/** PulledCallback class */
+public interface PulledCallback {
+ /**
+ * Pull data for the specified atom tag.
+ *
+ * @param atomTag atom tag
+ * @param data List of AtomsPulled data, filled AtomsPulled to be reported
+ */
+ void onPulledCallback(int atomTag, List<AtomsPulled> data);
+}
diff --git a/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLib.java b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLib.java
new file mode 100644
index 0000000..23bcb94
--- /dev/null
+++ b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLib.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.StatsEvent;
+import android.util.StatsLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/** Statslib class */
+public class StatsLib {
+
+ private static final String LOG_TAG = StatsLib.class.getSimpleName();
+ private static final boolean DBG = true;
+ private static final int DEFAULT_FREQUENCY_WRITING_PUSHED_ATOM_IN_MILLS = 50;
+
+ private final StatsLibPulledAtomCallback mStatsLibPulledAtomCallback;
+ private final WritePushedAtomHandler mHandler;
+
+ /** Default constructor. */
+ public StatsLib(Context context) {
+ mStatsLibPulledAtomCallback = new StatsLibPulledAtomCallback(context);
+ log("created StatsLib.");
+ HandlerThread handlerThread = new HandlerThread("StatsLibStorage");
+ handlerThread.start();
+ mHandler = new WritePushedAtomHandler(handlerThread.getLooper());
+ }
+
+ @VisibleForTesting
+ protected StatsLib(StatsLibPulledAtomCallback cb) {
+ mStatsLibPulledAtomCallback = cb;
+ log("created StatsLib.");
+ HandlerThread handlerThread = new HandlerThread("StatsLibStorage");
+ handlerThread.start();
+ mHandler = new WritePushedAtomHandler(handlerThread.getLooper());
+ }
+
+ /**
+ * Registers the target atom to be pulled.
+ *
+ * @param statsId the pulled atom tag to register to take data from.
+ */
+ public void registerPulledAtomCallback(int statsId) {
+ mStatsLibPulledAtomCallback.registerAtom(statsId);
+ }
+
+ /**
+ * Registers the target atom to be pulled.
+ *
+ * @param statsId The tag of the atom for this puller callback.
+ * @param callback callback to be registered.
+ */
+ public void registerPulledAtomCallback(int statsId, PulledCallback callback) {
+ mStatsLibPulledAtomCallback.registerAtom(statsId, callback);
+ }
+
+ /**
+ * checks whether stats id was already registered or not.
+ *
+ * @param statsId The tag of the atom for this puller callback.
+ * @return true already registered.
+ */
+ public boolean isRegisteredPulledAtomCallback(int statsId) {
+ return mStatsLibPulledAtomCallback.isRegisteredAtom(statsId);
+ }
+
+ /**
+ * Unregisters the target atom being pulled.
+ *
+ * @param statsId The tag of the atom to remove callback and tag
+ */
+ public void unregisterPulledAtomCallback(int statsId) {
+ if (isRegisteredPulledAtomCallback(statsId)) {
+ mStatsLibPulledAtomCallback.unregisterAtom(statsId);
+ }
+ }
+
+ /**
+ * Write the pushed atoms
+ *
+ * @param pushed AtomsPushed
+ */
+ public void write(AtomsPushed pushed) {
+ if (pushed == null) {
+ loge("writePushedAtoms: pushed is null");
+ return;
+ }
+ mHandler.sendMessage(Message.obtain(mHandler, 0, pushed));
+ }
+
+ protected void onWritePushedAtom(AtomsPushed pushed) {
+ final StatsEvent.Builder builder = StatsEvent.newBuilder();
+ builder.setAtomId(pushed.getStatsId());
+ pushed.build(builder);
+ builder.usePooledBuffer();
+ StatsLog.write(builder.build());
+ log("writePushedAtoms: pushed=" + pushed);
+
+ append(pushed);
+ }
+
+ private class WritePushedAtomHandler extends Handler {
+ /**
+ * Use the provided {@link Looper} instead of the default one.
+ *
+ * @param looper The looper, must not be null.
+ */
+ WritePushedAtomHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ try {
+ AtomsPushed pushed = (AtomsPushed) message.obj;
+ onWritePushedAtom(pushed);
+ /* Atom logging frequency should not exceed once per 10 milliseconds (i.e.
+ * consecutive atom calls should be at least 10 milliseconds apart). This ensures
+ * that our logging socket is not spammed so that the socket does not drop data. If
+ * your logging line might trigger frequently, we suggest putting a guardrail to
+ * check that at least 1 second has passed since the last atom push.
+ */
+ Thread.sleep(DEFAULT_FREQUENCY_WRITING_PUSHED_ATOM_IN_MILLS);
+ } catch (InterruptedException | ClassCastException e) {
+ loge("WritePushedAtomHandler, e:" + e);
+ }
+ }
+ }
+
+ /**
+ * Append the Pushed atoms
+ *
+ * @param pushed AtomsPushed
+ */
+ private void append(AtomsPushed pushed) {
+ StatsLibStorage storage = getStorage();
+ if (storage == null) {
+ loge("appendPushedAtoms: storage is null");
+ return;
+ }
+ storage.appendPushedAtoms(pushed);
+ log("appendPushedAtoms: pushed=" + pushed);
+ }
+
+ /**
+ * Append the Pulled atoms
+ *
+ * @param pulled AtomsPulled
+ */
+ public void append(AtomsPulled pulled) {
+ if (pulled == null) {
+ return;
+ }
+ StatsLibStorage storage = getStorage();
+ if (storage == null) {
+ loge("appendPulledAtoms: storage is null");
+ return;
+ }
+ if (!isRegisteredPulledAtomCallback(pulled.getStatsId())) {
+ registerPulledAtomCallback(pulled.getStatsId());
+ }
+ storage.appendPulledAtoms(pulled);
+ log("appendPulledAtoms: pulled=" + pulled);
+ }
+
+ private StatsLibStorage getStorage() {
+ if (mStatsLibPulledAtomCallback == null) {
+ return null;
+ }
+ return mStatsLibPulledAtomCallback.getStatsLibStorage();
+ }
+
+ private void log(String s) {
+ if (DBG) Log.d(LOG_TAG, s);
+ }
+
+ private void loge(String s) {
+ Log.e(LOG_TAG, s);
+ }
+}
diff --git a/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibPulledAtomCallback.java b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibPulledAtomCallback.java
new file mode 100644
index 0000000..2b47606
--- /dev/null
+++ b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibPulledAtomCallback.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.util.Log;
+import android.util.StatsEvent;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * StatsLibPulledAtomCallback class
+ *
+ * <p>This class registers to statsd. Called once a day for this class pull stat to send to statsd.
+ */
+class StatsLibPulledAtomCallback implements StatsManager.StatsPullAtomCallback {
+
+ private static final String LOG_TAG = StatsLibPulledAtomCallback.class.getSimpleName();
+ private static final boolean DBG = true;
+ private static final StatsManager.PullAtomMetadata POLICY_PULL_DAILY =
+ new StatsManager.PullAtomMetadata.Builder()
+ .setCoolDownMillis(5L)
+ .setTimeoutMillis(2L)
+ .build();
+ private final StatsManager mStatsManager;
+ private final StatsLibStorage mStatsLibStorage;
+ private final HashMap<Integer, PulledCallback> mRegisteredCallback;
+
+ /**
+ * Constructor of StatsLibPulledAtomCallback
+ *
+ * @param context Context
+ */
+ StatsLibPulledAtomCallback(Context context) {
+ mRegisteredCallback = new HashMap<>();
+ mStatsLibStorage = new StatsLibStorage(context);
+ mStatsManager = context.getSystemService(StatsManager.class);
+ log("created StatsLibPulledAtomCallback.");
+ }
+
+ /** get a StatsLibStorage, which stores pulled atoms */
+ StatsLibStorage getStatsLibStorage() {
+ return mStatsLibStorage;
+ }
+
+ private void log(String s) {
+ if (DBG) Log.d(LOG_TAG, s);
+ }
+
+ /**
+ * Register a callback
+ *
+ * @param atomTag The tag of the atom for this puller callback.
+ */
+ void registerAtom(int atomTag) {
+ registerAtom(atomTag, new EmptyCallback());
+ }
+
+ /**
+ * Register a callback
+ *
+ * @param atomTag The tag of the atom for this puller callback.
+ */
+ void registerAtom(int atomTag, PulledCallback callback) {
+ if (!mRegisteredCallback.containsKey(atomTag)) {
+ mStatsLibStorage.init(atomTag);
+ mStatsLibStorage.loadFromFile(atomTag);
+ mStatsManager.setPullAtomCallback(
+ atomTag, POLICY_PULL_DAILY, new MetricExecutor(), this);
+ mRegisteredCallback.put(atomTag, callback);
+ }
+ }
+
+ /**
+ * is registered a callback
+ *
+ * @param atomTag The tag of the atom for this puller callback.
+ */
+ boolean isRegisteredAtom(int atomTag) {
+ return mRegisteredCallback.containsKey(atomTag);
+ }
+
+ /**
+ * Register a callback
+ *
+ * @param atomTag The tag of the atom for this puller callback.
+ */
+ void unregisterAtom(int atomTag) {
+ if (mRegisteredCallback.containsKey(atomTag)) {
+ mStatsManager.clearPullAtomCallback(atomTag);
+ mRegisteredCallback.remove(atomTag);
+ mStatsLibStorage.saveToFile(atomTag);
+ }
+ }
+
+ @Override
+ public int onPullAtom(int atomTag, List<StatsEvent> data) {
+ log("onPullAtom: atomTag:" + atomTag);
+ AtomsPulled[] arrayPulled = getStatsLibStorage().popPulledAtoms(atomTag);
+ for (AtomsPulled pulled : arrayPulled) {
+ final StatsEvent.Builder builder = StatsEvent.newBuilder();
+ builder.setAtomId(pulled.getStatsId());
+ pulled.build(builder);
+ data.add(builder.build());
+ }
+
+ PulledCallback callback = mRegisteredCallback.get(atomTag);
+ if (callback == null) {
+ return StatsManager.PULL_SUCCESS;
+ }
+ List<AtomsPulled> list = new ArrayList<>();
+ callback.onPulledCallback(atomTag, list);
+ for (AtomsPulled pulled : list) {
+ final StatsEvent.Builder builder = StatsEvent.newBuilder();
+ builder.setAtomId(pulled.getStatsId());
+ pulled.build(builder);
+ data.add(builder.build());
+ }
+
+ return StatsManager.PULL_SUCCESS;
+ }
+
+ private static class MetricExecutor implements Executor {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+
+ @Override
+ public String toString() {
+ return "METRIC_EXECUTOR";
+ }
+ }
+
+ static class EmptyCallback implements PulledCallback {
+ public void onPulledCallback(int atomTag, List<AtomsPulled> data) {}
+ }
+}
diff --git a/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibStorage.java b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibStorage.java
new file mode 100644
index 0000000..8279052
--- /dev/null
+++ b/libs/TelephonyStatsLib/src/com/android/telephony/statslib/StatsLibStorage.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Stores and aggregates metrics for pulled atoms. */
+class StatsLibStorage {
+
+ private static final String LOG_TAG = StatsLibStorage.class.getSimpleName();
+ private static final boolean DBG = true;
+ private static final boolean TEST_DBG = true;
+
+ private static final String STORAGE_FILE =
+ StatsLibStorage.class.getSimpleName() + "_persist_ID.pb";
+ private final Handler mHandler;
+
+ private final Context mContext;
+ private final ConcurrentHashMap<Integer, List<AtomsPushed>> mPushed;
+ private final ConcurrentHashMap<Integer, ConcurrentHashMap<String, AtomsPulled>> mPulled;
+
+ /** Constructor of StatsLibStorage */
+ StatsLibStorage(Context context) {
+ mContext = context;
+ mPushed = new ConcurrentHashMap<>();
+ mPulled = new ConcurrentHashMap<>();
+ HandlerThread handlerThread = new HandlerThread("StatsLibStorage");
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ log("created StatsLibStorage.");
+ }
+
+ @VisibleForTesting
+ protected StatsLibStorage(
+ Context context,
+ ConcurrentHashMap<Integer, List<AtomsPushed>> pushedMap,
+ ConcurrentHashMap<Integer, ConcurrentHashMap<String, AtomsPulled>> pulledMap,
+ Handler testHandler) {
+ mContext = context;
+ mPushed = pushedMap;
+ mPulled = pulledMap;
+ mHandler = testHandler;
+ log("created StatsLibStorage for testing");
+ }
+
+ private void log(String s) {
+ if (DBG) Log.d(LOG_TAG, s);
+ }
+
+ private void loge(String s) {
+ Log.e(LOG_TAG, s);
+ }
+
+ private void logt(String s) {
+ if (TEST_DBG) Log.d(LOG_TAG, s);
+ }
+
+ /** Initialize storage for the given statsId. */
+ void init(int statsId) {
+ List<AtomsPushed> pushed = mPushed.get(statsId);
+ if (pushed != null) {
+ pushed.clear();
+ }
+ ConcurrentHashMap<String, AtomsPulled> pulled = mPulled.get(statsId);
+ if (pulled != null) {
+ pulled.clear();
+ }
+ }
+
+ /**
+ * Append the Pushed Atoms
+ *
+ * @param info AtomsInfoBase
+ */
+ void appendPushedAtoms(AtomsPushed info) {
+ List<AtomsPushed> atoms =
+ mPushed.computeIfAbsent(info.getStatsId(), k -> new ArrayList<>());
+ atoms.add(info.copy());
+ logt("appendPushedAtoms, AtomsPushed:" + info);
+ }
+
+ /** Returns the array of the stored atoms and deletes them all. */
+ AtomsPushed[] popPushedAtoms(int statsId) {
+ List<AtomsPushed> atoms = mPushed.computeIfAbsent(statsId, k -> new ArrayList<>());
+ AtomsPushed[] infos = atoms.toArray(new AtomsPushed[0]);
+ logt("popPushedAtoms, AtomsPushed:" + atoms);
+ atoms.clear();
+ return infos;
+ }
+
+ /**
+ * Append the Pulled Atoms
+ *
+ * @param info AtomsInfoBase
+ */
+ void appendPulledAtoms(AtomsPulled info) {
+ ConcurrentHashMap<String, AtomsPulled> atoms =
+ mPulled.computeIfAbsent(info.getStatsId(), k -> new ConcurrentHashMap<>());
+ AtomsPulled alreadyExistPulled = atoms.get(info.getDimension());
+ if (alreadyExistPulled == null) {
+ atoms.put(info.getDimension(), info.copy());
+ logt("appendPulledAtoms, AtomsPulled:" + info);
+ } else {
+ alreadyExistPulled.accumulate(info);
+ logt("appendPulledAtoms, alreadyExistPulled:" + alreadyExistPulled);
+ }
+ if (isSerializable(info)) {
+ saveToFile(info.getStatsId());
+ }
+ }
+
+ /** Returns the array of the stored atoms and deletes them all. */
+ AtomsPulled[] popPulledAtoms(int statsId) {
+ ConcurrentHashMap<String, AtomsPulled> atoms = mPulled.get(statsId);
+ if (atoms == null) {
+ return null;
+ }
+ AtomsPulled[] infos = atoms.values().toArray(new AtomsPulled[0]);
+ logt("popPulledAtoms, atoms:" + atoms);
+ atoms.clear();
+ saveToFile(statsId);
+ return infos;
+ }
+
+ /** save serializable pulled atoms to a backup file for given statsId. */
+ void saveToFile(int statsId) {
+ mHandler.post(() -> saveToFileImmediately(statsId));
+ }
+
+ /** save atoms to file run as thread */
+ private void saveToFileImmediately(int statsId) {
+ FileOutputStream fileOutputStream = null;
+ ObjectOutputStream objectOutputStream = null;
+ List<AtomsPulled> toFileList = getSerializablePulledAtoms(statsId);
+ try {
+ // TODO b/265727262 if possible, Requires changes to repositories that do not require
+ // selinux rule.
+ String filename = getFileName(statsId);
+ fileOutputStream = mContext.openFileOutput(filename, Context.MODE_PRIVATE);
+ objectOutputStream = new ObjectOutputStream(fileOutputStream);
+ objectOutputStream.writeObject(toFileList);
+ objectOutputStream.flush();
+ logt("saveToFileImmediately, " + filename + " saved, toFileList:" + toFileList);
+ } catch (IOException e) {
+ loge("cannot save atoms, e:" + e);
+ } finally {
+ if (fileOutputStream != null) {
+ try {
+ fileOutputStream.close();
+ } catch (IOException e) {
+ loge("exception in saveToFile filestream close, e:" + e);
+ }
+ }
+ if (objectOutputStream != null) {
+ try {
+ objectOutputStream.close();
+ } catch (IOException e) {
+ loge("exception in saveToFile object stream close, e:" + e);
+ }
+ }
+ }
+ }
+
+ private boolean isSerializable(AtomsPulled info) {
+ return info instanceof Serializable;
+ }
+
+ private String getFileName(int statsId) {
+ return (STORAGE_FILE).replace("ID", Integer.toString(statsId));
+ }
+
+ private List<AtomsPulled> getSerializablePulledAtoms(int statsId) {
+ ConcurrentHashMap<String, AtomsPulled> pulls =
+ mPulled.computeIfAbsent(statsId, k -> new ConcurrentHashMap<>());
+ List<AtomsPulled> serializable = new ArrayList<>();
+ for (AtomsPulled p : pulls.values()) {
+ if (isSerializable(p)) {
+ serializable.add(p);
+ }
+ }
+ return serializable;
+ }
+
+ private void setSerializablePulledAtoms(
+ int statsId, List<AtomsPulled> serializablePulledAtoms) {
+ if (serializablePulledAtoms == null || serializablePulledAtoms.isEmpty()) {
+ return;
+ }
+ ConcurrentHashMap<String, AtomsPulled> atoms =
+ mPulled.computeIfAbsent(statsId, k -> new ConcurrentHashMap<>());
+ for (AtomsPulled info : serializablePulledAtoms) {
+ AtomsPulled alreadyExistPulled = atoms.get(info.getDimension());
+ if (alreadyExistPulled == null) {
+ atoms.put(info.getDimension(), info.copy());
+ } else {
+ alreadyExistPulled.accumulate(info);
+ }
+ }
+ }
+
+
+ /** save serializable pulled atoms to a backup file for given statsId. */
+ void loadFromFile(int statsId) {
+ mHandler.post(() -> loadFromFileImmediately(statsId));
+ }
+
+ /** load atoms from file run as thread */
+ private void loadFromFileImmediately(int statsId) {
+ FileInputStream fileInputStream = null;
+ ObjectInputStream objectInputStream = null;
+ List<AtomsPulled> fromFileList;
+ try {
+ // TODO b/265727262 if possible, Requires changes to repositories that do not require
+ // selinux rule.
+ String filename = getFileName(statsId);
+ fileInputStream = mContext.openFileInput(filename);
+ objectInputStream = new ObjectInputStream(fileInputStream);
+ fromFileList = (ArrayList<AtomsPulled>) objectInputStream.readObject();
+ logt("loadFromFile, " + filename + " loaded, fromFileList:" + fromFileList);
+ setSerializablePulledAtoms(statsId, fromFileList);
+ } catch (IOException e) {
+ loge("IOException cannot load atoms, e:" + e);
+ } catch (ClassNotFoundException e) {
+ loge("ClassNotFoundException cannot load atoms, e:" + e);
+ } finally {
+ if (fileInputStream != null) {
+ try {
+ fileInputStream.close();
+ } catch (IOException e) {
+ loge("exception in loadFromFile filestream close, e:" + e);
+ }
+ }
+ if (objectInputStream != null) {
+ try {
+ objectInputStream.close();
+ } catch (IOException e) {
+ loge("exception in loadFromFile object stream close, e:" + e);
+ }
+ }
+ }
+ }
+}
diff --git a/libs/TelephonyStatsLib/tests/AndroidManifest.xml b/libs/TelephonyStatsLib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..13d43e9
--- /dev/null
+++ b/libs/TelephonyStatsLib/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2023 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.telephony.statslib">
+
+ <uses-sdk android:minSdkVersion="32" android:targetSdkVersion="33" />
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+ <application android:label="TelephonyStatsLibTests" android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.telephony.statslib"
+ android:label="Tests for TelephonyStatsLib">
+ </instrumentation>
+</manifest>
diff --git a/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPulledTestInfo.java b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPulledTestInfo.java
new file mode 100644
index 0000000..cb2ee42
--- /dev/null
+++ b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPulledTestInfo.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.util.StatsEvent;
+
+import java.util.Objects;
+
+/** AtomsPulledTestInfo class */
+public class AtomsPulledTestInfo extends AtomsPulled {
+
+ /** atom #1 : test atom #1 type */
+ private int mType;
+
+ /** atom #2 : test atom #2 count */
+ private int mCount;
+
+ /** Constructor of AtomsTestInfo */
+ public AtomsPulledTestInfo() {}
+
+ /** Constructor of AtomsPulledTestInfo */
+ public AtomsPulledTestInfo(int type, int count) {
+ mType = type;
+ mCount = count;
+ }
+
+ /**
+ * Copy Constructor of AtomsPulledTestInfo
+ *
+ * @param info The info param to copy from.
+ */
+ public AtomsPulledTestInfo(AtomsPulledTestInfo info) {
+ mType = info.mType;
+ mCount = info.mCount;
+ }
+
+ /**
+ * Write the atom information to be recorded to the builder according to the type in order.
+ *
+ * @param builder Builder class for StatsEvent Builder object.
+ */
+ @Override
+ public void build(StatsEvent.Builder builder) {
+ builder.writeInt(mType); // atom #1
+ builder.writeInt(mCount); // atom #2
+ }
+
+ /** Return atom id defined in proto. */
+ @Override
+ public int getStatsId() {
+ return 700;
+ }
+
+ /** Return copy of the AtomsTestInfo */
+ @Override
+ public AtomsPulled copy() {
+ return new AtomsPulledTestInfo(this);
+ }
+
+ @Override
+ public String getDimension() {
+ return Integer.toString(mType);
+ }
+
+ public void accumulate(AtomsPulled info) {
+ if (!(info instanceof AtomsPulledTestInfo)) {
+ return;
+ }
+ AtomsPulledTestInfo atomsPulledTestInfo = (AtomsPulledTestInfo) info;
+ this.mCount += atomsPulledTestInfo.getCount();
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int type) {
+ mType = type;
+ }
+
+ public int getCount() {
+ return mCount;
+ }
+
+ public void setCount(int count) {
+ mCount = count;
+ }
+
+ @Override
+ public String toString() {
+ return "AtomsPulledTestInfo{" + "mType=" + mType + ", mCount=" + mCount + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AtomsPulledTestInfo)) return false;
+ AtomsPulledTestInfo that = (AtomsPulledTestInfo) o;
+ return mType == that.mType && mCount == that.mCount;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mCount);
+ }
+}
diff --git a/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPushedTestInfo.java b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPushedTestInfo.java
new file mode 100644
index 0000000..baf0ba5
--- /dev/null
+++ b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsPushedTestInfo.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.util.StatsEvent;
+
+import java.util.Objects;
+
+/** AtomsTestInfo class */
+public class AtomsPushedTestInfo extends AtomsPushed {
+
+ /** atom #1 : test atom #1 */
+ private int mTestAtom;
+
+ /** Constructor of AtomsTestInfo */
+ public AtomsPushedTestInfo() {}
+
+ /**
+ * Constructor of AtomsTestInfo
+ *
+ * @param testAtom initial value
+ */
+ public AtomsPushedTestInfo(int testAtom) {
+ mTestAtom = testAtom;
+ }
+
+ /**
+ * Copy Constructor of AtomsTestInfo
+ *
+ * @param info The info param to copy from.
+ */
+ public AtomsPushedTestInfo(AtomsPushedTestInfo info) {
+ mTestAtom = info.mTestAtom;
+ }
+
+ /**
+ * Write the atom information to be recorded to the builder according to the type in order.
+ *
+ * @param builder Builder class for StatsEvent Builder object.
+ */
+ @Override
+ public void build(StatsEvent.Builder builder) {
+ builder.writeInt(mTestAtom); // atom #1
+ }
+
+ /** Return atom id defined in proto. */
+ @Override
+ public int getStatsId() {
+ return 701;
+ }
+
+ /** Return copy of the AtomsTestInfo */
+ @Override
+ public AtomsPushed copy() {
+ return new AtomsPushedTestInfo(this);
+ }
+
+ public int getTestAtom() {
+ return mTestAtom;
+ }
+
+ public void setTestAtom(int testAtom) {
+ mTestAtom = testAtom;
+ }
+
+ @Override
+ public String toString() {
+ return "AtomsPushedTestInfo{" + "mTestAtom=" + mTestAtom + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AtomsPushedTestInfo)) return false;
+ AtomsPushedTestInfo that = (AtomsPushedTestInfo) o;
+ return mTestAtom == that.mTestAtom;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTestAtom);
+ }
+}
diff --git a/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsSerializablePulledTestInfo.java b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsSerializablePulledTestInfo.java
new file mode 100644
index 0000000..1762173
--- /dev/null
+++ b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/AtomsSerializablePulledTestInfo.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import android.util.StatsEvent;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/** AtomsSerializablePulledTestInfo class */
+public class AtomsSerializablePulledTestInfo extends AtomsPulled implements Serializable {
+
+ private static final long serialVersionUID = 804402117L; // 0x2FF233C5
+
+ /** atom #1 : test atom #1 type */
+ private int mType;
+
+ /** atom #2 : test atom #2 count */
+ private int mCount;
+
+ /** transient variable which will not be persisted */
+ private transient int mTransient;
+
+ /** Constructor of AtomsTestInfo */
+ public AtomsSerializablePulledTestInfo() {}
+
+ /** Constructor of AtomsSerializablePulledTestInfo */
+ public AtomsSerializablePulledTestInfo(int type, int count) {
+ mType = type;
+ mCount = count;
+ }
+
+ /**
+ * Copy Constructor of AtomsSerializablePulledTestInfo
+ *
+ * @param info The info param to copy from.
+ */
+ public AtomsSerializablePulledTestInfo(AtomsSerializablePulledTestInfo info) {
+ mType = info.mType;
+ mCount = info.mCount;
+ }
+
+ /**
+ * Write the atom information to be recorded to the builder according to the type in order.
+ *
+ * @param builder Builder class for StatsEvent Builder object.
+ */
+ @Override
+ public void build(StatsEvent.Builder builder) {
+ builder.writeInt(mType); // atom #1
+ builder.writeInt(mCount); // atom #2
+ }
+
+ /** Return atom id defined in proto. */
+ @Override
+ public int getStatsId() {
+ return 702;
+ }
+
+ /** Return copy of the AtomsTestInfo */
+ @Override
+ public AtomsPulled copy() {
+ return new AtomsSerializablePulledTestInfo(this);
+ }
+
+ @Override
+ public String getDimension() {
+ return Integer.toString(mType);
+ }
+
+ public void accumulate(AtomsPulled info) {
+ if (!(info instanceof AtomsSerializablePulledTestInfo)) {
+ return;
+ }
+ AtomsSerializablePulledTestInfo atomsSerializablePulledTestInfo =
+ (AtomsSerializablePulledTestInfo) info;
+ this.mCount += atomsSerializablePulledTestInfo.getCount();
+ this.mTransient += atomsSerializablePulledTestInfo.getTransient();
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int type) {
+ mType = type;
+ }
+
+ public int getCount() {
+ return mCount;
+ }
+
+ public void setCount(int count) {
+ mCount = count;
+ }
+
+ public void setTransient(int aTransient) {
+ mTransient = aTransient;
+ }
+
+ public int getTransient() {
+ return mTransient;
+ }
+
+ @Override
+ public String toString() {
+ return "AtomsSerializablePulledTestInfo{"
+ + "mType="
+ + mType
+ + ", mCount="
+ + mCount
+ + ", mTransient="
+ + mTransient
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AtomsSerializablePulledTestInfo)) return false;
+ AtomsSerializablePulledTestInfo that = (AtomsSerializablePulledTestInfo) o;
+ return mType == that.mType && mCount == that.mCount && mTransient == that.mTransient;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mCount, mTransient);
+ }
+}
diff --git a/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibStorageTest.java b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibStorageTest.java
new file mode 100644
index 0000000..05f1bfa
--- /dev/null
+++ b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibStorageTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class StatsLibStorageTest {
+
+ private StatsLibStorage mStorage;
+ private ConcurrentHashMap<Integer, List<AtomsPushed>> mPushed;
+ private ConcurrentHashMap<Integer, ConcurrentHashMap<String, AtomsPulled>> mPulled;
+ private CountDownLatch mLatch;
+ private StorageTestHandler mStorageTestHandler;
+
+ @Before
+ public void setUp() {
+ Context context = spy(ApplicationProvider.getApplicationContext());
+ mPushed = new ConcurrentHashMap<>();
+ mPulled = new ConcurrentHashMap<>();
+ HandlerThread handlerThread = new HandlerThread("StatsLibStorage");
+ handlerThread.start();
+ mStorageTestHandler = new StorageTestHandler(handlerThread.getLooper());
+ mLatch = new CountDownLatch(1);
+ mStorage = new StatsLibStorage(context, mPushed, mPulled, mStorageTestHandler);
+ }
+
+ private class StorageTestHandler extends Handler {
+ StorageTestHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void dispatchMessage(Message msg) {
+ super.dispatchMessage(msg);
+ mLatch.countDown();
+ }
+ }
+
+ @After
+ public void tearDown() {
+ mStorage = null;
+ }
+
+ private void testPushedAppendAndPop(AtomsPushed first, AtomsPushed second, AtomsPushed third) {
+ assertNotEquals(first, second);
+ assertNotEquals(first, third);
+ assertNotEquals(second, third);
+
+ mStorage.appendPushedAtoms(first);
+ assertEquals(1, mPushed.get(first.getStatsId()).size());
+
+ mStorage.appendPushedAtoms(second);
+ assertEquals(2, mPushed.get(first.getStatsId()).size());
+
+ mStorage.appendPushedAtoms(third);
+ assertEquals(3, mPushed.get(first.getStatsId()).size());
+
+ AtomsPushed[] infos = mStorage.popPushedAtoms(first.getStatsId());
+ assertEquals(0, mPushed.get(first.getStatsId()).size());
+
+ assertEquals(first, infos[0]);
+ assertEquals(second, infos[1]);
+ assertEquals(third, infos[2]);
+ }
+
+ @Test
+ public void testPushedAtomsBaseInfo() {
+ AtomsPushedTestInfo first = new AtomsPushedTestInfo();
+ AtomsPushedTestInfo second = new AtomsPushedTestInfo();
+ AtomsPushedTestInfo third = new AtomsPushedTestInfo(3);
+
+ first.setTestAtom(1);
+ second.setTestAtom(2);
+
+ assertEquals(1, first.getTestAtom());
+ assertEquals(2, second.getTestAtom());
+
+ testPushedAppendAndPop(first, second, third);
+ }
+
+ @Test
+ public void testModifyInMiddleForAtomsTestInfo() {
+ AtomsPushedTestInfo first = new AtomsPushedTestInfo();
+ first.setTestAtom(1);
+ mStorage.appendPushedAtoms(first);
+ assertEquals(1, mPushed.get(first.getStatsId()).size());
+
+ AtomsPushedTestInfo copyFirst = new AtomsPushedTestInfo(first);
+ mStorage.appendPushedAtoms(copyFirst);
+ assertEquals(2, mPushed.get(first.getStatsId()).size());
+
+ // modified after append. storage should not be applied this.
+ copyFirst.setTestAtom(10);
+
+ AtomsPushed[] infos = mStorage.popPushedAtoms(first.getStatsId());
+ assertEquals(0, mPushed.get(first.getStatsId()).size());
+
+ assertEquals(first, infos[0]);
+ assertNotEquals(copyFirst, infos[1]);
+ }
+
+ @Test
+ public void testPulledAtomsBaseInfo() {
+ AtomsPulledTestInfo first = new AtomsPulledTestInfo();
+ AtomsPulledTestInfo second = new AtomsPulledTestInfo();
+ AtomsPulledTestInfo third = new AtomsPulledTestInfo(2, 300);
+
+ first.setType(1);
+ second.setType(1);
+
+ first.setCount(10);
+ second.setCount(20);
+
+ assertNotEquals(first, second);
+ assertNotEquals(first, third);
+ assertNotEquals(second, third);
+
+ int statsId = first.getStatsId();
+ assertEquals(statsId, second.getStatsId());
+ assertEquals(statsId, third.getStatsId());
+
+ int expectedLen = 1;
+ mStorage.appendPulledAtoms(first);
+ assertEquals(expectedLen, mPulled.get(statsId).size());
+
+ if (!first.getDimension().equals(second.getDimension())) {
+ expectedLen++;
+ }
+ mStorage.appendPulledAtoms(second);
+ assertEquals(expectedLen, mPulled.get(statsId).size());
+
+ if (!first.getDimension().equals(third.getDimension())) {
+ expectedLen++;
+ }
+ mStorage.appendPulledAtoms(third);
+ assertEquals(expectedLen, mPulled.get(statsId).size());
+
+ AtomsPulled[] infos = mStorage.popPulledAtoms(statsId);
+ assertEquals(0, mPulled.get(statsId).size());
+
+ for (AtomsPulled info : infos) {
+ if (!(info instanceof AtomsPulledTestInfo)) {
+ Assert.fail();
+ continue;
+ }
+ AtomsPulledTestInfo atomsPulledTestInfo = (AtomsPulledTestInfo) info;
+ if (atomsPulledTestInfo.getDimension().equals(first.getDimension())) {
+ assertEquals(atomsPulledTestInfo.getCount(), first.getCount() + second.getCount());
+ } else {
+ assertEquals(atomsPulledTestInfo.getCount(), third.getCount());
+ }
+ }
+ }
+
+ @Test
+ public void testSerializableAtoms() throws InterruptedException {
+ AtomsSerializablePulledTestInfo first = new AtomsSerializablePulledTestInfo();
+ AtomsSerializablePulledTestInfo second = new AtomsSerializablePulledTestInfo();
+ AtomsSerializablePulledTestInfo third = new AtomsSerializablePulledTestInfo(1, 10);
+
+ first.setType(1);
+ first.setCount(10);
+
+ second.setType(1);
+ second.setCount(10);
+
+ // third.setType(1);
+ // third.setCount(10);
+ third.setTransient(5000);
+
+ assertEquals(first, second);
+ assertNotEquals(first, third);
+
+ int statsId = first.getStatsId();
+ assertEquals(statsId, second.getStatsId());
+ assertEquals(statsId, third.getStatsId());
+
+ int expectedLen = 1;
+ // All dimensions are same in this test.
+ mLatch = new CountDownLatch(1);
+ mStorage.appendPulledAtoms(first);
+ assertEquals(expectedLen, mPulled.get(statsId).size());
+ mLatch.await(1, TimeUnit.SECONDS);
+
+ mLatch = new CountDownLatch(1);
+ mStorage.appendPulledAtoms(second);
+ assertEquals(expectedLen, mPulled.get(statsId).size());
+ mLatch.await(1, TimeUnit.SECONDS);
+
+ mLatch = new CountDownLatch(1);
+ mStorage.appendPulledAtoms(third);
+ assertEquals(expectedLen, mPulled.get(statsId).size());
+ mLatch.await(1, TimeUnit.SECONDS);
+
+ mLatch = new CountDownLatch(1);
+ mStorage.saveToFile(statsId);
+ mLatch.await(1, TimeUnit.SECONDS);
+
+ mLatch = new CountDownLatch(1);
+ mStorage.init(statsId);
+ mStorage.loadFromFile(statsId);
+ mLatch.await(1, TimeUnit.SECONDS);
+
+ AtomsPulled[] infos = mStorage.popPulledAtoms(statsId);
+ assertEquals(0, mPulled.get(statsId).size());
+ assertEquals(1, infos.length);
+
+ assertTrue(infos[0] instanceof AtomsSerializablePulledTestInfo);
+ AtomsSerializablePulledTestInfo result = (AtomsSerializablePulledTestInfo) infos[0];
+
+ assertEquals(30, result.getCount());
+ assertEquals(0, result.getTransient());
+ }
+}
diff --git a/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibTest.java b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibTest.java
new file mode 100644
index 0000000..bc53fae
--- /dev/null
+++ b/libs/TelephonyStatsLib/tests/com/android/telephony/statslib/StatsLibTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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.telephony.statslib;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.util.StatsLog;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class StatsLibTest {
+
+ private static final String RET_SUCCESS = "success";
+ private static final String RET_FAILED = "failed";
+
+ @Mock Context mMockContext;
+ @Mock StatsLibPulledAtomCallback mMockPulledAtomCallback;
+ @Mock StatsLibStorage mMockStorage;
+ @Mock PulledCallback mMockPulledCallback;
+ private StatsLib mStatsLib;
+ private MockitoSession mMockitoSession;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mMockContext = spy(ApplicationProvider.getApplicationContext());
+ mMockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(StatsLog.class)
+ .startMocking();
+ when(mMockPulledAtomCallback.getStatsLibStorage()).thenReturn(mMockStorage);
+ mStatsLib = new StatsLib(mMockPulledAtomCallback);
+ }
+
+ @After
+ public void tearDown() {
+ mStatsLib = null;
+ if (mMockitoSession != null) {
+ mMockitoSession.finishMocking();
+ mMockitoSession = null;
+ }
+ }
+
+ @Test
+ public void testWritePushedAtomHandler() {
+ AtomsPushedTestInfo first = new AtomsPushedTestInfo(1);
+ AtomsPushedTestInfo second = new AtomsPushedTestInfo(2);
+ AtomsPushedTestInfo third = new AtomsPushedTestInfo(3);
+
+ ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+ Callable<String> callableTask =
+ () -> {
+ mStatsLib.write(null);
+ mStatsLib.write(first);
+ mStatsLib.write(second);
+ mStatsLib.write(third);
+ return RET_SUCCESS;
+ };
+ int delay = 100;
+ ScheduledFuture<String> future =
+ executor.schedule(callableTask, delay, TimeUnit.MILLISECONDS);
+ String result;
+ try {
+ result = future.get();
+ } catch (ExecutionException | InterruptedException e) {
+ result = RET_FAILED;
+ }
+
+ assertEquals(result, RET_SUCCESS);
+ verify(mMockStorage, timeout(300).times(3)).appendPushedAtoms(any(AtomsPushed.class));
+ }
+
+ @Test
+ public void testRegisterPulledAtomCallback() {
+ AtomsPulledTestInfo first = new AtomsPulledTestInfo();
+ mStatsLib.registerPulledAtomCallback(first.getStatsId(), mMockPulledCallback);
+ verify(mMockPulledAtomCallback).registerAtom(first.getStatsId(), mMockPulledCallback);
+ }
+
+ @Test
+ public void testNullStorage() {
+ when(mMockPulledAtomCallback.getStatsLibStorage()).thenReturn(null);
+
+ mStatsLib.onWritePushedAtom(new AtomsPushedTestInfo());
+ mStatsLib.append(new AtomsPulledTestInfo());
+ verify(mMockStorage, never()).appendPushedAtoms(any());
+ verify(mMockStorage, never()).appendPulledAtoms(any());
+ }
+
+ @Test
+ public void testAppendPulledAtom() {
+ final int type1 = 1;
+ final int type2 = 2;
+ Random random = new Random();
+ int count1 = random.nextInt(100);
+ int count2 = random.nextInt(1000);
+ int count3 = random.nextInt(10000);
+ int count4 = random.nextInt(100000);
+
+ mStatsLib.append(null);
+ mStatsLib.append(new AtomsPulledTestInfo(type1, count1));
+ mStatsLib.append(new AtomsPulledTestInfo(type1, count2));
+ mStatsLib.append(new AtomsPulledTestInfo(type2, count3));
+ mStatsLib.append(new AtomsPulledTestInfo(type2, count4));
+
+ verify(mMockStorage, times(4)).appendPulledAtoms(any());
+ }
+}