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