From 65e0ade1514f99893890270f5602212085276ea3 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Wed, 19 May 2021 11:46:08 -0700 Subject: [PATCH] Add initial Ledger implementation. Add the ledger for an app's current balance and transactions. Writing to and reading from disk will be added later. Bug: 158300259 Test: Android builds Change-Id: Ica9f5d5957f710538c9d8741555f0568fc0bc7ee --- .../java/com/android/server/tare/Ledger.java | 120 ++++++++++++++++++ .../com/android/server/tare/TareUtils.java | 27 ++++ .../com/android/server/tare/LedgerTest.java | 75 +++++++++++ 3 files changed, 222 insertions(+) create mode 100644 apex/jobscheduler/service/java/com/android/server/tare/Ledger.java create mode 100644 apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java create mode 100644 services/tests/servicestests/src/com/android/server/tare/LedgerTest.java diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java new file mode 100644 index 0000000000000..e2fd8a464dfd2 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2021 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.server.tare; + +import static android.text.format.DateUtils.HOUR_IN_MILLIS; + +import static com.android.server.tare.TareUtils.narcToArc; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.IndentingPrintWriter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Ledger to track the last recorded balance and recent activities of an app. + */ +class Ledger { + static class Transaction { + public final long startTimeMs; + public final long endTimeMs; + @NonNull + public final String reason; + @Nullable + public final String tag; + public final long delta; + + Transaction(long startTimeMs, long endTimeMs, + @NonNull String reason, @Nullable String tag, long delta) { + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; + this.reason = reason; + this.tag = tag; + this.delta = delta; + } + } + + /** Last saved balance. This doesn't take currently ongoing events into account. */ + private long mCurrentBalance = 0; + private final List mTransactions = new ArrayList<>(); + private final ArrayMap mCumulativeDeltaPerReason = new ArrayMap<>(); + private long mEarliestSumTime; + + Ledger() { + } + + long getCurrentBalance() { + return mCurrentBalance; + } + + void recordTransaction(@NonNull Transaction transaction) { + mTransactions.add(transaction); + mCurrentBalance += transaction.delta; + + Long sum = mCumulativeDeltaPerReason.get(transaction.reason); + if (sum == null) { + sum = 0L; + } + sum += transaction.delta; + mCumulativeDeltaPerReason.put(transaction.reason, sum); + mEarliestSumTime = Math.min(mEarliestSumTime, transaction.startTimeMs); + } + + long get24HourSum(@NonNull String reason, final long now) { + final long windowStartTime = now - 24 * HOUR_IN_MILLIS; + if (mEarliestSumTime < windowStartTime) { + // Need to redo sums + mCumulativeDeltaPerReason.clear(); + for (int i = mTransactions.size() - 1; i >= 0; --i) { + final Transaction transaction = mTransactions.get(i); + if (transaction.endTimeMs <= windowStartTime) { + break; + } + final Long sumObj = mCumulativeDeltaPerReason.get(transaction.reason); + long sum = sumObj == null ? 0 : sumObj; + if (transaction.startTimeMs >= windowStartTime) { + sum += transaction.delta; + } else { + // Pro-rate durationed deltas. Intentionally floor the result. + sum += (long) (1.0 * (transaction.endTimeMs - windowStartTime) + * transaction.delta) + / (transaction.endTimeMs - transaction.startTimeMs); + } + mCumulativeDeltaPerReason.put(transaction.reason, sum); + } + mEarliestSumTime = windowStartTime; + } + Long sum = mCumulativeDeltaPerReason.get(reason); + if (sum == null) { + return 0; + } + return sum; + } + + void dump(IndentingPrintWriter pw) { + pw.println("Ledger{"); + pw.increaseIndent(); + + pw.print("cur balance", narcToArc(getCurrentBalance())).println(); + + pw.decreaseIndent(); + pw.println("}"); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java b/apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java new file mode 100644 index 0000000000000..b3d1a0c7c23fe --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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.server.tare; + +class TareUtils { + static long arcToNarc(int arcs) { + return arcs * 1_000_000_000L; + } + + static int narcToArc(long narcs) { + return (int) (narcs / 1_000_000_000); + } +} diff --git a/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java b/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java new file mode 100644 index 0000000000000..f7e1c60384029 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 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.server.tare; + +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static org.junit.Assert.assertEquals; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test that the ledger records transactions correctly. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class LedgerTest { + + @Test + public void testInitialState() { + final Ledger ledger = new Ledger(); + assertEquals(0, ledger.getCurrentBalance()); + assertEquals(0, ledger.get24HourSum("anything", 0)); + } + + @Test + public void testMultipleTransactions() { + final Ledger ledger = new Ledger(); + ledger.recordTransaction(new Ledger.Transaction(0, 1000, "test", null, 5)); + assertEquals(5, ledger.getCurrentBalance()); + assertEquals(5, ledger.get24HourSum("test", 60_000)); + ledger.recordTransaction(new Ledger.Transaction(2000, 2000, "test", null, 25)); + assertEquals(30, ledger.getCurrentBalance()); + assertEquals(30, ledger.get24HourSum("test", 60_000)); + ledger.recordTransaction(new Ledger.Transaction(5000, 5500, "test", null, -10)); + assertEquals(20, ledger.getCurrentBalance()); + assertEquals(20, ledger.get24HourSum("test", 60_000)); + } + + @Test + public void test24HourSum() { + final Ledger ledger = new Ledger(); + ledger.recordTransaction(new Ledger.Transaction(0, 1000, "test", null, 500)); + assertEquals(500, ledger.get24HourSum("test", 24 * HOUR_IN_MILLIS)); + ledger.recordTransaction( + new Ledger.Transaction(2 * HOUR_IN_MILLIS, 3 * HOUR_IN_MILLIS, "test", null, 2500)); + assertEquals(3000, ledger.get24HourSum("test", 24 * HOUR_IN_MILLIS)); + ledger.recordTransaction( + new Ledger.Transaction(4 * HOUR_IN_MILLIS, 4 * HOUR_IN_MILLIS, "test", null, 1)); + assertEquals(3001, ledger.get24HourSum("test", 24 * HOUR_IN_MILLIS)); + assertEquals(2501, ledger.get24HourSum("test", 25 * HOUR_IN_MILLIS)); + assertEquals(2501, ledger.get24HourSum("test", 26 * HOUR_IN_MILLIS)); + // Pro-rated as the second transaction phases out + assertEquals(1251, + ledger.get24HourSum("test", 26 * HOUR_IN_MILLIS + 30 * MINUTE_IN_MILLIS)); + assertEquals(1, ledger.get24HourSum("test", 27 * HOUR_IN_MILLIS)); + assertEquals(0, ledger.get24HourSum("test", 28 * HOUR_IN_MILLIS)); + } +}