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 package android.content.syncmanager.cts;
17 
18 import static android.content.syncmanager.cts.common.Values.ACCOUNT_1_A;
19 import static android.content.syncmanager.cts.common.Values.APP1_AUTHORITY;
20 import static android.content.syncmanager.cts.common.Values.APP1_PACKAGE;
21 
22 import static com.android.compatibility.common.util.BundleUtils.makeBundle;
23 import static com.android.compatibility.common.util.ConnectivityUtils.assertNetworkConnected;
24 import static com.android.compatibility.common.util.SystemUtil.runCommandAndPrintOnLogcat;
25 import static com.android.compatibility.common.util.TestUtils.waitUntil;
26 
27 import static junit.framework.TestCase.assertEquals;
28 
29 import static org.junit.Assert.assertTrue;
30 import static org.junit.Assume.assumeTrue;
31 
32 import android.accounts.Account;
33 import android.app.usage.UsageStatsManager;
34 import android.content.ContentResolver;
35 import android.content.Context;
36 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.AddAccount;
37 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.ClearSyncInvocations;
38 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.GetSyncInvocations;
39 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.RemoveAllAccounts;
40 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.SetResult;
41 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.SetResult.Result;
42 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Response;
43 import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.SyncInvocation;
44 import android.os.Bundle;
45 import android.os.PowerManager;
46 import android.util.Log;
47 
48 import androidx.test.InstrumentationRegistry;
49 import androidx.test.filters.FlakyTest;
50 import androidx.test.filters.LargeTest;
51 import androidx.test.runner.AndroidJUnit4;
52 
53 import com.android.compatibility.common.util.AmUtils;
54 import com.android.compatibility.common.util.BatteryUtils;
55 import com.android.compatibility.common.util.OnFailureRule;
56 import com.android.compatibility.common.util.ParcelUtils;
57 import com.android.compatibility.common.util.ShellUtils;
58 import com.android.compatibility.common.util.SystemUtil;
59 import com.android.compatibility.common.util.UserSettings;
60 import com.android.compatibility.common.util.UserSettings.Namespace;
61 
62 import org.junit.After;
63 import org.junit.Before;
64 import org.junit.Rule;
65 import org.junit.Test;
66 import org.junit.runner.Description;
67 import org.junit.runner.RunWith;
68 import org.junit.runners.model.Statement;
69 
70 @LargeTest
71 @RunWith(AndroidJUnit4.class)
72 public class CtsSyncManagerTest {
73     private static final String TAG = "CtsSyncManagerTest";
74 
75     public static final int DEFAULT_TIMEOUT_SECONDS = 10 * 60;
76 
77     public static final boolean DEBUG = false;
78 
79     private static final int STANDBY_BUCKET_NEVER = 50;
80 
81     @Rule
82     public final OnFailureRule mDumpOnFailureRule = new OnFailureRule(TAG) {
83         @Override
84         protected void onTestFailure(Statement base, Description description, Throwable t) {
85             runCommandAndPrintOnLogcat(TAG, "dumpsys content");
86             runCommandAndPrintOnLogcat(TAG, "dumpsys jobscheduler");
87         }
88     };
89 
90     protected final BroadcastRpc mRpc = new BroadcastRpc();
91 
92     Context mContext;
93     ContentResolver mContentResolver;
94 
95     @Before
setUp()96     public void setUp() throws Exception {
97         assertNetworkConnected(InstrumentationRegistry.getContext());
98 
99         BatteryUtils.runDumpsysBatteryUnplug();
100         BatteryUtils.enableAdaptiveBatterySaver(false);
101         // Don't wait so tests can also run for devices without battery saver.
102         BatteryUtils.enableBatterySaver(false, false);
103 
104         AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_ACTIVE);
105 
106         mContext = InstrumentationRegistry.getContext();
107         mContentResolver = mContext.getContentResolver();
108 
109         ContentResolver.setMasterSyncAutomatically(true);
110 
111         mRpc.invoke(APP1_PACKAGE, rb ->
112                 rb.setSetResult(SetResult.newBuilder().setResult(Result.OK)));
113 
114         Thread.sleep(1000); // Don't make the system too busy...
115     }
116 
117     @After
tearDown()118     public void tearDown() throws Exception {
119         resetSyncConfig();
120         setDozeState(false);
121         BatteryUtils.runDumpsysBatteryReset();
122     }
123 
124     private static final UserSettings sGlobalSettings = new UserSettings(Namespace.GLOBAL);
125 
resetSyncConfig()126     private static void resetSyncConfig() {
127         sGlobalSettings.set("sync_manager_constants", "null");
128     }
129 
writeSyncConfig( int initialSyncRetryTimeInSeconds, float retryTimeIncreaseFactor, int maxSyncRetryTimeInSeconds, int maxRetriesWithAppStandbyExemption)130     private static void writeSyncConfig(
131             int initialSyncRetryTimeInSeconds,
132             float retryTimeIncreaseFactor,
133             int maxSyncRetryTimeInSeconds,
134             int maxRetriesWithAppStandbyExemption) {
135         sGlobalSettings.set("sync_manager_constants",
136                 "initial_sync_retry_time_in_seconds=" + initialSyncRetryTimeInSeconds + "," +
137                 "retry_time_increase_factor=" + retryTimeIncreaseFactor + "," +
138                 "max_sync_retry_time_in_seconds=" + maxSyncRetryTimeInSeconds + "," +
139                 "max_retries_with_app_standby_exemption=" + maxRetriesWithAppStandbyExemption);
140     }
141 
142     /** Return the part of "dumpsys content" that's relevant to the current sync status. */
getSyncDumpsys()143     private String getSyncDumpsys() {
144         final String out = SystemUtil.runCommandAndExtractSection("dumpsys content",
145                 "^Active Syncs:.*", false,
146                 "^Sync Statistics", false);
147         return out;
148     }
149 
removeAllAccounts()150     private void removeAllAccounts() throws Exception {
151         mRpc.invoke(APP1_PACKAGE,
152                 rb -> rb.setRemoveAllAccounts(RemoveAllAccounts.newBuilder()));
153 
154         Thread.sleep(1000);
155 
156         AmUtils.waitForBroadcastIdle();
157 
158         waitUntil("Dumpsys still mentions " + ACCOUNT_1_A, DEFAULT_TIMEOUT_SECONDS,
159                 () -> !getSyncDumpsys().contains(ACCOUNT_1_A.name));
160 
161         Thread.sleep(1000);
162     }
163 
clearSyncInvocations(String packageName)164     private void clearSyncInvocations(String packageName) throws Exception {
165         mRpc.invoke(packageName,
166                 rb -> rb.setClearSyncInvocations(ClearSyncInvocations.newBuilder()));
167     }
168 
addAccountAndLetInitialSyncRun(Account account, String authority)169     private void addAccountAndLetInitialSyncRun(Account account, String authority)
170             throws Exception {
171         // Add the first account, which will trigger an initial sync.
172         mRpc.invoke(APP1_PACKAGE,
173                 rb -> rb.setAddAccount(AddAccount.newBuilder().setName(account.name)));
174 
175         waitUntil("Syncable isn't initialized", DEFAULT_TIMEOUT_SECONDS,
176                 () -> ContentResolver.getIsSyncable(account, authority) == 1);
177 
178         waitUntil("Periodic sync should set up", DEFAULT_TIMEOUT_SECONDS,
179                 () -> ContentResolver.getPeriodicSyncs(account, authority).size() == 1);
180         assertEquals("Periodic should be 24h",
181                 24 * 60 * 60, ContentResolver.getPeriodicSyncs(account, authority).get(0).period);
182     }
183 
184     @Test
testInitialSync()185     public void testInitialSync() throws Exception {
186         removeAllAccounts();
187 
188         mRpc.invoke(APP1_PACKAGE, rb -> rb.setClearSyncInvocations(
189                 ClearSyncInvocations.newBuilder()));
190 
191         // Add the first account, which will trigger an initial sync.
192         addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY);
193 
194         // Check the sync request parameters.
195 
196         Response res = mRpc.invoke(APP1_PACKAGE,
197                 rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder()));
198         assertEquals(1, res.getSyncInvocations().getSyncInvocationsCount());
199 
200         SyncInvocation si = res.getSyncInvocations().getSyncInvocations(0);
201 
202         assertEquals(ACCOUNT_1_A.name, si.getAccountName());
203         assertEquals(ACCOUNT_1_A.type, si.getAccountType());
204         assertEquals(APP1_AUTHORITY, si.getAuthority());
205 
206         Bundle extras = ParcelUtils.fromBytes(si.getExtras().toByteArray());
207         assertTrue(extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE));
208     }
209 
210     @Test
211     @FlakyTest
testSoftErrorRetriesActiveApp()212     public void testSoftErrorRetriesActiveApp() throws Exception {
213         removeAllAccounts();
214 
215         // Let the initial sync happen.
216         addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY);
217 
218         writeSyncConfig(2, 1, 2, 3);
219 
220         clearSyncInvocations(APP1_PACKAGE);
221 
222         AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_ACTIVE);
223 
224         // Set soft error.
225         mRpc.invoke(APP1_PACKAGE, rb ->
226                 rb.setSetResult(SetResult.newBuilder().setResult(Result.SOFT_ERROR)));
227 
228         Bundle b = makeBundle(
229                 "testSoftErrorRetriesActiveApp", true,
230                 ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
231 
232         ContentResolver.requestSync(ACCOUNT_1_A, APP1_AUTHORITY, b);
233 
234         // First sync + 3 retries == 4, so should be called more than 4 times.
235         // But it's active, so it should retry more than that.
236         waitUntil("Should retry more than 3 times.", DEFAULT_TIMEOUT_SECONDS, () -> {
237             final Response res = mRpc.invoke(APP1_PACKAGE,
238                     rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder()));
239             final int calls = res.getSyncInvocations().getSyncInvocationsCount();
240             Log.i(TAG, "NumSyncInvocations=" + calls);
241             return calls > 4; // Arbitrarily bigger than 4.
242         });
243     }
244 
245     @Test
testExpeditedJobSync()246     public void testExpeditedJobSync() throws Exception {
247         setDozeState(false);
248         removeAllAccounts();
249 
250         // Let the initial sync happen.
251         addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY);
252 
253         writeSyncConfig(2, 1, 2, 3);
254 
255         clearSyncInvocations(APP1_PACKAGE);
256 
257         AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_RARE);
258 
259         Bundle b = makeBundle(ContentResolver.SYNC_EXTRAS_SCHEDULE_AS_EXPEDITED_JOB, true,
260                 ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
261 
262         ContentResolver.requestSync(ACCOUNT_1_A, APP1_AUTHORITY, b);
263 
264         waitUntil("Expedited job sync didn't run in Doze", 30, () -> {
265             final Response res = mRpc.invoke(APP1_PACKAGE,
266                     rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder()));
267             final int calls = res.getSyncInvocations().getSyncInvocationsCount();
268             Log.i(TAG, "NumSyncInvocations=" + calls);
269             return calls == 1;
270         });
271     }
272 
273     @Test
testExpeditedJobSync_InDoze()274     public void testExpeditedJobSync_InDoze() throws Exception {
275         assumeTrue(isDozeFeatureEnabled());
276 
277         setDozeState(false);
278         removeAllAccounts();
279 
280         // Let the initial sync happen.
281         addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY);
282 
283         writeSyncConfig(2, 1, 2, 3);
284 
285         clearSyncInvocations(APP1_PACKAGE);
286 
287         AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_RARE);
288 
289         setDozeState(true);
290         Bundle b = makeBundle(ContentResolver.SYNC_EXTRAS_SCHEDULE_AS_EXPEDITED_JOB, true,
291                 ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
292 
293         ContentResolver.requestSync(ACCOUNT_1_A, APP1_AUTHORITY, b);
294 
295         waitUntil("Expedited job sync should still run in Doze", 30, () -> {
296             final Response res = mRpc.invoke(APP1_PACKAGE,
297                     rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder()));
298             final int calls = res.getSyncInvocations().getSyncInvocationsCount();
299             Log.i(TAG, "NumSyncInvocations=" + calls);
300             return calls == 1;
301         });
302     }
303 
304     @Test
testInitialSyncInNeverBucket()305     public void testInitialSyncInNeverBucket() throws Exception {
306         removeAllAccounts();
307 
308         AmUtils.setStandbyBucket(APP1_PACKAGE, STANDBY_BUCKET_NEVER);
309 
310         mRpc.invoke(APP1_PACKAGE, rb -> rb.setClearSyncInvocations(
311                 ClearSyncInvocations.newBuilder()));
312 
313         addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY);
314 
315         // App should be brought out of the NEVER bucket to handle the sync
316         assertTrue("Standby bucket should be WORKING_SET or better",
317                 AmUtils.getStandbyBucket(APP1_PACKAGE)
318                         <= UsageStatsManager.STANDBY_BUCKET_WORKING_SET);
319 
320         // Check the sync request parameters.
321         Response res = mRpc.invoke(APP1_PACKAGE,
322                 rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder()));
323         assertEquals(1, res.getSyncInvocations().getSyncInvocationsCount());
324 
325         SyncInvocation si = res.getSyncInvocations().getSyncInvocations(0);
326 
327         assertEquals(ACCOUNT_1_A.name, si.getAccountName());
328         assertEquals(ACCOUNT_1_A.type, si.getAccountType());
329         assertEquals(APP1_AUTHORITY, si.getAuthority());
330 
331         Bundle extras = ParcelUtils.fromBytes(si.getExtras().toByteArray());
332         assertTrue(extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE));
333     }
334 
isDozeFeatureEnabled()335     private static boolean isDozeFeatureEnabled() {
336         final String output = ShellUtils.runShellCommand("cmd deviceidle enabled deep").trim();
337         return Integer.parseInt(output) != 0;
338     }
339 
setDozeState(final boolean on)340     private void setDozeState(final boolean on) throws Exception {
341         ShellUtils.runShellCommand("cmd deviceidle " + (on ? "force-idle" : "unforce"));
342         if (!on) {
343             // Make sure the device doesn't stay idle, even after unforcing.
344             ShellUtils.runShellCommand("cmd deviceidle motion");
345         }
346         final PowerManager powerManager =
347                 InstrumentationRegistry.getContext().getSystemService(PowerManager.class);
348         waitUntil("Doze mode didn't change to " + (on ? "on" : "off"), 10,
349                 () -> powerManager.isDeviceIdleMode() == on);
350     }
351 
352     // WIP This test doesn't work yet.
353 //    @Test
354 //    public void testSoftErrorRetriesFrequentApp() throws Exception {
355 //        runTest(() -> {
356 //            removeAllAccounts();
357 //
358 //            // Let the initial sync happen.
359 //            addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY);
360 //
361 //            writeSyncConfig(2, 1, 2, 3);
362 //
363 //            clearSyncInvocations(APP1_PACKAGE);
364 //
365 //            AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_FREQUENT);
366 //
367 //            // Set soft error.
368 //            mRpc.invoke(APP1_PACKAGE, rb ->
369 //                    rb.setSetResult(SetResult.newBuilder().setResult(Result.SOFT_ERROR)));
370 //
371 //            Bundle b = makeBundle(
372 //                    "testSoftErrorRetriesFrequentApp", true,
373 //                    ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
374 //
375 //            ContentResolver.requestSync(ACCOUNT_1_A, APP1_AUTHORITY, b);
376 //
377 //            waitUntil("Should retry more than 3 times.", () -> {
378 //                final Response res = mRpc.invoke(APP1_PACKAGE,
379 //                        rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder()));
380 //                final int calls =  res.getSyncInvocations().getSyncInvocationsCount();
381 //                Log.i(TAG, "NumSyncInvocations=" + calls);
382 //                return calls >= 4; // First sync + 3 retries == 4, so at least 4 times.
383 //            });
384 //
385 //            Thread.sleep(10_000);
386 //
387 //            // One more retry is okay because of how the job scheduler throttle jobs, but no further.
388 //            final Response res = mRpc.invoke(APP1_PACKAGE,
389 //                    rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder()));
390 //            final int calls =  res.getSyncInvocations().getSyncInvocationsCount();
391 //            assertTrue("# of syncs must be equal or less than 5, but was " + calls, calls <= 5);
392 //        });
393 //    }
394 }
395