diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java index 2ecc0ed798d2e..5d29515bdcbd5 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java @@ -28,6 +28,7 @@ import static com.android.server.tare.EconomicPolicy.eventToString; import static com.android.server.tare.EconomicPolicy.getEventType; import static com.android.server.tare.TareUtils.appToString; import static com.android.server.tare.TareUtils.getCurrentTimeMillis; +import static com.android.server.tare.TareUtils.narcToString; import android.annotation.NonNull; import android.annotation.Nullable; @@ -460,8 +461,9 @@ class Agent { Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta)); } + @VisibleForTesting @GuardedBy("mLock") - private void recordTransactionLocked(final int userId, @NonNull final String pkgName, + void recordTransactionLocked(final int userId, @NonNull final String pkgName, @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction, final boolean notifyOnAffordabilityChange) { if (transaction.delta == 0) { @@ -476,12 +478,14 @@ class Agent { final long maxCirculationAllowed = mIrs.getMaxCirculationLocked(); final long newArcsInCirculation = mCurrentNarcsInCirculation + transaction.delta; if (transaction.delta > 0 && newArcsInCirculation > maxCirculationAllowed) { - final long newDelta = maxCirculationAllowed - mCurrentNarcsInCirculation; + // Set lower bound at 0 so we don't accidentally take away credits when we were trying + // to _give_ the app credits. + final long newDelta = Math.max(0, maxCirculationAllowed - mCurrentNarcsInCirculation); Slog.i(TAG, "Would result in too many credits in circulation. Decreasing transaction " + eventToString(transaction.eventId) + (transaction.tag == null ? "" : ":" + transaction.tag) + " for " + appToString(userId, pkgName) - + " by " + (transaction.delta - newDelta)); + + " by " + narcToString(transaction.delta - newDelta)); transaction = new Ledger.Transaction( transaction.startTimeMs, transaction.endTimeMs, transaction.eventId, transaction.tag, newDelta); @@ -490,12 +494,15 @@ class Agent { if (transaction.delta > 0 && originalBalance + transaction.delta > mCompleteEconomicPolicy.getMaxSatiatedBalance()) { - final long newDelta = mCompleteEconomicPolicy.getMaxSatiatedBalance() - originalBalance; + // Set lower bound at 0 so we don't accidentally take away credits when we were trying + // to _give_ the app credits. + final long newDelta = + Math.max(0, mCompleteEconomicPolicy.getMaxSatiatedBalance() - originalBalance); Slog.i(TAG, "Would result in becoming too rich. Decreasing transaction " + eventToString(transaction.eventId) + (transaction.tag == null ? "" : ":" + transaction.tag) + " for " + appToString(userId, pkgName) - + " by " + (transaction.delta - newDelta)); + + " by " + narcToString(transaction.delta - newDelta)); transaction = new Ledger.Transaction( transaction.startTimeMs, transaction.endTimeMs, transaction.eventId, transaction.tag, newDelta); diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java new file mode 100644 index 0000000000000..dd6b7443627f1 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java @@ -0,0 +1,183 @@ +/* + * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.content.Context; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +/** Tests various aspects of the Agent. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AgentTest { + private MockitoSession mMockingSession; + @Mock + private CompleteEconomicPolicy mEconomicPolicy; + @Mock + private Context mContext; + @Mock + private InternalResourceService mIrs; + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .mockStatic(LocalServices.class) + .startMocking(); + when(mIrs.getContext()).thenReturn(mContext); + when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mock(AlarmManager.class)); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + @Test + public void testRecordTransaction_UnderMax() { + Agent agent = new Agent(mIrs, mEconomicPolicy); + Ledger ledger = new Ledger(); + + doReturn(1_000_000L).when(mIrs).getMaxCirculationLocked(); + doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance(); + + Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(5, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, 995); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1000, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, -500); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(500, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1_000_000L, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, -1_000_001L); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(-1, ledger.getCurrentBalance()); + } + + @Test + public void testRecordTransaction_MaxCirculation() { + Agent agent = new Agent(mIrs, mEconomicPolicy); + Ledger ledger = new Ledger(); + + doReturn(1000L).when(mIrs).getMaxCirculationLocked(); + doReturn(1000L).when(mEconomicPolicy).getMaxSatiatedBalance(); + + Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(5, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, 995); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1000, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, -500); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(500, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, 2000); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1000, ledger.getCurrentBalance()); + + // MaxCirculation can change as the battery level changes. Any already allocated ARCSs + // shouldn't be removed by recordTransaction(). + doReturn(900L).when(mIrs).getMaxCirculationLocked(); + + transaction = new Ledger.Transaction(0, 0, 0, null, 100); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1000, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, -50); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(950, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, -200); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(750, ledger.getCurrentBalance()); + + doReturn(800L).when(mIrs).getMaxCirculationLocked(); + + transaction = new Ledger.Transaction(0, 0, 0, null, 100); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(800, ledger.getCurrentBalance()); + } + + @Test + public void testRecordTransaction_MaxSatiatedBalance() { + Agent agent = new Agent(mIrs, mEconomicPolicy); + Ledger ledger = new Ledger(); + + doReturn(1_000_000L).when(mIrs).getMaxCirculationLocked(); + doReturn(1000L).when(mEconomicPolicy).getMaxSatiatedBalance(); + + Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(5, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, 995); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1000, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, -500); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(500, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1_000, ledger.getCurrentBalance()); + + // Shouldn't change in normal operation, but adding test case in case it does. + doReturn(900L).when(mEconomicPolicy).getMaxSatiatedBalance(); + + transaction = new Ledger.Transaction(0, 0, 0, null, 500); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(1_000, ledger.getCurrentBalance()); + + transaction = new Ledger.Transaction(0, 0, 0, null, -1001); + agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); + assertEquals(-1, ledger.getCurrentBalance()); + } +}