blob: c0db7fbfab5a3460937f37e8a9ea0a00995d7f53 [file] [log] [blame]
/*
* 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);
// });
// }
}