1/* 2 * Copyright (C) 2009 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 17package android.content; 18 19import android.accounts.Account; 20import android.os.Bundle; 21import android.os.IBinder; 22import android.os.Process; 23import android.os.RemoteException; 24 25import java.util.concurrent.atomic.AtomicInteger; 26 27/** 28 * An abstract implementation of a SyncAdapter that spawns a thread to invoke a sync operation. 29 * If a sync operation is already in progress when a startSync() request is received then an error 30 * will be returned to the new request and the existing request will be allowed to continue. 31 * When a startSync() is received and there is no sync operation in progress then a thread 32 * will be started to run the operation and {@link #onPerformSync} will be invoked on that thread. 33 * If a cancelSync() is received that matches an existing sync operation then the thread 34 * that is running that sync operation will be interrupted, which will indicate to the thread 35 * that the sync has been canceled. 36 */ 37public abstract class AbstractThreadedSyncAdapter { 38 /** 39 * Kernel event log tag. Also listed in data/etc/event-log-tags. 40 * @deprecated Private constant. May go away in the next release. 41 */ 42 @Deprecated 43 public static final int LOG_SYNC_DETAILS = 2743; 44 45 private final Context mContext; 46 private final AtomicInteger mNumSyncStarts; 47 private final ISyncAdapterImpl mISyncAdapterImpl; 48 49 // all accesses to this member variable must be synchronized on mSyncThreadLock 50 private SyncThread mSyncThread; 51 private final Object mSyncThreadLock = new Object(); 52 53 private final boolean mAutoInitialize; 54 55 /** 56 * Creates an {@link AbstractThreadedSyncAdapter}. 57 * @param context the {@link android.content.Context} that this is running within. 58 * @param autoInitialize if true then sync requests that have 59 * {@link ContentResolver#SYNC_EXTRAS_INITIALIZE} set will be internally handled by 60 * {@link AbstractThreadedSyncAdapter} by calling 61 * {@link ContentResolver#setIsSyncable(android.accounts.Account, String, int)} with 1 if it 62 * is currently set to <0. 63 */ 64 public AbstractThreadedSyncAdapter(Context context, boolean autoInitialize) { 65 mContext = context; 66 mISyncAdapterImpl = new ISyncAdapterImpl(); 67 mNumSyncStarts = new AtomicInteger(0); 68 mSyncThread = null; 69 mAutoInitialize = autoInitialize; 70 } 71 72 public Context getContext() { 73 return mContext; 74 } 75 76 private class ISyncAdapterImpl extends ISyncAdapter.Stub { 77 public void startSync(ISyncContext syncContext, String authority, Account account, 78 Bundle extras) { 79 final SyncContext syncContextClient = new SyncContext(syncContext); 80 81 boolean alreadyInProgress; 82 // synchronize to make sure that mSyncThread doesn't change between when we 83 // check it and when we use it 84 synchronized (mSyncThreadLock) { 85 if (mSyncThread == null) { 86 if (mAutoInitialize 87 && extras != null 88 && extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { 89 if (ContentResolver.getIsSyncable(account, authority) < 0) { 90 ContentResolver.setIsSyncable(account, authority, 1); 91 } 92 syncContextClient.onFinished(new SyncResult()); 93 return; 94 } 95 mSyncThread = new SyncThread( 96 "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(), 97 syncContextClient, authority, account, extras); 98 mSyncThread.start(); 99 alreadyInProgress = false; 100 } else { 101 alreadyInProgress = true; 102 } 103 } 104 105 // do this outside since we don't want to call back into the syncContext while 106 // holding the synchronization lock 107 if (alreadyInProgress) { 108 syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); 109 } 110 } 111 112 public void cancelSync(ISyncContext syncContext) { 113 // synchronize to make sure that mSyncThread doesn't change between when we 114 // check it and when we use it 115 final SyncThread syncThread; 116 synchronized (mSyncThreadLock) { 117 syncThread = mSyncThread; 118 } 119 if (syncThread != null 120 && syncThread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { 121 onSyncCanceled(); 122 } 123 } 124 125 public void initialize(Account account, String authority) throws RemoteException { 126 Bundle extras = new Bundle(); 127 extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); 128 startSync(null, authority, account, extras); 129 } 130 } 131 132 /** 133 * The thread that invokes {@link AbstractThreadedSyncAdapter#onPerformSync}. It also acquires 134 * the provider for this sync before calling onPerformSync and releases it afterwards. Cancel 135 * this thread in order to cancel the sync. 136 */ 137 private class SyncThread extends Thread { 138 private final SyncContext mSyncContext; 139 private final String mAuthority; 140 private final Account mAccount; 141 private final Bundle mExtras; 142 143 private SyncThread(String name, SyncContext syncContext, String authority, 144 Account account, Bundle extras) { 145 super(name); 146 mSyncContext = syncContext; 147 mAuthority = authority; 148 mAccount = account; 149 mExtras = extras; 150 } 151 152 public void run() { 153 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 154 155 if (isCanceled()) { 156 return; 157 } 158 159 SyncResult syncResult = new SyncResult(); 160 ContentProviderClient provider = null; 161 try { 162 provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority); 163 if (provider != null) { 164 AbstractThreadedSyncAdapter.this.onPerformSync(mAccount, mExtras, 165 mAuthority, provider, syncResult); 166 } else { 167 syncResult.databaseError = true; 168 } 169 } finally { 170 if (provider != null) { 171 provider.release(); 172 } 173 if (!isCanceled()) { 174 mSyncContext.onFinished(syncResult); 175 } 176 // synchronize so that the assignment will be seen by other threads 177 // that also synchronize accesses to mSyncThread 178 synchronized (mSyncThreadLock) { 179 mSyncThread = null; 180 } 181 } 182 } 183 184 private boolean isCanceled() { 185 return Thread.currentThread().isInterrupted(); 186 } 187 } 188 189 /** 190 * @return a reference to the IBinder of the SyncAdapter service. 191 */ 192 public final IBinder getSyncAdapterBinder() { 193 return mISyncAdapterImpl.asBinder(); 194 } 195 196 /** 197 * Perform a sync for this account. SyncAdapter-specific parameters may 198 * be specified in extras, which is guaranteed to not be null. Invocations 199 * of this method are guaranteed to be serialized. 200 * 201 * @param account the account that should be synced 202 * @param extras SyncAdapter-specific parameters 203 * @param authority the authority of this sync request 204 * @param provider a ContentProviderClient that points to the ContentProvider for this 205 * authority 206 * @param syncResult SyncAdapter-specific parameters 207 */ 208 public abstract void onPerformSync(Account account, Bundle extras, 209 String authority, ContentProviderClient provider, SyncResult syncResult); 210 211 /** 212 * Indicates that a sync operation has been canceled. This will be invoked on a separate 213 * thread than the sync thread and so you must consider the multi-threaded implications 214 * of the work that you do in this method. 215 * 216 */ 217 public void onSyncCanceled() { 218 final SyncThread syncThread; 219 synchronized (mSyncThreadLock) { 220 syncThread = mSyncThread; 221 } 222 if (syncThread != null) { 223 syncThread.interrupt(); 224 } 225 } 226} 227