| /* |
| * 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."); |
| } |
| } |
| } |