diff options
author | Sangyun Yun <sangyun@google.com> | 2023-02-06 19:25:32 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2023-02-06 19:25:32 +0000 |
commit | f066efb37af0faacd79ec870f5f338ac6ff6a3ef (patch) | |
tree | 0efdbc994be787f5d6f1eb6cb3e0546eec952643 | |
parent | 93498df0ec500468682ba38eedf84ebd393fe30d (diff) | |
parent | 2b027ec0d3db0eaafb35c23279580ccdf3945e04 (diff) | |
download | Telephony-f066efb37af0faacd79ec870f5f338ac6ff6a3ef.tar.gz |
Merge "TelephonyStatsLib, support handling atoms and metrics"
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()); + } +} |