1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.fuelgauge.batterytip;
18 
19 import static android.os.StatsDimensionsValue.FLOAT_VALUE_TYPE;
20 import static android.os.StatsDimensionsValue.INT_VALUE_TYPE;
21 import static android.os.StatsDimensionsValue.TUPLE_VALUE_TYPE;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.ArgumentMatchers.anyBoolean;
27 import static org.mockito.ArgumentMatchers.anyInt;
28 import static org.mockito.ArgumentMatchers.anyLong;
29 import static org.mockito.ArgumentMatchers.anyString;
30 import static org.mockito.ArgumentMatchers.eq;
31 import static org.mockito.Mockito.doNothing;
32 import static org.mockito.Mockito.doReturn;
33 import static org.mockito.Mockito.doThrow;
34 import static org.mockito.Mockito.mock;
35 import static org.mockito.Mockito.never;
36 import static org.mockito.Mockito.spy;
37 import static org.mockito.Mockito.verify;
38 import static org.mockito.Mockito.when;
39 
40 import android.app.JobSchedulerImpl;
41 import android.app.StatsManager;
42 import android.app.job.IJobScheduler;
43 import android.app.job.JobInfo;
44 import android.app.job.JobParameters;
45 import android.app.job.JobScheduler;
46 import android.app.job.JobWorkItem;
47 import android.app.settings.SettingsEnums;
48 import android.content.Context;
49 import android.content.Intent;
50 import android.os.Binder;
51 import android.os.Bundle;
52 import android.os.Process;
53 import android.os.StatsDimensionsValue;
54 import android.os.UserManager;
55 
56 import com.android.internal.logging.nano.MetricsProto;
57 import com.android.settings.R;
58 import com.android.settings.fuelgauge.BatteryUtils;
59 import com.android.settings.testutils.FakeFeatureFactory;
60 import com.android.settings.testutils.shadow.ShadowConnectivityManager;
61 import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
62 
63 import org.junit.Before;
64 import org.junit.Test;
65 import org.junit.runner.RunWith;
66 import org.mockito.Mock;
67 import org.mockito.MockitoAnnotations;
68 import org.robolectric.Robolectric;
69 import org.robolectric.RobolectricTestRunner;
70 import org.robolectric.RuntimeEnvironment;
71 import org.robolectric.android.controller.ServiceController;
72 import org.robolectric.annotation.Config;
73 
74 import java.util.ArrayList;
75 import java.util.List;
76 import java.util.concurrent.TimeUnit;
77 
78 @RunWith(RobolectricTestRunner.class)
79 @Config(shadows = {ShadowConnectivityManager.class})
80 public class AnomalyDetectionJobServiceTest {
81     private static final int UID = 12345;
82     private static final String SYSTEM_PACKAGE = "com.android.system";
83     private static final String SUBSCRIBER_COOKIES_AUTO_RESTRICTION =
84             "anomaly_type=6,auto_restriction=true";
85     private static final String SUBSCRIBER_COOKIES_NOT_AUTO_RESTRICTION =
86             "anomaly_type=6,auto_restriction=false";
87     private static final int ANOMALY_TYPE = 6;
88     private static final long VERSION_CODE = 15;
89     @Mock
90     private UserManager mUserManager;
91     @Mock
92     private BatteryDatabaseManager mBatteryDatabaseManager;
93     @Mock
94     private BatteryUtils mBatteryUtils;
95     @Mock
96     private PowerAllowlistBackend mPowerAllowlistBackend;
97     @Mock
98     private StatsDimensionsValue mStatsDimensionsValue;
99     @Mock
100     private JobParameters mJobParameters;
101     @Mock
102     private JobWorkItem mJobWorkItem;
103 
104     private BatteryTipPolicy mPolicy;
105     private Bundle mBundle;
106     private AnomalyDetectionJobService mAnomalyDetectionJobService;
107     private FakeFeatureFactory mFeatureFactory;
108     private Context mContext;
109     private JobScheduler mJobScheduler;
110 
111     @Before
setUp()112     public void setUp() {
113         MockitoAnnotations.initMocks(this);
114 
115         mContext = spy(RuntimeEnvironment.application);
116         mJobScheduler = spy(new JobSchedulerImpl(IJobScheduler.Stub.asInterface(new Binder())));
117         when(mContext.getSystemService(JobScheduler.class)).thenReturn(mJobScheduler);
118 
119         mPolicy = new BatteryTipPolicy(mContext);
120         mBundle = new Bundle();
121         mBundle.putParcelable(StatsManager.EXTRA_STATS_DIMENSIONS_VALUE, mStatsDimensionsValue);
122         mFeatureFactory = FakeFeatureFactory.setupForTest();
123         when(mBatteryUtils.getAppLongVersionCode(any())).thenReturn(VERSION_CODE);
124 
125         final ServiceController<AnomalyDetectionJobService> controller =
126                 Robolectric.buildService(AnomalyDetectionJobService.class);
127         mAnomalyDetectionJobService = spy(controller.get());
128         doNothing().when(mAnomalyDetectionJobService).jobFinished(any(), anyBoolean());
129     }
130 
131     @Test
scheduleCleanUp()132     public void scheduleCleanUp() {
133         AnomalyDetectionJobService.scheduleAnomalyDetection(mContext, new Intent());
134 
135         JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
136         List<JobInfo> pendingJobs = jobScheduler.getAllPendingJobs();
137         assertThat(pendingJobs).hasSize(1);
138 
139         JobInfo pendingJob = pendingJobs.get(0);
140         assertThat(pendingJob.getId()).isEqualTo(R.integer.job_anomaly_detection);
141         assertThat(pendingJob.getMaxExecutionDelayMillis())
142                 .isEqualTo(TimeUnit.MINUTES.toMillis(30));
143     }
144 
145     @Test
saveAnomalyToDatabase_systemAllowlisted_doNotSave()146     public void saveAnomalyToDatabase_systemAllowlisted_doNotSave() {
147         doReturn(UID).when(mAnomalyDetectionJobService).extractUidFromStatsDimensionsValue(any());
148         doReturn(true).when(mPowerAllowlistBackend).isAllowlisted(any(String[].class));
149 
150         mAnomalyDetectionJobService.saveAnomalyToDatabase(mContext,
151                 mUserManager, mBatteryDatabaseManager, mBatteryUtils, mPolicy,
152                 mPowerAllowlistBackend, mContext.getContentResolver(),
153                 mFeatureFactory.powerUsageFeatureProvider,
154                 mFeatureFactory.metricsFeatureProvider, mBundle);
155 
156         verify(mBatteryDatabaseManager, never()).insertAnomaly(anyInt(), anyString(), anyInt(),
157                 anyInt(), anyLong());
158     }
159 
160     @Test
saveAnomalyToDatabase_systemApp_doNotSaveButLog()161     public void saveAnomalyToDatabase_systemApp_doNotSaveButLog() {
162         final ArrayList<String> cookies = new ArrayList<>();
163         cookies.add(SUBSCRIBER_COOKIES_AUTO_RESTRICTION);
164         mBundle.putStringArrayList(StatsManager.EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES, cookies);
165         doReturn(SYSTEM_PACKAGE).when(mBatteryUtils).getPackageName(anyInt());
166         doReturn(false).when(mPowerAllowlistBackend).isSysAllowlisted(SYSTEM_PACKAGE);
167         doReturn(Process.FIRST_APPLICATION_UID).when(
168                 mAnomalyDetectionJobService).extractUidFromStatsDimensionsValue(any());
169         doReturn(true).when(mBatteryUtils).shouldHideAnomaly(any(), anyInt(), any());
170 
171         mAnomalyDetectionJobService.saveAnomalyToDatabase(mContext,
172                 mUserManager, mBatteryDatabaseManager, mBatteryUtils, mPolicy,
173                 mPowerAllowlistBackend, mContext.getContentResolver(),
174                 mFeatureFactory.powerUsageFeatureProvider,
175                 mFeatureFactory.metricsFeatureProvider, mBundle);
176 
177         verify(mBatteryDatabaseManager, never()).insertAnomaly(anyInt(), anyString(), anyInt(),
178                 anyInt(), anyLong());
179         verify(mFeatureFactory.metricsFeatureProvider).action(SettingsEnums.PAGE_UNKNOWN,
180                 MetricsProto.MetricsEvent.ACTION_ANOMALY_IGNORED,
181                 SettingsEnums.PAGE_UNKNOWN,
182                 SYSTEM_PACKAGE + "/" + VERSION_CODE,
183                  ANOMALY_TYPE);
184     }
185 
186     @Test
saveAnomalyToDatabase_systemUid_doNotSave()187     public void saveAnomalyToDatabase_systemUid_doNotSave() {
188         doReturn(Process.SYSTEM_UID).when(
189                 mAnomalyDetectionJobService).extractUidFromStatsDimensionsValue(any());
190 
191         mAnomalyDetectionJobService.saveAnomalyToDatabase(mContext,
192                 mUserManager, mBatteryDatabaseManager, mBatteryUtils, mPolicy,
193                 mPowerAllowlistBackend, mContext.getContentResolver(),
194                 mFeatureFactory.powerUsageFeatureProvider, mFeatureFactory.metricsFeatureProvider,
195                 mBundle);
196 
197         verify(mBatteryDatabaseManager, never()).insertAnomaly(anyInt(), anyString(), anyInt(),
198                 anyInt(), anyLong());
199     }
200 
201     @Test
saveAnomalyToDatabase_uidNull_doNotSave()202     public void saveAnomalyToDatabase_uidNull_doNotSave() {
203         doReturn(AnomalyDetectionJobService.UID_NULL).when(
204                 mAnomalyDetectionJobService).extractUidFromStatsDimensionsValue(any());
205 
206         mAnomalyDetectionJobService.saveAnomalyToDatabase(mContext,
207                 mUserManager, mBatteryDatabaseManager, mBatteryUtils, mPolicy,
208                 mPowerAllowlistBackend, mContext.getContentResolver(),
209                 mFeatureFactory.powerUsageFeatureProvider, mFeatureFactory.metricsFeatureProvider,
210                 mBundle);
211 
212         verify(mBatteryDatabaseManager, never()).insertAnomaly(anyInt(), anyString(), anyInt(),
213                 anyInt(), anyLong());
214     }
215 
216     @Test
saveAnomalyToDatabase_normalAppWithAutoRestriction_save()217     public void saveAnomalyToDatabase_normalAppWithAutoRestriction_save() {
218         final ArrayList<String> cookies = new ArrayList<>();
219         cookies.add(SUBSCRIBER_COOKIES_AUTO_RESTRICTION);
220         mBundle.putStringArrayList(StatsManager.EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES, cookies);
221         doReturn(SYSTEM_PACKAGE).when(mBatteryUtils).getPackageName(anyInt());
222         doReturn(false).when(mPowerAllowlistBackend).isSysAllowlisted(SYSTEM_PACKAGE);
223         doReturn(Process.FIRST_APPLICATION_UID).when(
224                 mAnomalyDetectionJobService).extractUidFromStatsDimensionsValue(any());
225 
226         mAnomalyDetectionJobService.saveAnomalyToDatabase(mContext,
227                 mUserManager, mBatteryDatabaseManager, mBatteryUtils, mPolicy,
228                 mPowerAllowlistBackend, mContext.getContentResolver(),
229                 mFeatureFactory.powerUsageFeatureProvider, mFeatureFactory.metricsFeatureProvider,
230                 mBundle);
231 
232         verify(mBatteryDatabaseManager).insertAnomaly(anyInt(), anyString(), eq(6),
233                 eq(AnomalyDatabaseHelper.State.AUTO_HANDLED), anyLong());
234         verify(mFeatureFactory.metricsFeatureProvider).action(SettingsEnums.PAGE_UNKNOWN,
235                 MetricsProto.MetricsEvent.ACTION_ANOMALY_TRIGGERED,
236                 SettingsEnums.PAGE_UNKNOWN,
237                 SYSTEM_PACKAGE + "/" + VERSION_CODE,
238                 ANOMALY_TYPE);
239     }
240 
241     @Test
saveAnomalyToDatabase_normalAppWithoutAutoRestriction_save()242     public void saveAnomalyToDatabase_normalAppWithoutAutoRestriction_save() {
243         final ArrayList<String> cookies = new ArrayList<>();
244         cookies.add(SUBSCRIBER_COOKIES_NOT_AUTO_RESTRICTION);
245         mBundle.putStringArrayList(StatsManager.EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES, cookies);
246         doReturn(SYSTEM_PACKAGE).when(mBatteryUtils).getPackageName(anyInt());
247         doReturn(false).when(mPowerAllowlistBackend).isSysAllowlisted(SYSTEM_PACKAGE);
248         doReturn(Process.FIRST_APPLICATION_UID).when(
249                 mAnomalyDetectionJobService).extractUidFromStatsDimensionsValue(any());
250 
251         mAnomalyDetectionJobService.saveAnomalyToDatabase(mContext,
252                 mUserManager, mBatteryDatabaseManager, mBatteryUtils, mPolicy,
253                 mPowerAllowlistBackend, mContext.getContentResolver(),
254                 mFeatureFactory.powerUsageFeatureProvider, mFeatureFactory.metricsFeatureProvider,
255                 mBundle);
256 
257         verify(mBatteryDatabaseManager).insertAnomaly(anyInt(), anyString(), eq(6),
258                 eq(AnomalyDatabaseHelper.State.NEW), anyLong());
259         verify(mFeatureFactory.metricsFeatureProvider).action(SettingsEnums.PAGE_UNKNOWN,
260                 MetricsProto.MetricsEvent.ACTION_ANOMALY_TRIGGERED,
261                 SettingsEnums.PAGE_UNKNOWN,
262                 SYSTEM_PACKAGE + "/" + VERSION_CODE,
263                 ANOMALY_TYPE);
264     }
265 
266     @Test
extractUidFromStatsDimensionsValue_extractCorrectUid()267     public void extractUidFromStatsDimensionsValue_extractCorrectUid() {
268         // Build an integer dimensions value.
269         final StatsDimensionsValue intValue = mock(StatsDimensionsValue.class);
270         when(intValue.isValueType(INT_VALUE_TYPE)).thenReturn(true);
271         when(intValue.getField()).thenReturn(AnomalyDetectionJobService.STATSD_UID_FILED);
272         when(intValue.getIntValue()).thenReturn(UID);
273 
274         // Build a tuple dimensions value and put the previous integer dimensions value inside.
275         final StatsDimensionsValue tupleValue = mock(StatsDimensionsValue.class);
276         when(tupleValue.isValueType(TUPLE_VALUE_TYPE)).thenReturn(true);
277         final List<StatsDimensionsValue> statsDimensionsValues = new ArrayList<>();
278         statsDimensionsValues.add(intValue);
279         when(tupleValue.getTupleValueList()).thenReturn(statsDimensionsValues);
280 
281         assertThat(mAnomalyDetectionJobService.extractUidFromStatsDimensionsValue(
282                 tupleValue)).isEqualTo(UID);
283     }
284 
285     @Test
extractUidFromStatsDimensionsValue_wrongFormat_returnNull()286     public void extractUidFromStatsDimensionsValue_wrongFormat_returnNull() {
287         // Build a float dimensions value
288         final StatsDimensionsValue floatValue = mock(StatsDimensionsValue.class);
289         when(floatValue.isValueType(FLOAT_VALUE_TYPE)).thenReturn(true);
290         when(floatValue.getField()).thenReturn(AnomalyDetectionJobService.STATSD_UID_FILED);
291         when(floatValue.getFloatValue()).thenReturn(0f);
292 
293         assertThat(mAnomalyDetectionJobService.extractUidFromStatsDimensionsValue(
294                 floatValue)).isEqualTo(AnomalyDetectionJobService.UID_NULL);
295     }
296 
297     @Test
stopJobWhileDequeuingWork_shouldNotCrash()298     public void stopJobWhileDequeuingWork_shouldNotCrash() {
299         when(mJobParameters.dequeueWork()).thenThrow(new SecurityException());
300 
301         mAnomalyDetectionJobService.onStopJob(mJobParameters);
302 
303         // Should not crash even job is stopped
304         mAnomalyDetectionJobService.dequeueWork(mJobParameters);
305     }
306 
307     @Test
stopJobWhileCompletingWork_shouldNotCrash()308     public void stopJobWhileCompletingWork_shouldNotCrash() {
309         doThrow(new SecurityException()).when(mJobParameters).completeWork(any());
310 
311         mAnomalyDetectionJobService.onStopJob(mJobParameters);
312 
313         // Should not crash even job is stopped
314         mAnomalyDetectionJobService.completeWork(mJobParameters, mJobWorkItem);
315     }
316 
317     @Test
restartWorkAfterBeenStopped_jobStarted()318     public void restartWorkAfterBeenStopped_jobStarted() {
319         mAnomalyDetectionJobService.onStopJob(mJobParameters);
320         mAnomalyDetectionJobService.onStartJob(mJobParameters);
321 
322         assertThat(mAnomalyDetectionJobService.mIsJobCanceled).isFalse();
323     }
324 }
325