| /* |
| * Copyright (C) 2018 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 android.content.syncmanager.cts; |
| |
| import static android.content.syncmanager.cts.common.Values.ACCOUNT_1_A; |
| import static android.content.syncmanager.cts.common.Values.APP1_AUTHORITY; |
| import static android.content.syncmanager.cts.common.Values.APP1_PACKAGE; |
| |
| import static com.android.compatibility.common.util.BundleUtils.makeBundle; |
| import static com.android.compatibility.common.util.ConnectivityUtils.assertNetworkConnected; |
| import static com.android.compatibility.common.util.SettingsUtils.putGlobalSetting; |
| import static com.android.compatibility.common.util.SystemUtil.runCommandAndPrintOnLogcat; |
| |
| import static junit.framework.TestCase.assertEquals; |
| |
| import static org.junit.Assert.assertTrue; |
| |
| import android.accounts.Account; |
| import android.app.usage.UsageStatsManager; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.AddAccount; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.ClearSyncInvocations; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.GetSyncInvocations; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.RemoveAllAccounts; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.SetResult; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Request.SetResult.Result; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.Response; |
| import android.content.syncmanager.cts.SyncManagerCtsProto.Payload.SyncInvocation; |
| import android.os.Bundle; |
| import android.util.Log; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.LargeTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.compatibility.common.util.AmUtils; |
| import com.android.compatibility.common.util.BatteryUtils; |
| import com.android.compatibility.common.util.OnFailureRule; |
| import com.android.compatibility.common.util.ParcelUtils; |
| import com.android.compatibility.common.util.SystemUtil; |
| import com.android.compatibility.common.util.TestUtils; |
| import com.android.compatibility.common.util.TestUtils.BooleanSupplierWithThrow; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.Description; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.model.Statement; |
| |
| // TODO Don't run if no network is available. |
| @LargeTest |
| @RunWith(AndroidJUnit4.class) |
| public class CtsSyncManagerTest { |
| private static final String TAG = "CtsSyncManagerTest"; |
| |
| public static final int DEFAULT_TIMEOUT_SECONDS = 30; |
| |
| public static final boolean DEBUG = false; |
| private static final int TIMEOUT_MS = 10 * 60 * 1000; |
| |
| @Rule |
| public final OnFailureRule mDumpOnFailureRule = new OnFailureRule(TAG) { |
| @Override |
| protected void onTestFailure(Statement base, Description description, Throwable t) { |
| runCommandAndPrintOnLogcat(TAG, "dumpsys content"); |
| runCommandAndPrintOnLogcat(TAG, "dumpsys jobscheduler"); |
| } |
| }; |
| |
| protected final BroadcastRpc mRpc = new BroadcastRpc(); |
| |
| Context mContext; |
| ContentResolver mContentResolver; |
| |
| @Before |
| public void setUp() throws Exception { |
| assertNetworkConnected(InstrumentationRegistry.getContext()); |
| |
| BatteryUtils.runDumpsysBatteryUnplug(); |
| |
| AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_ACTIVE); |
| |
| mContext = InstrumentationRegistry.getContext(); |
| mContentResolver = mContext.getContentResolver(); |
| |
| ContentResolver.setMasterSyncAutomatically(true); |
| |
| Thread.sleep(1000); // Don't make the system too busy... |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| resetSyncConfig(); |
| BatteryUtils.runDumpsysBatteryReset(); |
| } |
| |
| private static void resetSyncConfig() { |
| putGlobalSetting("sync_manager_constants", "null"); |
| } |
| |
| private static void writeSyncConfig( |
| int initialSyncRetryTimeInSeconds, |
| float retryTimeIncreaseFactor, |
| int maxSyncRetryTimeInSeconds, |
| int maxRetriesWithAppStandbyExemption) { |
| putGlobalSetting("sync_manager_constants", |
| "initial_sync_retry_time_in_seconds=" + initialSyncRetryTimeInSeconds + "," + |
| "retry_time_increase_factor=" + retryTimeIncreaseFactor + "," + |
| "max_sync_retry_time_in_seconds=" + maxSyncRetryTimeInSeconds + "," + |
| "max_retries_with_app_standby_exemption=" + maxRetriesWithAppStandbyExemption); |
| } |
| |
| /** Return the part of "dumpsys content" that's relevant to the current sync status. */ |
| private String getSyncDumpsys() { |
| final String out = SystemUtil.runCommandAndExtractSection("dumpsys content", |
| "^Active Syncs:.*", false, |
| "^Sync Statistics", false); |
| return out; |
| } |
| |
| private void waitUntil(String message, BooleanSupplierWithThrow predicate) throws Exception { |
| TestUtils.waitUntil(message, TIMEOUT_MS, predicate); |
| } |
| |
| private void removeAllAccounts() throws Exception { |
| mRpc.invoke(APP1_PACKAGE, |
| rb -> rb.setRemoveAllAccounts(RemoveAllAccounts.newBuilder())); |
| |
| Thread.sleep(1000); |
| |
| AmUtils.waitForBroadcastIdle(); |
| |
| waitUntil("Dumpsys still mentions " + ACCOUNT_1_A, |
| () -> !getSyncDumpsys().contains(ACCOUNT_1_A.name)); |
| |
| Thread.sleep(1000); |
| } |
| |
| private void clearSyncInvocations(String packageName) throws Exception { |
| mRpc.invoke(packageName, |
| rb -> rb.setClearSyncInvocations(ClearSyncInvocations.newBuilder())); |
| } |
| |
| private void addAccountAndLetInitialSyncRun(Account account, String authority) |
| throws Exception { |
| // Add the first account, which will trigger an initial sync. |
| mRpc.invoke(APP1_PACKAGE, |
| rb -> rb.setAddAccount(AddAccount.newBuilder().setName(account.name))); |
| |
| waitUntil("Syncable isn't initialized", |
| () -> ContentResolver.getIsSyncable(account, authority) == 1); |
| |
| waitUntil("Periodic sync should set up", |
| () -> ContentResolver.getPeriodicSyncs(account, authority).size() == 1); |
| assertEquals("Periodic should be 24h", |
| 24 * 60 * 60, ContentResolver.getPeriodicSyncs(account, authority).get(0).period); |
| } |
| |
| @Test |
| public void testInitialSync() throws Exception { |
| removeAllAccounts(); |
| |
| mRpc.invoke(APP1_PACKAGE, rb -> rb.setClearSyncInvocations( |
| ClearSyncInvocations.newBuilder())); |
| |
| // Add the first account, which will trigger an initial sync. |
| addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY); |
| |
| // Check the sync request parameters. |
| |
| Response res = mRpc.invoke(APP1_PACKAGE, |
| rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder())); |
| assertEquals(1, res.getSyncInvocations().getSyncInvocationsCount()); |
| |
| SyncInvocation si = res.getSyncInvocations().getSyncInvocations(0); |
| |
| assertEquals(ACCOUNT_1_A.name, si.getAccountName()); |
| assertEquals(ACCOUNT_1_A.type, si.getAccountType()); |
| assertEquals(APP1_AUTHORITY, si.getAuthority()); |
| |
| Bundle extras = ParcelUtils.fromBytes(si.getExtras().toByteArray()); |
| assertTrue(extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE)); |
| } |
| |
| @Test |
| public void testSoftErrorRetriesActiveApp() throws Exception { |
| removeAllAccounts(); |
| |
| // Let the initial sync happen. |
| addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY); |
| |
| writeSyncConfig(2, 1, 2, 3); |
| |
| clearSyncInvocations(APP1_PACKAGE); |
| |
| AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_ACTIVE); |
| |
| // Set soft error. |
| mRpc.invoke(APP1_PACKAGE, rb -> |
| rb.setSetResult(SetResult.newBuilder().setResult(Result.SOFT_ERROR))); |
| |
| Bundle b = makeBundle( |
| "testSoftErrorRetriesActiveApp", true, |
| ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); |
| |
| ContentResolver.requestSync(ACCOUNT_1_A, APP1_AUTHORITY, b); |
| |
| // First sync + 3 retries == 4, so should be called more than 4 times. |
| // But it's active, so it should retry more than that. |
| waitUntil("Should retry more than 3 times.", () -> { |
| final Response res = mRpc.invoke(APP1_PACKAGE, |
| rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder())); |
| final int calls = res.getSyncInvocations().getSyncInvocationsCount(); |
| Log.i(TAG, "NumSyncInvocations=" + calls); |
| return calls > 4; // Arbitrarily bigger than 4. |
| }); |
| } |
| |
| // WIP This test doesn't work yet. |
| // @Test |
| // public void testSoftErrorRetriesFrequentApp() throws Exception { |
| // runTest(() -> { |
| // removeAllAccounts(); |
| // |
| // // Let the initial sync happen. |
| // addAccountAndLetInitialSyncRun(ACCOUNT_1_A, APP1_AUTHORITY); |
| // |
| // writeSyncConfig(2, 1, 2, 3); |
| // |
| // clearSyncInvocations(APP1_PACKAGE); |
| // |
| // AmUtils.setStandbyBucket(APP1_PACKAGE, UsageStatsManager.STANDBY_BUCKET_FREQUENT); |
| // |
| // // Set soft error. |
| // mRpc.invoke(APP1_PACKAGE, rb -> |
| // rb.setSetResult(SetResult.newBuilder().setResult(Result.SOFT_ERROR))); |
| // |
| // Bundle b = makeBundle( |
| // "testSoftErrorRetriesFrequentApp", true, |
| // ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); |
| // |
| // ContentResolver.requestSync(ACCOUNT_1_A, APP1_AUTHORITY, b); |
| // |
| // waitUntil("Should retry more than 3 times.", () -> { |
| // final Response res = mRpc.invoke(APP1_PACKAGE, |
| // rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder())); |
| // final int calls = res.getSyncInvocations().getSyncInvocationsCount(); |
| // Log.i(TAG, "NumSyncInvocations=" + calls); |
| // return calls >= 4; // First sync + 3 retries == 4, so at least 4 times. |
| // }); |
| // |
| // Thread.sleep(10_000); |
| // |
| // // One more retry is okay because of how the job scheduler throttle jobs, but no further. |
| // final Response res = mRpc.invoke(APP1_PACKAGE, |
| // rb -> rb.setGetSyncInvocations(GetSyncInvocations.newBuilder())); |
| // final int calls = res.getSyncInvocations().getSyncInvocationsCount(); |
| // assertTrue("# of syncs must be equal or less than 5, but was " + calls, calls <= 5); |
| // }); |
| // } |
| } |