blob: 380e2226eb0c2f059c38e034e030069563c60d73 [file] [log] [blame]
/*
* Copyright 2014 Google Inc. All rights reserved.
*
* 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 com.google.samples.apps.iosched.sync;
import android.accounts.Account;
import android.content.*;
import android.net.ConnectivityManager;
import android.os.Bundle;
import com.google.samples.apps.iosched.Config;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.service.SessionAlarmService;
import com.google.samples.apps.iosched.service.SessionCalendarService;
import com.google.samples.apps.iosched.sync.userdata.AbstractUserDataSyncHelper;
import com.google.samples.apps.iosched.sync.userdata.UserDataSyncHelperFactory;
import com.google.samples.apps.iosched.util.AccountUtils;
import com.google.samples.apps.iosched.util.PrefUtils;
import com.google.samples.apps.iosched.util.UIUtils;
import java.io.IOException;
import static com.google.samples.apps.iosched.util.LogUtils.*;
/**
* A helper class for dealing with conference data synchronization.
* All operations occur on the thread they're called from, so it's best to wrap
* calls in an {@link android.os.AsyncTask}, or better yet, a
* {@link android.app.Service}.
*/
public class SyncHelper {
private static final String TAG = makeLogTag("SyncHelper");
private Context mContext;
private ConferenceDataHandler mConferenceDataHandler;
private RemoteConferenceDataFetcher mRemoteDataFetcher;
public SyncHelper(Context context) {
mContext = context;
mConferenceDataHandler = new ConferenceDataHandler(mContext);
mRemoteDataFetcher = new RemoteConferenceDataFetcher(mContext);
}
public static void requestManualSync(Account mChosenAccount) {
requestManualSync(mChosenAccount, false);
}
public static void requestManualSync(Account mChosenAccount, boolean userDataSyncOnly) {
if (mChosenAccount != null) {
LOGD(TAG, "Requesting manual sync for account " + mChosenAccount.name
+" userDataSyncOnly="+userDataSyncOnly);
Bundle b = new Bundle();
b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
if (userDataSyncOnly) {
b.putBoolean(SyncAdapter.EXTRA_SYNC_USER_DATA_ONLY, true);
}
ContentResolver.setSyncAutomatically(mChosenAccount, ScheduleContract.CONTENT_AUTHORITY, true);
ContentResolver.setIsSyncable(mChosenAccount, ScheduleContract.CONTENT_AUTHORITY, 1);
boolean pending = ContentResolver.isSyncPending(mChosenAccount,
ScheduleContract.CONTENT_AUTHORITY);
if (pending) {
LOGD(TAG, "Warning: sync is PENDING. Will cancel.");
}
boolean active = ContentResolver.isSyncActive(mChosenAccount,
ScheduleContract.CONTENT_AUTHORITY);
if (active) {
LOGD(TAG, "Warning: sync is ACTIVE. Will cancel.");
}
if (pending || active) {
LOGD(TAG, "Cancelling previously pending/active sync.");
ContentResolver.cancelSync(mChosenAccount, ScheduleContract.CONTENT_AUTHORITY);
}
LOGD(TAG, "Requesting sync now.");
ContentResolver.requestSync(mChosenAccount, ScheduleContract.CONTENT_AUTHORITY, b);
} else {
LOGD(TAG, "Can't request manual sync -- no chosen account.");
}
}
/**
* Attempts to perform conference data synchronization. The data comes from the remote URL
* configured in {@link com.google.samples.apps.iosched.Config#MANIFEST_URL}. The remote URL
* must point to a manifest file that, in turn, can reference other files. For more details
* about conference data synchronization, refer to the documentation at
* http://code.google.com/p/iosched.
*
* @param syncResult (optional) the sync result object to update with statistics.
* @param account the account associated with this sync
* @return Whether or not the synchronization made any changes to the data.
*/
public boolean performSync(SyncResult syncResult, Account account, Bundle extras) {
boolean dataChanged = false;
if (!PrefUtils.isDataBootstrapDone(mContext)) {
LOGD(TAG, "Sync aborting (data bootstrap not done yet)");
return false;
}
long lastAttemptTime = PrefUtils.getLastSyncAttemptedTime(mContext);
long now = UIUtils.getCurrentTime(mContext);
long timeSinceAttempt = now - lastAttemptTime;
final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
final boolean userDataOnly = extras.getBoolean(SyncAdapter.EXTRA_SYNC_USER_DATA_ONLY, false);
if (!manualSync && timeSinceAttempt >= 0 && timeSinceAttempt < Config.MIN_INTERVAL_BETWEEN_SYNCS) {
/*
Code removed because it was causing a runaway sync; probably because we are setting
syncResult.delayUntil incorrectly.
Random r = new Random();
long toWait = 10000 + r.nextInt(30000) // random jitter between 10 - 40 seconds
+ Config.MIN_INTERVAL_BETWEEN_SYNCS - timeSinceAttempt;
LOGW(TAG, "Sync throttled!! Another sync was attempted just " + timeSinceAttempt
+ "ms ago. Requesting delay of " + toWait + "ms.");
syncResult.fullSyncRequested = true;
syncResult.delayUntil = (System.currentTimeMillis() + toWait) / 1000L;
return false;*/
}
LOGI(TAG, "Performing sync for account: " + account);
PrefUtils.markSyncAttemptedNow(mContext);
long opStart;
long remoteSyncDuration, choresDuration;
opStart = System.currentTimeMillis();
// remote sync consists of these operations, which we try one by one (and tolerate
// individual failures on each)
final int OP_REMOTE_SYNC = 0;
final int OP_USER_SCHEDULE_SYNC = 1;
final int OP_USER_FEEDBACK_SYNC = 2;
int[] opsToPerform = userDataOnly ?
new int[] { OP_USER_SCHEDULE_SYNC } :
new int[] { OP_REMOTE_SYNC, OP_USER_SCHEDULE_SYNC, OP_USER_FEEDBACK_SYNC};
for (int op : opsToPerform) {
try {
switch (op) {
case OP_REMOTE_SYNC:
dataChanged |= doRemoteSync();
break;
case OP_USER_SCHEDULE_SYNC:
dataChanged |= doUserScheduleSync(account.name);
break;
case OP_USER_FEEDBACK_SYNC:
doUserFeedbackSync();
break;
}
} catch (AuthException ex) {
syncResult.stats.numAuthExceptions++;
// if we have a token, try to refresh it
if (AccountUtils.hasToken(mContext, account.name)) {
AccountUtils.refreshAuthToken(mContext);
} else {
LOGW(TAG, "No auth token yet for this account. Skipping remote sync.");
}
} catch (Throwable throwable) {
throwable.printStackTrace();
LOGE(TAG, "Error performing remote sync.");
increaseIoExceptions(syncResult);
}
}
remoteSyncDuration = System.currentTimeMillis() - opStart;
// If data has changed, there are a few chores we have to do
opStart = System.currentTimeMillis();
if (dataChanged) {
try {
performPostSyncChores(mContext);
} catch (Throwable throwable) {
throwable.printStackTrace();
LOGE(TAG, "Error performing post sync chores.");
}
}
clearExpertsIfNecessary();
choresDuration = System.currentTimeMillis() - opStart;
int operations = mConferenceDataHandler.getContentProviderOperationsDone();
if (syncResult != null && syncResult.stats != null) {
syncResult.stats.numEntries += operations;
syncResult.stats.numUpdates += operations;
}
if (dataChanged) {
long totalDuration = choresDuration + remoteSyncDuration;
LOGD(TAG, "SYNC STATS:\n" +
" * Account synced: " + (account == null ? "null" : account.name) + "\n" +
" * Content provider operations: " + operations + "\n" +
" * Remote sync took: " + remoteSyncDuration + "ms\n" +
" * Post-sync chores took: " + choresDuration + "ms\n" +
" * Total time: " + totalDuration + "ms\n" +
" * Total data read from cache: \n" +
(mRemoteDataFetcher.getTotalBytesReadFromCache() / 1024) + "kB\n" +
" * Total data downloaded: \n" +
(mRemoteDataFetcher.getTotalBytesDownloaded() / 1024) + "kB");
}
LOGI(TAG, "End of sync (" + (dataChanged ? "data changed" : "no data change") + ")");
updateSyncInterval(mContext, account);
return dataChanged;
}
public static void performPostSyncChores(final Context context) {
// Update search index
LOGD(TAG, "Updating search index.");
context.getContentResolver().update(ScheduleContract.SearchIndex.CONTENT_URI,
new ContentValues(), null, null);
// Sync calendars
LOGD(TAG, "Session data changed. Syncing starred sessions with Calendar.");
syncCalendar(context);
}
private static void syncCalendar(Context context) {
Intent intent = new Intent(SessionCalendarService.ACTION_UPDATE_ALL_SESSIONS_CALENDAR);
intent.setClass(context, SessionCalendarService.class);
context.startService(intent);
}
private void doUserFeedbackSync() {
LOGD(TAG, "Syncing feedback");
new FeedbackSyncHelper(mContext).sync();
}
/**
* Checks if the remote server has new data that we need to import. If so, download
* the new data and import it into the database.
*
* @return Whether or not data was changed.
* @throws IOException if there is a problem downloading or importing the data.
*/
private boolean doRemoteSync() throws IOException {
if (!isOnline()) {
LOGD(TAG, "Not attempting remote sync because device is OFFLINE");
return false;
}
LOGD(TAG, "Starting remote sync.");
// Fetch the remote data files via RemoteConferenceDataFetcher
String[] dataFiles = mRemoteDataFetcher.fetchConferenceDataIfNewer(
mConferenceDataHandler.getDataTimestamp());
if (dataFiles != null) {
LOGI(TAG, "Applying remote data.");
// save the remote data to the database
mConferenceDataHandler.applyConferenceData(dataFiles,
mRemoteDataFetcher.getServerDataTimestamp(), true);
LOGI(TAG, "Done applying remote data.");
// mark that conference data sync succeeded
PrefUtils.markSyncSucceededNow(mContext);
return true;
} else {
// no data to process (everything is up to date)
// mark that conference data sync succeeded
PrefUtils.markSyncSucceededNow(mContext);
return false;
}
}
/**
* Checks if there are changes on MySchedule to sync with/from remote AppData folder.
*
* @return Whether or not data was changed.
* @throws IOException if there is a problem uploading the data.
*/
private boolean doUserScheduleSync(String accountName) throws IOException {
if (!isOnline()) {
LOGD(TAG, "Not attempting myschedule sync because device is OFFLINE");
return false;
}
LOGD(TAG, "Starting user data (myschedule) sync.");
AbstractUserDataSyncHelper helper = UserDataSyncHelperFactory.buildSyncHelper(
mContext, accountName);
boolean modified = helper.sync();
if (modified) {
// schedule notifications for the starred sessions
Intent scheduleIntent = new Intent(
SessionAlarmService.ACTION_SCHEDULE_ALL_STARRED_BLOCKS,
null, mContext, SessionAlarmService.class);
mContext.startService(scheduleIntent);
}
return modified;
}
// Returns whether we are connected to the internet.
private boolean isOnline() {
ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo() != null &&
cm.getActiveNetworkInfo().isConnectedOrConnecting();
}
private void increaseIoExceptions(SyncResult syncResult) {
if (syncResult != null && syncResult.stats != null) {
++syncResult.stats.numIoExceptions;
}
}
private void increaseSuccesses(SyncResult syncResult) {
if (syncResult != null && syncResult.stats != null) {
++syncResult.stats.numEntries;
++syncResult.stats.numUpdates;
}
}
private boolean clearExpertsIfNecessary() {
if (Config.hasExpertsDirectoryExpired()) {
return 0 < mContext.getContentResolver()
.delete(ScheduleContract.Experts.CONTENT_URI, null, null);
}
return false;
}
public static class AuthException extends RuntimeException {
}
public static long calculateRecommendedSyncInterval(final Context context) {
long now = UIUtils.getCurrentTime(context);
long aroundConferenceStart = Config.CONFERENCE_START_MILLIS - Config.AUTO_SYNC_AROUND_CONFERENCE_THRESH;
if (now < aroundConferenceStart) {
return Config.AUTO_SYNC_INTERVAL_LONG_BEFORE_CONFERENCE;
} else if (now <= Config.CONFERENCE_END_MILLIS) {
return Config.AUTO_SYNC_INTERVAL_AROUND_CONFERENCE;
} else {
return Config.AUTO_SYNC_INTERVAL_AFTER_CONFERENCE;
}
}
public static void updateSyncInterval(final Context context, final Account account) {
LOGD(TAG, "Checking sync interval for " + account);
long recommended = calculateRecommendedSyncInterval(context);
long current = PrefUtils.getCurSyncInterval(context);
LOGD(TAG, "Recommended sync interval " + recommended + ", current " + current);
if (recommended != current) {
LOGD(TAG, "Setting up sync for account " + account + ", interval " + recommended + "ms");
ContentResolver.setIsSyncable(account, ScheduleContract.CONTENT_AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ScheduleContract.CONTENT_AUTHORITY, true);
ContentResolver.addPeriodicSync(account, ScheduleContract.CONTENT_AUTHORITY,
new Bundle(), recommended / 1000L);
PrefUtils.setCurSyncInterval(context, recommended);
} else {
LOGD(TAG, "No need to update sync interval.");
}
}
}