diff options
31 files changed, 2630 insertions, 2061 deletions
@@ -85,8 +85,10 @@ LOCAL_SRC_FILES += \ core/java/android/bluetooth/IBluetoothDevice.aidl \ core/java/android/bluetooth/IBluetoothDeviceCallback.aidl \ core/java/android/bluetooth/IBluetoothHeadset.aidl \ + core/java/android/content/IContentService.aidl \ core/java/android/content/ISyncAdapter.aidl \ core/java/android/content/ISyncContext.aidl \ + core/java/android/content/ISyncStatusObserver.aidl \ core/java/android/content/pm/IPackageDataObserver.aidl \ core/java/android/content/pm/IPackageDeleteObserver.aidl \ core/java/android/content/pm/IPackageInstallObserver.aidl \ diff --git a/api/current.xml b/api/current.xml index f01ed14..2bab739 100644 --- a/api/current.xml +++ b/api/current.xml @@ -92268,6 +92268,19 @@ visibility="public" > </method> +<method name="getBroadcastCookie" + return="java.lang.Object" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="index" type="int"> +</parameter> +</method> <method name="getBroadcastItem" return="E" abstract="false" @@ -92305,6 +92318,21 @@ <parameter name="callback" type="E"> </parameter> </method> +<method name="onCallbackDied" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="callback" type="E"> +</parameter> +<parameter name="cookie" type="java.lang.Object"> +</parameter> +</method> <method name="register" return="boolean" abstract="false" @@ -92318,6 +92346,21 @@ <parameter name="callback" type="E"> </parameter> </method> +<method name="register" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="callback" type="E"> +</parameter> +<parameter name="cookie" type="java.lang.Object"> +</parameter> +</method> <method name="unregister" return="boolean" abstract="false" diff --git a/core/java/android/content/ActiveSyncInfo.aidl b/core/java/android/content/ActiveSyncInfo.aidl new file mode 100644 index 0000000..1142206 --- /dev/null +++ b/core/java/android/content/ActiveSyncInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 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; + +parcelable ActiveSyncInfo; diff --git a/core/java/android/content/ActiveSyncInfo.java b/core/java/android/content/ActiveSyncInfo.java new file mode 100644 index 0000000..63be8d1 --- /dev/null +++ b/core/java/android/content/ActiveSyncInfo.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2009 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; + +import android.os.Parcel; +import android.os.Parcelable.Creator; + +/** @hide */ +public class ActiveSyncInfo { + public final int authorityId; + public final String account; + public final String authority; + public final long startTime; + + ActiveSyncInfo(int authorityId, String account, String authority, + long startTime) { + this.authorityId = authorityId; + this.account = account; + this.authority = authority; + this.startTime = startTime; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(authorityId); + parcel.writeString(account); + parcel.writeString(authority); + parcel.writeLong(startTime); + } + + ActiveSyncInfo(Parcel parcel) { + authorityId = parcel.readInt(); + account = parcel.readString(); + authority = parcel.readString(); + startTime = parcel.readLong(); + } + + public static final Creator<ActiveSyncInfo> CREATOR = new Creator<ActiveSyncInfo>() { + public ActiveSyncInfo createFromParcel(Parcel in) { + return new ActiveSyncInfo(in); + } + + public ActiveSyncInfo[] newArray(int size) { + return new ActiveSyncInfo[size]; + } + }; +}
\ No newline at end of file diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 0a71d57..74144fc 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -25,9 +25,13 @@ import android.database.CursorWrapper; import android.database.IContentObserver; import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.os.ServiceManager; import android.text.TextUtils; +import android.util.Config; +import android.util.Log; import java.io.File; import java.io.FileInputStream; @@ -85,8 +89,7 @@ public abstract class ContentResolver { */ public static final String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; - public ContentResolver(Context context) - { + public ContentResolver(Context context) { mContext = context; } @@ -605,7 +608,7 @@ public abstract class ContentResolver { ContentObserver observer) { try { - ContentServiceNative.getDefault().registerContentObserver(uri, notifyForDescendents, + getContentService().registerContentObserver(uri, notifyForDescendents, observer.getContentObserver()); } catch (RemoteException e) { } @@ -621,7 +624,7 @@ public abstract class ContentResolver { try { IContentObserver contentObserver = observer.releaseContentObserver(); if (contentObserver != null) { - ContentServiceNative.getDefault().unregisterContentObserver( + getContentService().unregisterContentObserver( contentObserver); } } catch (RemoteException e) { @@ -651,7 +654,7 @@ public abstract class ContentResolver { */ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { try { - ContentServiceNative.getDefault().notifyChange( + getContentService().notifyChange( uri, observer == null ? null : observer.getContentObserver(), observer != null && observer.deliverSelfNotifications(), syncToNetwork); } catch (RemoteException e) { @@ -677,7 +680,7 @@ public abstract class ContentResolver { public void startSync(Uri uri, Bundle extras) { validateSyncExtrasBundle(extras); try { - ContentServiceNative.getDefault().startSync(uri, extras); + getContentService().startSync(uri, extras); } catch (RemoteException e) { } } @@ -718,7 +721,7 @@ public abstract class ContentResolver { public void cancelSync(Uri uri) { try { - ContentServiceNative.getDefault().cancelSync(uri); + getContentService().cancelSync(uri); } catch (RemoteException e) { } } @@ -779,6 +782,22 @@ public abstract class ContentResolver { } } + /** @hide */ + public static final String CONTENT_SERVICE_NAME = "content"; + + /** @hide */ + public static IContentService getContentService() { + if (sContentService != null) { + return sContentService; + } + IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME); + if (Config.LOGV) Log.v("ContentService", "default service binder = " + b); + sContentService = IContentService.Stub.asInterface(b); + if (Config.LOGV) Log.v("ContentService", "default service = " + sContentService); + return sContentService; + } + + private static IContentService sContentService; private final Context mContext; private static final String TAG = "ContentResolver"; } diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index b028868..6cd2c54 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -21,6 +21,7 @@ import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; +import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Config; @@ -34,7 +35,7 @@ import java.util.ArrayList; /** * {@hide} */ -public final class ContentService extends ContentServiceNative { +public final class ContentService extends IContentService.Stub { private static final String TAG = "ContentService"; private Context mContext; private boolean mFactoryTest; @@ -73,6 +74,21 @@ public final class ContentService extends ContentServiceNative { } } + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (RuntimeException e) { + // The content service only throws security exceptions, so let's + // log all others. + if (!(e instanceof SecurityException)) { + Log.e(TAG, "Content Service Crash", e); + } + throw e; + } + } + /*package*/ ContentService(Context context, boolean factoryTest) { mContext = context; mFactoryTest = factoryTest; @@ -204,9 +220,158 @@ public final class ContentService extends ContentServiceNative { } } + public boolean getSyncProviderAutomatically(String providerName) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, + "no permission to read the sync settings"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + return syncManager.getSyncStorageEngine().getSyncProviderAutomatically( + null, providerName); + } + } finally { + restoreCallingIdentity(identityToken); + } + return false; + } + + public void setSyncProviderAutomatically(String providerName, boolean sync) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + syncManager.getSyncStorageEngine().setSyncProviderAutomatically( + null, providerName, sync); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public boolean getListenForNetworkTickles() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, + "no permission to read the sync settings"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + return syncManager.getSyncStorageEngine().getListenForNetworkTickles(); + } + } finally { + restoreCallingIdentity(identityToken); + } + return false; + } + + public void setListenForNetworkTickles(boolean flag) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + syncManager.getSyncStorageEngine().setListenForNetworkTickles(flag); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public boolean isSyncActive(String account, String authority) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, + "no permission to read the sync stats"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + return syncManager.getSyncStorageEngine().isSyncActive( + account, authority); + } + } finally { + restoreCallingIdentity(identityToken); + } + return false; + } + + public ActiveSyncInfo getActiveSync() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, + "no permission to read the sync stats"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + return syncManager.getSyncStorageEngine().getActiveSync(); + } + } finally { + restoreCallingIdentity(identityToken); + } + return null; + } + + public SyncStatusInfo getStatusByAuthority(String authority) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, + "no permission to read the sync stats"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + return syncManager.getSyncStorageEngine().getStatusByAuthority( + authority); + } + } finally { + restoreCallingIdentity(identityToken); + } + return null; + } + + public boolean isAuthorityPending(String account, String authority) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, + "no permission to read the sync stats"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + return syncManager.getSyncStorageEngine().isAuthorityPending( + account, authority); + } + } finally { + restoreCallingIdentity(identityToken); + } + return false; + } + + public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + syncManager.getSyncStorageEngine().addStatusChangeListener( + mask, callback); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void removeStatusChangeListener(ISyncStatusObserver callback) { + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + syncManager.getSyncStorageEngine().removeStatusChangeListener( + callback); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + public static IContentService main(Context context, boolean factoryTest) { ContentService service = new ContentService(context, factoryTest); - ServiceManager.addService("content", service); + ServiceManager.addService(ContentResolver.CONTENT_SERVICE_NAME, service); return service; } diff --git a/core/java/android/content/ContentServiceNative.java b/core/java/android/content/ContentServiceNative.java deleted file mode 100644 index 364f9ee..0000000 --- a/core/java/android/content/ContentServiceNative.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (C) 2006 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; - -import android.database.IContentObserver; -import android.net.Uri; -import android.os.Binder; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.Parcel; -import android.os.ServiceManager; -import android.os.Bundle; -import android.util.Config; -import android.util.Log; - -/** - * {@hide} - */ -abstract class ContentServiceNative extends Binder implements IContentService -{ - public ContentServiceNative() - { - attachInterface(this, descriptor); - } - - /** - * Cast a Binder object into a content resolver interface, generating - * a proxy if needed. - */ - static public IContentService asInterface(IBinder obj) - { - if (obj == null) { - return null; - } - IContentService in = - (IContentService)obj.queryLocalInterface(descriptor); - if (in != null) { - return in; - } - - return new ContentServiceProxy(obj); - } - - /** - * Retrieve the system's default/global content service. - */ - static public IContentService getDefault() - { - if (gDefault != null) { - return gDefault; - } - IBinder b = ServiceManager.getService("content"); - if (Config.LOGV) Log.v("ContentService", "default service binder = " + b); - gDefault = asInterface(b); - if (Config.LOGV) Log.v("ContentService", "default service = " + gDefault); - return gDefault; - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) - { - try { - switch (code) { - case 5038: { - data.readString(); // ignore the interface token that service generated - Uri uri = Uri.parse(data.readString()); - notifyChange(uri, null, false, false); - return true; - } - - case REGISTER_CONTENT_OBSERVER_TRANSACTION: { - Uri uri = Uri.CREATOR.createFromParcel(data); - boolean notifyForDescendents = data.readInt() != 0; - IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder()); - registerContentObserver(uri, notifyForDescendents, observer); - return true; - } - - case UNREGISTER_CHANGE_OBSERVER_TRANSACTION: { - IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder()); - unregisterContentObserver(observer); - return true; - } - - case NOTIFY_CHANGE_TRANSACTION: { - Uri uri = Uri.CREATOR.createFromParcel(data); - IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder()); - boolean observerWantsSelfNotifications = data.readInt() != 0; - boolean syncToNetwork = data.readInt() != 0; - notifyChange(uri, observer, observerWantsSelfNotifications, syncToNetwork); - return true; - } - - case START_SYNC_TRANSACTION: { - Uri url = null; - int hasUrl = data.readInt(); - if (hasUrl != 0) { - url = Uri.CREATOR.createFromParcel(data); - } - startSync(url, data.readBundle()); - return true; - } - - case CANCEL_SYNC_TRANSACTION: { - Uri url = null; - int hasUrl = data.readInt(); - if (hasUrl != 0) { - url = Uri.CREATOR.createFromParcel(data); - } - cancelSync(url); - return true; - } - - default: - return super.onTransact(code, data, reply, flags); - } - } catch (Exception e) { - Log.e("ContentServiceNative", "Caught exception in transact", e); - } - - return false; - } - - public IBinder asBinder() - { - return this; - } - - private static IContentService gDefault; -} - - -final class ContentServiceProxy implements IContentService -{ - public ContentServiceProxy(IBinder remote) - { - mRemote = remote; - } - - public IBinder asBinder() - { - return mRemote; - } - - public void registerContentObserver(Uri uri, boolean notifyForDescendents, - IContentObserver observer) throws RemoteException - { - Parcel data = Parcel.obtain(); - uri.writeToParcel(data, 0); - data.writeInt(notifyForDescendents ? 1 : 0); - data.writeStrongInterface(observer); - mRemote.transact(REGISTER_CONTENT_OBSERVER_TRANSACTION, data, null, 0); - data.recycle(); - } - - public void unregisterContentObserver(IContentObserver observer) throws RemoteException { - Parcel data = Parcel.obtain(); - data.writeStrongInterface(observer); - mRemote.transact(UNREGISTER_CHANGE_OBSERVER_TRANSACTION, data, null, 0); - data.recycle(); - } - - public void notifyChange(Uri uri, IContentObserver observer, - boolean observerWantsSelfNotifications, boolean syncToNetwork) - throws RemoteException { - Parcel data = Parcel.obtain(); - uri.writeToParcel(data, 0); - data.writeStrongInterface(observer); - data.writeInt(observerWantsSelfNotifications ? 1 : 0); - data.writeInt(syncToNetwork ? 1 : 0); - mRemote.transact(NOTIFY_CHANGE_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); - data.recycle(); - } - - public void startSync(Uri url, Bundle extras) throws RemoteException { - Parcel data = Parcel.obtain(); - if (url == null) { - data.writeInt(0); - } else { - data.writeInt(1); - url.writeToParcel(data, 0); - } - extras.writeToParcel(data, 0); - mRemote.transact(START_SYNC_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); - data.recycle(); - } - - public void cancelSync(Uri url) throws RemoteException { - Parcel data = Parcel.obtain(); - if (url == null) { - data.writeInt(0); - } else { - data.writeInt(1); - url.writeToParcel(data, 0); - } - mRemote.transact(CANCEL_SYNC_TRANSACTION, data, null /* reply */, IBinder.FLAG_ONEWAY); - data.recycle(); - } - - private IBinder mRemote; -} - diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl new file mode 100644 index 0000000..8617d949 --- /dev/null +++ b/core/java/android/content/IContentService.aidl @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2009 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; + +import android.content.ActiveSyncInfo; +import android.content.ISyncStatusObserver; +import android.content.SyncStatusInfo; +import android.net.Uri; +import android.os.Bundle; +import android.database.IContentObserver; + +/** + * @hide + */ +interface IContentService { + void registerContentObserver(in Uri uri, boolean notifyForDescendentsn, + IContentObserver observer); + void unregisterContentObserver(IContentObserver observer); + + void notifyChange(in Uri uri, IContentObserver observer, + boolean observerWantsSelfNotifications, boolean syncToNetwork); + + void startSync(in Uri url, in Bundle extras); + void cancelSync(in Uri uri); + + /** + * Check if the provider should be synced when a network tickle is received + * @param providerName the provider whose setting we are querying + * @return true of the provider should be synced when a network tickle is received + */ + boolean getSyncProviderAutomatically(String providerName); + + /** + * Set whether or not the provider is synced when it receives a network tickle. + * + * @param providerName the provider whose behavior is being controlled + * @param sync true if the provider should be synced when tickles are received for it + */ + void setSyncProviderAutomatically(String providerName, boolean sync); + + void setListenForNetworkTickles(boolean flag); + + boolean getListenForNetworkTickles(); + + /** + * Returns true if there is currently a sync operation for the given + * account or authority in the pending list, or actively being processed. + */ + boolean isSyncActive(String account, String authority); + + ActiveSyncInfo getActiveSync(); + + /** + * Returns the status that matches the authority. If there are multiples accounts for + * the authority, the one with the latest "lastSuccessTime" status is returned. + * @param authority the authority whose row should be selected + * @return the SyncStatusInfo for the authority, or null if none exists + */ + SyncStatusInfo getStatusByAuthority(String authority); + + /** + * Return true if the pending status is true of any matching authorities. + */ + boolean isAuthorityPending(String account, String authority); + + void addStatusChangeListener(int mask, ISyncStatusObserver callback); + + void removeStatusChangeListener(ISyncStatusObserver callback); +} diff --git a/core/java/android/content/IContentService.java b/core/java/android/content/IContentService.java deleted file mode 100644 index a3047da..0000000 --- a/core/java/android/content/IContentService.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2006 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; - -import android.database.IContentObserver; -import android.net.Uri; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Bundle; - -/** - * {@hide} - */ -public interface IContentService extends IInterface -{ - public void registerContentObserver(Uri uri, boolean notifyForDescendentsn, - IContentObserver observer) throws RemoteException; - public void unregisterContentObserver(IContentObserver observer) throws RemoteException; - - public void notifyChange(Uri uri, IContentObserver observer, - boolean observerWantsSelfNotifications, boolean syncToNetwork) - throws RemoteException; - - public void startSync(Uri url, Bundle extras) throws RemoteException; - public void cancelSync(Uri uri) throws RemoteException; - - static final String SERVICE_NAME = "content"; - - /* IPC constants */ - static final String descriptor = "android.content.IContentService"; - - static final int REGISTER_CONTENT_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1; - static final int UNREGISTER_CHANGE_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; - static final int NOTIFY_CHANGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; - static final int START_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4; - static final int CANCEL_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5; -} - diff --git a/core/java/android/content/ISyncStatusObserver.aidl b/core/java/android/content/ISyncStatusObserver.aidl new file mode 100644 index 0000000..eb26845 --- /dev/null +++ b/core/java/android/content/ISyncStatusObserver.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2009 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; + +/** + * @hide + */ +oneway interface ISyncStatusObserver { + void onStatusChanged(int which); +} diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 96470c3..ade5f3d 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -32,8 +32,6 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; -import android.database.Cursor; -import android.database.DatabaseUtils; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -43,18 +41,13 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; -import android.os.Parcel; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; -import android.preference.Preference; -import android.preference.PreferenceGroup; -import android.provider.Sync; import android.provider.Settings; -import android.provider.Sync.History; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; @@ -73,13 +66,12 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; -import java.util.Observer; -import java.util.Observable; /** * @hide @@ -138,7 +130,6 @@ class SyncManager { volatile private PowerManager.WakeLock mHandleAlarmWakeLock; volatile private boolean mDataConnectionIsConnected = false; volatile private boolean mStorageIsLow = false; - private Sync.Settings.QueryMap mSyncSettings; private final NotificationManager mNotificationMgr; private AlarmManager mAlarmService = null; @@ -225,17 +216,6 @@ class SyncManager { private static final String SYNC_POLL_ALARM = "android.content.syncmanager.SYNC_POLL_ALARM"; private final SyncHandler mSyncHandler; - private static final String[] SYNC_ACTIVE_PROJECTION = new String[]{ - Sync.Active.ACCOUNT, - Sync.Active.AUTHORITY, - Sync.Active.START_TIME, - }; - - private static final String[] SYNC_PENDING_PROJECTION = new String[]{ - Sync.Pending.ACCOUNT, - Sync.Pending.AUTHORITY - }; - private static final int MAX_SYNC_POLL_DELAY_SECONDS = 36 * 60 * 60; // 36 hours private static final int MIN_SYNC_POLL_DELAY_SECONDS = 24 * 60 * 60; // 24 hours @@ -289,6 +269,14 @@ class SyncManager { HANDLE_SYNC_ALARM_WAKE_LOCK); mHandleAlarmWakeLock.setReferenceCounted(false); + mSyncStorageEngine.addStatusChangeListener( + SyncStorageEngine.CHANGE_SETTINGS, new ISyncStatusObserver.Stub() { + public void onStatusChanged(int which) { + // force the sync loop to run if the settings change + sendCheckAlarmsMessage(); + } + }); + if (!factoryTest) { AccountMonitorListener listener = new AccountMonitorListener() { public void onAccountsUpdated(String[] accounts) { @@ -448,20 +436,10 @@ class SyncManager { return mActiveSyncContext; } - private Sync.Settings.QueryMap getSyncSettings() { - if (mSyncSettings == null) { - mSyncSettings = new Sync.Settings.QueryMap(mContext.getContentResolver(), true, - new Handler()); - mSyncSettings.addObserver(new Observer(){ - public void update(Observable o, Object arg) { - // force the sync loop to run if the settings change - sendCheckAlarmsMessage(); - } - }); - } - return mSyncSettings; + public SyncStorageEngine getSyncStorageEngine() { + return mSyncStorageEngine; } - + private void ensureContentResolver() { if (mContentResolver == null) { mContentResolver = mContext.getContentResolver(); @@ -574,15 +552,15 @@ class SyncManager { int source; if (uploadOnly) { - source = Sync.History.SOURCE_LOCAL; + source = SyncStorageEngine.SOURCE_LOCAL; } else if (force) { - source = Sync.History.SOURCE_USER; + source = SyncStorageEngine.SOURCE_USER; } else if (url == null) { - source = Sync.History.SOURCE_POLL; + source = SyncStorageEngine.SOURCE_POLL; } else { // this isn't strictly server, since arbitrary callers can (and do) request // a non-forced two-way sync on a specific url - source = Sync.History.SOURCE_SERVER; + source = SyncStorageEngine.SOURCE_SERVER; } List<String> names = new ArrayList<String>(); @@ -667,9 +645,7 @@ class SyncManager { public void updateHeartbeatTime() { mHeartbeatTime = SystemClock.elapsedRealtime(); - ensureContentResolver(); - mContentResolver.notifyChange(Sync.Active.CONTENT_URI, - null /* this change wasn't made through an observer */); + mSyncStorageEngine.reportActiveChange(); } private void sendSyncAlarmMessage() { @@ -876,7 +852,7 @@ class SyncManager { final String key; long earliestRunTime; long delay; - Long rowId = null; + SyncStorageEngine.PendingOperation pendingOperation; SyncOperation(String account, int source, String authority, Bundle extras, long delay) { this.account = account; @@ -916,7 +892,7 @@ class SyncManager { sb.append(" when: ").append(earliestRunTime); sb.append(" delay: ").append(delay); sb.append(" key: {").append(key).append("}"); - if (rowId != null) sb.append(" rowId: ").append(rowId); + if (pendingOperation != null) sb.append(" pendingOperation: ").append(pendingOperation); return sb.toString(); } @@ -999,244 +975,264 @@ class SyncManager { protected void dump(FileDescriptor fd, PrintWriter pw) { StringBuilder sb = new StringBuilder(); - dumpSyncState(sb); - sb.append("\n"); + dumpSyncState(pw, sb); if (isSyncEnabled()) { - dumpSyncHistory(sb); + dumpSyncHistory(pw, sb); } - pw.println(sb.toString()); } - protected void dumpSyncState(StringBuilder sb) { - sb.append("sync enabled: ").append(isSyncEnabled()).append("\n"); - sb.append("data connected: ").append(mDataConnectionIsConnected).append("\n"); - sb.append("memory low: ").append(mStorageIsLow).append("\n"); + static String formatTime(long time) { + Time tobj = new Time(); + tobj.set(time); + return tobj.format("%Y-%m-%d %H:%M:%S"); + } + + protected void dumpSyncState(PrintWriter pw, StringBuilder sb) { + pw.print("sync enabled: "); pw.println(isSyncEnabled()); + pw.print("data connected: "); pw.println(mDataConnectionIsConnected); + pw.print("memory low: "); pw.println(mStorageIsLow); final String[] accounts = mAccounts; - sb.append("accounts: "); + pw.print("accounts: "); if (accounts != null) { - sb.append(accounts.length); + pw.println(accounts.length); } else { - sb.append("none"); + pw.println("none"); } - sb.append("\n"); final long now = SystemClock.elapsedRealtime(); - sb.append("now: ").append(now).append("\n"); - sb.append("uptime: ").append(DateUtils.formatElapsedTime(now/1000)).append(" (HH:MM:SS)\n"); - sb.append("time spent syncing : ") - .append(DateUtils.formatElapsedTime( - mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000)) - .append(" (HH:MM:SS), sync ") - .append(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not ") - .append("in progress").append("\n"); + pw.print("now: "); pw.println(now); + pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000)); + pw.println(" (HH:MM:SS)"); + pw.print("time spent syncing: "); + pw.print(DateUtils.formatElapsedTime( + mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000)); + pw.print(" (HH:MM:SS), sync "); + pw.print(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not "); + pw.println("in progress"); if (mSyncHandler.mAlarmScheduleTime != null) { - sb.append("next alarm time: ").append(mSyncHandler.mAlarmScheduleTime) - .append(" (") - .append(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000)) - .append(" (HH:MM:SS) from now)\n"); + pw.print("next alarm time: "); pw.print(mSyncHandler.mAlarmScheduleTime); + pw.print(" ("); + pw.print(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000)); + pw.println(" (HH:MM:SS) from now)"); } else { - sb.append("no alarm is scheduled (there had better not be any pending syncs)\n"); + pw.println("no alarm is scheduled (there had better not be any pending syncs)"); } - sb.append("active sync: ").append(mActiveSyncContext).append("\n"); + pw.print("active sync: "); pw.println(mActiveSyncContext); - sb.append("notification info: "); + pw.print("notification info: "); + sb.setLength(0); mSyncHandler.mSyncNotificationInfo.toString(sb); - sb.append("\n"); + pw.println(sb.toString()); synchronized (mSyncQueue) { - sb.append("sync queue: "); + pw.print("sync queue: "); + sb.setLength(0); mSyncQueue.dump(sb); - } - - Cursor c = mSyncStorageEngine.query(Sync.Active.CONTENT_URI, - SYNC_ACTIVE_PROJECTION, null, null, null); - sb.append("\n"); - try { - if (c.moveToNext()) { - final long durationInSeconds = (now - c.getLong(2)) / 1000; - sb.append("Active sync: ").append(c.getString(0)) - .append(" ").append(c.getString(1)) - .append(", duration is ") - .append(DateUtils.formatElapsedTime(durationInSeconds)).append(".\n"); - } else { - sb.append("No sync is in progress.\n"); - } - } finally { - c.close(); - } - - c = mSyncStorageEngine.query(Sync.Pending.CONTENT_URI, - SYNC_PENDING_PROJECTION, null, null, "account, authority"); - sb.append("\nPending Syncs\n"); - try { - if (c.getCount() != 0) { - dumpSyncPendingHeader(sb); - while (c.moveToNext()) { - dumpSyncPendingRow(sb, c); + pw.println(sb.toString()); + } + + ActiveSyncInfo active = mSyncStorageEngine.getActiveSync(); + if (active != null) { + SyncStorageEngine.AuthorityInfo authority + = mSyncStorageEngine.getAuthority(active.authorityId); + final long durationInSeconds = (now - active.startTime) / 1000; + pw.print("Active sync: "); + pw.print(authority != null ? authority.account : "<no account>"); + pw.print(" "); + pw.print(authority != null ? authority.authority : "<no account>"); + pw.print(", duration is "); + pw.println(DateUtils.formatElapsedTime(durationInSeconds)); + } else { + pw.println("No sync is in progress."); + } + + ArrayList<SyncStorageEngine.PendingOperation> ops + = mSyncStorageEngine.getPendingOperations(); + if (ops != null && ops.size() > 0) { + pw.println(); + pw.println("Pending Syncs"); + final int N = ops.size(); + for (int i=0; i<N; i++) { + SyncStorageEngine.PendingOperation op = ops.get(i); + pw.print(" #"); pw.print(i); pw.print(": account="); + pw.print(op.account); pw.print(" authority="); + pw.println(op.authority); + if (op.extras != null && op.extras.size() > 0) { + sb.setLength(0); + SyncOperation.extrasToStringBuilder(op.extras, sb); + pw.print(" extras: "); pw.println(sb.toString()); } - dumpSyncPendingFooter(sb); - } else { - sb.append("none\n"); } - } finally { - c.close(); } - String currentAccount = null; - c = mSyncStorageEngine.query(Sync.Status.CONTENT_URI, - STATUS_PROJECTION, null, null, "account, authority"); - sb.append("\nSync history by account and authority\n"); - try { - while (c.moveToNext()) { - if (!TextUtils.equals(currentAccount, c.getString(0))) { - if (currentAccount != null) { - dumpSyncHistoryFooter(sb); + HashSet<String> processedAccounts = new HashSet<String>(); + ArrayList<SyncStatusInfo> statuses + = mSyncStorageEngine.getSyncStatus(); + if (statuses != null && statuses.size() > 0) { + pw.println(); + pw.println("Sync Status"); + final int N = statuses.size(); + for (int i=0; i<N; i++) { + SyncStatusInfo status = statuses.get(i); + SyncStorageEngine.AuthorityInfo authority + = mSyncStorageEngine.getAuthority(status.authorityId); + if (authority != null) { + String curAccount = authority.account; + + if (processedAccounts.contains(curAccount)) { + continue; + } + + processedAccounts.add(curAccount); + + pw.print(" Account "); pw.print(authority.account); + pw.println(":"); + for (int j=i; j<N; j++) { + status = statuses.get(j); + authority = mSyncStorageEngine.getAuthority(status.authorityId); + if (!curAccount.equals(authority.account)) { + continue; + } + pw.print(" "); pw.print(authority.authority); + pw.println(":"); + pw.print(" count: local="); pw.print(status.numSourceLocal); + pw.print(" poll="); pw.print(status.numSourcePoll); + pw.print(" server="); pw.print(status.numSourceServer); + pw.print(" user="); pw.print(status.numSourceUser); + pw.print(" total="); pw.println(status.numSyncs); + pw.print(" total duration: "); + pw.println(DateUtils.formatElapsedTime( + status.totalElapsedTime/1000)); + if (status.lastSuccessTime != 0) { + pw.print(" SUCCESS: source="); + pw.print(SyncStorageEngine.SOURCES[ + status.lastSuccessSource]); + pw.print(" time="); + pw.println(formatTime(status.lastSuccessTime)); + } else { + pw.print(" FAILURE: source="); + pw.print(SyncStorageEngine.SOURCES[ + status.lastFailureSource]); + pw.print(" initialTime="); + pw.print(formatTime(status.initialFailureTime)); + pw.print(" lastTime="); + pw.println(formatTime(status.lastFailureTime)); + pw.print(" message: "); pw.println(status.lastFailureMesg); + } } - currentAccount = c.getString(0); - dumpSyncHistoryHeader(sb, currentAccount); } - - dumpSyncHistoryRow(sb, c); } - if (c.getCount() > 0) dumpSyncHistoryFooter(sb); - } finally { - c.close(); } } - private void dumpSyncHistoryHeader(StringBuilder sb, String account) { - sb.append(" Account: ").append(account).append("\n"); - sb.append(" ___________________________________________________________________________________________________________________________\n"); - sb.append(" | | num times synced | total | last success | |\n"); - sb.append(" | authority | local | poll | server | user | total | duration | source | time | result if failing |\n"); + private void dumpTimeSec(PrintWriter pw, long time) { + pw.print(time/1000); pw.print('.'); pw.print((time/100)%10); + pw.print('s'); } - - private static String[] STATUS_PROJECTION = new String[]{ - Sync.Status.ACCOUNT, // 0 - Sync.Status.AUTHORITY, // 1 - Sync.Status.NUM_SYNCS, // 2 - Sync.Status.TOTAL_ELAPSED_TIME, // 3 - Sync.Status.NUM_SOURCE_LOCAL, // 4 - Sync.Status.NUM_SOURCE_POLL, // 5 - Sync.Status.NUM_SOURCE_SERVER, // 6 - Sync.Status.NUM_SOURCE_USER, // 7 - Sync.Status.LAST_SUCCESS_SOURCE, // 8 - Sync.Status.LAST_SUCCESS_TIME, // 9 - Sync.Status.LAST_FAILURE_SOURCE, // 10 - Sync.Status.LAST_FAILURE_TIME, // 11 - Sync.Status.LAST_FAILURE_MESG // 12 - }; - - private void dumpSyncHistoryRow(StringBuilder sb, Cursor c) { - boolean hasSuccess = !c.isNull(9); - boolean hasFailure = !c.isNull(11); - Time timeSuccess = new Time(); - if (hasSuccess) timeSuccess.set(c.getLong(9)); - Time timeFailure = new Time(); - if (hasFailure) timeFailure.set(c.getLong(11)); - sb.append(String.format(" | %-15s | %5d | %5d | %6d | %5d | %5d | %8s | %7s | %19s | %19s |\n", - c.getString(1), - c.getLong(4), - c.getLong(5), - c.getLong(6), - c.getLong(7), - c.getLong(2), - DateUtils.formatElapsedTime(c.getLong(3)/1000), - hasSuccess ? Sync.History.SOURCES[c.getInt(8)] : "", - hasSuccess ? timeSuccess.format("%Y-%m-%d %H:%M:%S") : "", - hasFailure ? History.mesgToString(c.getString(12)) : "")); - } - - private void dumpSyncHistoryFooter(StringBuilder sb) { - sb.append(" |___________________________________________________________________________________________________________________________|\n"); + + private void dumpDayStatistic(PrintWriter pw, SyncStorageEngine.DayStats ds) { + pw.print("Success ("); pw.print(ds.successCount); + if (ds.successCount > 0) { + pw.print(" for "); dumpTimeSec(pw, ds.successTime); + pw.print(" avg="); dumpTimeSec(pw, ds.successTime/ds.successCount); + } + pw.print(") Failure ("); pw.print(ds.failureCount); + if (ds.failureCount > 0) { + pw.print(" for "); dumpTimeSec(pw, ds.failureTime); + pw.print(" avg="); dumpTimeSec(pw, ds.failureTime/ds.failureCount); + } + pw.println(")"); } - - private void dumpSyncPendingHeader(StringBuilder sb) { - sb.append(" ____________________________________________________\n"); - sb.append(" | account | authority |\n"); - } - - private void dumpSyncPendingRow(StringBuilder sb, Cursor c) { - sb.append(String.format(" | %-30s | %-15s |\n", c.getString(0), c.getString(1))); - } - - private void dumpSyncPendingFooter(StringBuilder sb) { - sb.append(" |__________________________________________________|\n"); - } - - protected void dumpSyncHistory(StringBuilder sb) { - Cursor c = mSyncStorageEngine.query(Sync.History.CONTENT_URI, null, "event=?", - new String[]{String.valueOf(Sync.History.EVENT_STOP)}, - Sync.HistoryColumns.EVENT_TIME + " desc"); - try { - long numSyncsLastHour = 0, durationLastHour = 0; - long numSyncsLastDay = 0, durationLastDay = 0; - long numSyncsLastWeek = 0, durationLastWeek = 0; - long numSyncsLast4Weeks = 0, durationLast4Weeks = 0; - long numSyncsTotal = 0, durationTotal = 0; - - long now = System.currentTimeMillis(); - int indexEventTime = c.getColumnIndexOrThrow(Sync.History.EVENT_TIME); - int indexElapsedTime = c.getColumnIndexOrThrow(Sync.History.ELAPSED_TIME); - while (c.moveToNext()) { - long duration = c.getLong(indexElapsedTime); - long endTime = c.getLong(indexEventTime) + duration; - long millisSinceStart = now - endTime; - numSyncsTotal++; - durationTotal += duration; - if (millisSinceStart < MILLIS_IN_HOUR) { - numSyncsLastHour++; - durationLastHour += duration; + + protected void dumpSyncHistory(PrintWriter pw, StringBuilder sb) { + SyncStorageEngine.DayStats dses[] = mSyncStorageEngine.getDayStatistics(); + if (dses != null && dses[0] != null) { + pw.println(); + pw.println("Sync Statistics"); + pw.print(" Today: "); dumpDayStatistic(pw, dses[0]); + int today = dses[0].day; + int i; + SyncStorageEngine.DayStats ds; + + // Print each day in the current week. + for (i=1; i<=6 && i < dses.length; i++) { + ds = dses[i]; + if (ds == null) break; + int delta = today-ds.day; + if (delta > 6) break; + + pw.print(" Day-"); pw.print(delta); pw.print(": "); + dumpDayStatistic(pw, ds); + } + + // Aggregate all following days into weeks and print totals. + int weekDay = today; + while (i < dses.length) { + SyncStorageEngine.DayStats aggr = null; + weekDay -= 7; + while (i < dses.length) { + ds = dses[i]; + if (ds == null) { + i = dses.length; + break; + } + int delta = weekDay-ds.day; + if (delta > 6) break; + i++; + + if (aggr == null) { + aggr = new SyncStorageEngine.DayStats(weekDay); + } + aggr.successCount += ds.successCount; + aggr.successTime += ds.successTime; + aggr.failureCount += ds.failureCount; + aggr.failureTime += ds.failureTime; } - if (millisSinceStart < MILLIS_IN_DAY) { - numSyncsLastDay++; - durationLastDay += duration; + if (aggr != null) { + pw.print(" Week-"); pw.print((today-weekDay)/7); pw.print(": "); + dumpDayStatistic(pw, aggr); } - if (millisSinceStart < MILLIS_IN_WEEK) { - numSyncsLastWeek++; - durationLastWeek += duration; + } + } + + ArrayList<SyncStorageEngine.SyncHistoryItem> items + = mSyncStorageEngine.getSyncHistory(); + if (items != null && items.size() > 0) { + pw.println(); + pw.println("Recent Sync History"); + final int N = items.size(); + for (int i=0; i<N; i++) { + SyncStorageEngine.SyncHistoryItem item = items.get(i); + SyncStorageEngine.AuthorityInfo authority + = mSyncStorageEngine.getAuthority(item.authorityId); + pw.print(" #"); pw.print(i+1); pw.print(": "); + pw.print(authority != null ? authority.account : "<no account>"); + pw.print(" "); + pw.print(authority != null ? authority.authority : "<no account>"); + Time time = new Time(); + time.set(item.eventTime); + pw.print(" "); pw.print(SyncStorageEngine.SOURCES[item.source]); + pw.print(" @ "); + pw.print(formatTime(item.eventTime)); + pw.print(" for "); + dumpTimeSec(pw, item.elapsedTime); + pw.println(); + if (item.event != SyncStorageEngine.EVENT_STOP + || item.upstreamActivity !=0 + || item.downstreamActivity != 0) { + pw.print(" event="); pw.print(item.event); + pw.print(" upstreamActivity="); pw.print(item.upstreamActivity); + pw.print(" downstreamActivity="); pw.println(item.downstreamActivity); } - if (millisSinceStart < MILLIS_IN_4WEEKS) { - numSyncsLast4Weeks++; - durationLast4Weeks += duration; + if (item.mesg != null + && !SyncStorageEngine.MESG_SUCCESS.equals(item.mesg)) { + pw.print(" mesg="); pw.println(item.mesg); } } - dumpSyncIntervalHeader(sb); - dumpSyncInterval(sb, "hour", MILLIS_IN_HOUR, numSyncsLastHour, durationLastHour); - dumpSyncInterval(sb, "day", MILLIS_IN_DAY, numSyncsLastDay, durationLastDay); - dumpSyncInterval(sb, "week", MILLIS_IN_WEEK, numSyncsLastWeek, durationLastWeek); - dumpSyncInterval(sb, "4 weeks", - MILLIS_IN_4WEEKS, numSyncsLast4Weeks, durationLast4Weeks); - dumpSyncInterval(sb, "total", 0, numSyncsTotal, durationTotal); - dumpSyncIntervalFooter(sb); - } finally { - c.close(); } } - private void dumpSyncIntervalHeader(StringBuilder sb) { - sb.append("Sync Stats\n"); - sb.append(" ___________________________________________________________\n"); - sb.append(" | | | duration in sec | |\n"); - sb.append(" | interval | count | average | total | % of interval |\n"); - } - - private void dumpSyncInterval(StringBuilder sb, String label, - long interval, long numSyncs, long duration) { - sb.append(String.format(" | %-8s | %6d | %8.1f | %8.1f", - label, numSyncs, ((float)duration/numSyncs)/1000, (float)duration/1000)); - if (interval > 0) { - sb.append(String.format(" | %13.2f |\n", ((float)duration/interval)*100.0)); - } else { - sb.append(String.format(" | %13s |\n", "na")); - } - } - - private void dumpSyncIntervalFooter(StringBuilder sb) { - sb.append(" |_________________________________________________________|\n"); - } - /** * A helper object to keep track of the time we have spent syncing since the last boot */ @@ -1461,7 +1457,6 @@ class SyncManager { // found that is runnable (not disabled, etc). If that one is ready to run then // start it, otherwise just get out. SyncOperation syncOperation; - final Sync.Settings.QueryMap syncSettings = getSyncSettings(); final ConnectivityManager connManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); final boolean backgroundDataSetting = connManager.getBackgroundDataSetting(); @@ -1488,9 +1483,9 @@ class SyncManager { final boolean force = syncOperation.extras.getBoolean( ContentResolver.SYNC_EXTRAS_FORCE, false); if (!force && (!backgroundDataSetting - || !syncSettings.getListenForNetworkTickles() - || !syncSettings.getSyncProviderAutomatically( - syncOperation.authority))) { + || !mSyncStorageEngine.getListenForNetworkTickles() + || !mSyncStorageEngine.getSyncProviderAutomatically( + null, syncOperation.authority))) { if (isLoggable) { Log.v(TAG, "runStateIdle: sync off, dropping " + syncOperation); } @@ -1616,7 +1611,7 @@ class SyncManager { if (isLoggable) { Log.v(TAG, "finished sync operation " + syncOperation); } - historyMessage = History.MESG_SUCCESS; + historyMessage = SyncStorageEngine.MESG_SUCCESS; // TODO: set these correctly when the SyncResult is extended to include it downstreamActivity = 0; upstreamActivity = 0; @@ -1640,7 +1635,7 @@ class SyncManager { } catch (RemoteException e) { // we don't need to retry this in this case } - historyMessage = History.MESG_CANCELED; + historyMessage = SyncStorageEngine.MESG_CANCELED; downstreamActivity = 0; upstreamActivity = 0; } @@ -1675,14 +1670,22 @@ class SyncManager { * If SyncResult.error() is true then it is safe to call this. */ private int syncResultToErrorNumber(SyncResult syncResult) { - if (syncResult.syncAlreadyInProgress) return History.ERROR_SYNC_ALREADY_IN_PROGRESS; - if (syncResult.stats.numAuthExceptions > 0) return History.ERROR_AUTHENTICATION; - if (syncResult.stats.numIoExceptions > 0) return History.ERROR_IO; - if (syncResult.stats.numParseExceptions > 0) return History.ERROR_PARSE; - if (syncResult.stats.numConflictDetectedExceptions > 0) return History.ERROR_CONFLICT; - if (syncResult.tooManyDeletions) return History.ERROR_TOO_MANY_DELETIONS; - if (syncResult.tooManyRetries) return History.ERROR_TOO_MANY_RETRIES; - if (syncResult.databaseError) return History.ERROR_INTERNAL; + if (syncResult.syncAlreadyInProgress) + return SyncStorageEngine.ERROR_SYNC_ALREADY_IN_PROGRESS; + if (syncResult.stats.numAuthExceptions > 0) + return SyncStorageEngine.ERROR_AUTHENTICATION; + if (syncResult.stats.numIoExceptions > 0) + return SyncStorageEngine.ERROR_IO; + if (syncResult.stats.numParseExceptions > 0) + return SyncStorageEngine.ERROR_PARSE; + if (syncResult.stats.numConflictDetectedExceptions > 0) + return SyncStorageEngine.ERROR_CONFLICT; + if (syncResult.tooManyDeletions) + return SyncStorageEngine.ERROR_TOO_MANY_DELETIONS; + if (syncResult.tooManyRetries) + return SyncStorageEngine.ERROR_TOO_MANY_RETRIES; + if (syncResult.databaseError) + return SyncStorageEngine.ERROR_INTERNAL; throw new IllegalStateException("we are not in an error state, " + syncResult); } @@ -1904,7 +1907,8 @@ class SyncManager { final int source = syncOperation.syncSource; final long now = System.currentTimeMillis(); - EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_START, source); + EventLog.writeEvent(2720, syncOperation.authority, + SyncStorageEngine.EVENT_START, source); return mSyncStorageEngine.insertStartSyncEvent( syncOperation.account, syncOperation.authority, now, source); @@ -1912,7 +1916,8 @@ class SyncManager { public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage, int upstreamActivity, int downstreamActivity, long elapsedTime) { - EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_STOP, syncOperation.syncSource); + EventLog.writeEvent(2720, syncOperation.authority, + SyncStorageEngine.EVENT_STOP, syncOperation.syncSource); mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage, downstreamActivity, upstreamActivity); @@ -1921,18 +1926,6 @@ class SyncManager { static class SyncQueue { private SyncStorageEngine mSyncStorageEngine; - private final String[] COLUMNS = new String[]{ - "_id", - "authority", - "account", - "extras", - "source" - }; - private static final int COLUMN_ID = 0; - private static final int COLUMN_AUTHORITY = 1; - private static final int COLUMN_ACCOUNT = 2; - private static final int COLUMN_EXTRAS = 3; - private static final int COLUMN_SOURCE = 4; private static final boolean DEBUG_CHECK_DATA_CONSISTENCY = false; @@ -1946,25 +1939,28 @@ class SyncManager { public SyncQueue(SyncStorageEngine syncStorageEngine) { mSyncStorageEngine = syncStorageEngine; - Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS); - try { - while (cursor.moveToNext()) { - add(cursorToOperation(cursor), - true /* this is being added from the database */); - } - } finally { - cursor.close(); - if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); - } + ArrayList<SyncStorageEngine.PendingOperation> ops + = mSyncStorageEngine.getPendingOperations(); + final int N = ops.size(); + for (int i=0; i<N; i++) { + SyncStorageEngine.PendingOperation op = ops.get(i); + SyncOperation syncOperation = new SyncOperation( + op.account, op.syncSource, op.authority, op.extras, 0); + syncOperation.pendingOperation = op; + add(syncOperation, op); + } + + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); } public boolean add(SyncOperation operation) { return add(new SyncOperation(operation), - false /* this is not coming from the database */); + null /* this is not coming from the database */); } - private boolean add(SyncOperation operation, boolean fromDatabase) { - if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase); + private boolean add(SyncOperation operation, + SyncStorageEngine.PendingOperation pop) { + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null); // If this operation is expedited then set its earliestRunTime to be immediately // before the head of the list, or not if none are in the list. @@ -1996,7 +1992,7 @@ class SyncManager { if (existingOperation != null && operation.earliestRunTime >= existingOperation.earliestRunTime) { - if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase); + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null); return false; } @@ -2004,26 +2000,17 @@ class SyncManager { removeByKey(operationKey); } - if (operation.rowId == null) { - byte[] extrasData = null; - Parcel parcel = Parcel.obtain(); - try { - operation.extras.writeToParcel(parcel, 0); - extrasData = parcel.marshall(); - } finally { - parcel.recycle(); - } - ContentValues values = new ContentValues(); - values.put("account", operation.account); - values.put("authority", operation.authority); - values.put("source", operation.syncSource); - values.put("extras", extrasData); - Uri pendingUri = mSyncStorageEngine.insertIntoPending(values); - operation.rowId = pendingUri == null ? null : ContentUris.parseId(pendingUri); - if (operation.rowId == null) { + operation.pendingOperation = pop; + if (operation.pendingOperation == null) { + pop = new SyncStorageEngine.PendingOperation( + operation.account, operation.syncSource, + operation.authority, operation.extras); + pop = mSyncStorageEngine.insertIntoPending(pop); + if (pop == null) { throw new IllegalStateException("error adding pending sync operation " + operation); } + operation.pendingOperation = pop; } if (DEBUG_CHECK_DATA_CONSISTENCY) { @@ -2033,7 +2020,7 @@ class SyncManager { } mOpsByKey.put(operationKey, operation); mOpsByWhen.add(operation); - if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase); + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null); return true; } @@ -2045,7 +2032,7 @@ class SyncManager { "unable to find " + operationToRemove + " in mOpsByWhen"); } - if (mSyncStorageEngine.deleteFromPending(operationToRemove.rowId) != 1) { + if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) { throw new IllegalStateException("unable to find pending row for " + operationToRemove); } @@ -2065,7 +2052,7 @@ class SyncManager { throw new IllegalStateException("unable to find " + operation + " in mOpsByKey"); } - if (mSyncStorageEngine.deleteFromPending(operation.rowId) != 1) { + if (!mSyncStorageEngine.deleteFromPending(operation.pendingOperation)) { throw new IllegalStateException("unable to find pending row for " + operation); } @@ -2087,7 +2074,7 @@ class SyncManager { "unable to find " + syncOperation + " in mOpsByWhen"); } - if (mSyncStorageEngine.deleteFromPending(syncOperation.rowId) != 1) { + if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) { throw new IllegalStateException("unable to find pending row for " + syncOperation); } @@ -2128,48 +2115,29 @@ class SyncManager { } if (checkDatabase) { - // check that the DB contains the same rows as the in-memory data structures - Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS); - try { - if (mOpsByKey.size() != cursor.getCount()) { - StringBuilder sb = new StringBuilder(); - DatabaseUtils.dumpCursor(cursor, sb); + final int N = mSyncStorageEngine.getPendingOperationCount(); + if (mOpsByKey.size() != N) { + ArrayList<SyncStorageEngine.PendingOperation> ops + = mSyncStorageEngine.getPendingOperations(); + StringBuilder sb = new StringBuilder(); + for (int i=0; i<N; i++) { + SyncStorageEngine.PendingOperation op = ops.get(i); + sb.append("#"); + sb.append(i); + sb.append(": account="); + sb.append(op.account); + sb.append(" syncSource="); + sb.append(op.syncSource); + sb.append(" authority="); + sb.append(op.authority); sb.append("\n"); - dump(sb); - throw new IllegalStateException("DB size mismatch: " - + mOpsByKey .size() + " != " + cursor.getCount() + "\n" - + sb.toString()); } - } finally { - cursor.close(); + dump(sb); + throw new IllegalStateException("DB size mismatch: " + + mOpsByKey.size() + " != " + N + "\n" + + sb.toString()); } } } - - private SyncOperation cursorToOperation(Cursor cursor) { - byte[] extrasData = cursor.getBlob(COLUMN_EXTRAS); - Bundle extras; - Parcel parcel = Parcel.obtain(); - try { - parcel.unmarshall(extrasData, 0, extrasData.length); - parcel.setDataPosition(0); - extras = parcel.readBundle(); - } catch (RuntimeException e) { - // A RuntimeException is thrown if we were unable to parse the parcel. - // Create an empty parcel in this case. - extras = new Bundle(); - } finally { - parcel.recycle(); - } - - SyncOperation syncOperation = new SyncOperation( - cursor.getString(COLUMN_ACCOUNT), - cursor.getInt(COLUMN_SOURCE), - cursor.getString(COLUMN_AUTHORITY), - extras, - 0 /* delay */); - syncOperation.rowId = cursor.getLong(COLUMN_ID); - return syncOperation; - } } } diff --git a/core/java/android/content/SyncProvider.java b/core/java/android/content/SyncProvider.java deleted file mode 100644 index 6ddd046..0000000 --- a/core/java/android/content/SyncProvider.java +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2007 The Android Open Source Project -package android.content; - -import android.database.Cursor; -import android.net.Uri; - -/** - * ContentProvider that tracks the sync data and overall sync - * history on the device. - * - * @hide - */ -public class SyncProvider extends ContentProvider { - public SyncProvider() { - } - - private SyncStorageEngine mSyncStorageEngine; - - @Override - public boolean onCreate() { - mSyncStorageEngine = SyncStorageEngine.getSingleton(); - return true; - } - - @Override - public Cursor query(Uri url, String[] projectionIn, - String selection, String[] selectionArgs, String sort) { - return mSyncStorageEngine.query(url, projectionIn, selection, selectionArgs, sort); - } - - @Override - public Uri insert(Uri url, ContentValues initialValues) { - return mSyncStorageEngine.insert(true /* the caller is the provider */, - url, initialValues); - } - - @Override - public int delete(Uri url, String where, String[] whereArgs) { - return mSyncStorageEngine.delete(true /* the caller is the provider */, - url, where, whereArgs); - } - - @Override - public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) { - return mSyncStorageEngine.update(true /* the caller is the provider */, - url, initialValues, where, whereArgs); - } - - @Override - public String getType(Uri url) { - return mSyncStorageEngine.getType(url); - } -} diff --git a/core/java/android/content/SyncStatusInfo.aidl b/core/java/android/content/SyncStatusInfo.aidl new file mode 100644 index 0000000..b188c0b --- /dev/null +++ b/core/java/android/content/SyncStatusInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 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; + +parcelable SyncStatusInfo; diff --git a/core/java/android/content/SyncStatusInfo.java b/core/java/android/content/SyncStatusInfo.java new file mode 100644 index 0000000..6687fcb --- /dev/null +++ b/core/java/android/content/SyncStatusInfo.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2009 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; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** @hide */ +public class SyncStatusInfo implements Parcelable { + static final int VERSION = 1; + + public final int authorityId; + public long totalElapsedTime; + public int numSyncs; + public int numSourcePoll; + public int numSourceServer; + public int numSourceLocal; + public int numSourceUser; + public long lastSuccessTime; + public int lastSuccessSource; + public long lastFailureTime; + public int lastFailureSource; + public String lastFailureMesg; + public long initialFailureTime; + public boolean pending; + + SyncStatusInfo(int authorityId) { + this.authorityId = authorityId; + } + + public int getLastFailureMesgAsInt(int def) { + try { + if (lastFailureMesg != null) { + return Integer.parseInt(lastFailureMesg); + } + } catch (NumberFormatException e) { + } + return def; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(VERSION); + parcel.writeInt(authorityId); + parcel.writeLong(totalElapsedTime); + parcel.writeInt(numSyncs); + parcel.writeInt(numSourcePoll); + parcel.writeInt(numSourceServer); + parcel.writeInt(numSourceLocal); + parcel.writeInt(numSourceUser); + parcel.writeLong(lastSuccessTime); + parcel.writeInt(lastSuccessSource); + parcel.writeLong(lastFailureTime); + parcel.writeInt(lastFailureSource); + parcel.writeString(lastFailureMesg); + parcel.writeLong(initialFailureTime); + parcel.writeInt(pending ? 1 : 0); + } + + SyncStatusInfo(Parcel parcel) { + int version = parcel.readInt(); + if (version != VERSION) { + Log.w("SyncStatusInfo", "Unknown version: " + version); + } + authorityId = parcel.readInt(); + totalElapsedTime = parcel.readLong(); + numSyncs = parcel.readInt(); + numSourcePoll = parcel.readInt(); + numSourceServer = parcel.readInt(); + numSourceLocal = parcel.readInt(); + numSourceUser = parcel.readInt(); + lastSuccessTime = parcel.readLong(); + lastSuccessSource = parcel.readInt(); + lastFailureTime = parcel.readLong(); + lastFailureSource = parcel.readInt(); + lastFailureMesg = parcel.readString(); + initialFailureTime = parcel.readLong(); + pending = parcel.readInt() != 0; + } + + public static final Creator<SyncStatusInfo> CREATOR = new Creator<SyncStatusInfo>() { + public SyncStatusInfo createFromParcel(Parcel in) { + return new SyncStatusInfo(in); + } + + public SyncStatusInfo[] newArray(int size) { + return new SyncStatusInfo[size]; + } + }; +}
\ No newline at end of file diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index 282f6e7..1587462 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -1,125 +1,282 @@ +/* + * Copyright (C) 2009 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; -import android.Manifest; +import com.android.internal.os.AtomicFile; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.FastXmlSerializer; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + import android.database.Cursor; -import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.provider.Sync; -import android.text.TextUtils; -import android.util.Config; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.os.Parcel; +import android.os.RemoteCallbackList; +import android.os.RemoteException; import android.util.Log; +import android.util.SparseArray; +import android.util.Xml; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.util.ArrayList; +import java.util.Calendar; import java.util.HashMap; -import java.util.HashSet; +import java.util.Iterator; +import java.util.TimeZone; /** - * ContentProvider that tracks the sync data and overall sync + * Singleton that tracks the sync data and overall sync * history on the device. * * @hide */ -public class SyncStorageEngine { +public class SyncStorageEngine extends Handler { private static final String TAG = "SyncManager"; + private static final boolean DEBUG = false; + private static final boolean DEBUG_FILE = false; + + // @VisibleForTesting + static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; - private static final String DATABASE_NAME = "syncmanager.db"; - private static final int DATABASE_VERSION = 10; + /** Enum value for a sync start event. */ + public static final int EVENT_START = 0; - private static final int STATS = 1; - private static final int STATS_ID = 2; - private static final int HISTORY = 3; - private static final int HISTORY_ID = 4; - private static final int SETTINGS = 5; - private static final int PENDING = 7; - private static final int ACTIVE = 8; - private static final int STATUS = 9; + /** Enum value for a sync stop event. */ + public static final int EVENT_STOP = 1; - private static final UriMatcher sURLMatcher = - new UriMatcher(UriMatcher.NO_MATCH); + // TODO: i18n -- grab these out of resources. + /** String names for the sync event types. */ + public static final String[] EVENTS = { "START", "STOP" }; - private static final HashMap<String,String> HISTORY_PROJECTION_MAP; - private static final HashMap<String,String> PENDING_PROJECTION_MAP; - private static final HashMap<String,String> ACTIVE_PROJECTION_MAP; - private static final HashMap<String,String> STATUS_PROJECTION_MAP; + /** Enum value for a server-initiated sync. */ + public static final int SOURCE_SERVER = 0; - private final Context mContext; - private final SQLiteOpenHelper mOpenHelper; - private static SyncStorageEngine sSyncStorageEngine = null; - - static { - sURLMatcher.addURI("sync", "stats", STATS); - sURLMatcher.addURI("sync", "stats/#", STATS_ID); - sURLMatcher.addURI("sync", "history", HISTORY); - sURLMatcher.addURI("sync", "history/#", HISTORY_ID); - sURLMatcher.addURI("sync", "settings", SETTINGS); - sURLMatcher.addURI("sync", "status", STATUS); - sURLMatcher.addURI("sync", "active", ACTIVE); - sURLMatcher.addURI("sync", "pending", PENDING); - - HashMap<String,String> map; - PENDING_PROJECTION_MAP = map = new HashMap<String,String>(); - map.put(Sync.History._ID, Sync.History._ID); - map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT); - map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY); - - ACTIVE_PROJECTION_MAP = map = new HashMap<String,String>(); - map.put(Sync.History._ID, Sync.History._ID); - map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT); - map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY); - map.put("startTime", "startTime"); - - HISTORY_PROJECTION_MAP = map = new HashMap<String,String>(); - map.put(Sync.History._ID, "history._id as _id"); - map.put(Sync.History.ACCOUNT, "stats.account as account"); - map.put(Sync.History.AUTHORITY, "stats.authority as authority"); - map.put(Sync.History.EVENT, Sync.History.EVENT); - map.put(Sync.History.EVENT_TIME, Sync.History.EVENT_TIME); - map.put(Sync.History.ELAPSED_TIME, Sync.History.ELAPSED_TIME); - map.put(Sync.History.SOURCE, Sync.History.SOURCE); - map.put(Sync.History.UPSTREAM_ACTIVITY, Sync.History.UPSTREAM_ACTIVITY); - map.put(Sync.History.DOWNSTREAM_ACTIVITY, Sync.History.DOWNSTREAM_ACTIVITY); - map.put(Sync.History.MESG, Sync.History.MESG); - - STATUS_PROJECTION_MAP = map = new HashMap<String,String>(); - map.put(Sync.Status._ID, "status._id as _id"); - map.put(Sync.Status.ACCOUNT, "stats.account as account"); - map.put(Sync.Status.AUTHORITY, "stats.authority as authority"); - map.put(Sync.Status.TOTAL_ELAPSED_TIME, Sync.Status.TOTAL_ELAPSED_TIME); - map.put(Sync.Status.NUM_SYNCS, Sync.Status.NUM_SYNCS); - map.put(Sync.Status.NUM_SOURCE_LOCAL, Sync.Status.NUM_SOURCE_LOCAL); - map.put(Sync.Status.NUM_SOURCE_POLL, Sync.Status.NUM_SOURCE_POLL); - map.put(Sync.Status.NUM_SOURCE_SERVER, Sync.Status.NUM_SOURCE_SERVER); - map.put(Sync.Status.NUM_SOURCE_USER, Sync.Status.NUM_SOURCE_USER); - map.put(Sync.Status.LAST_SUCCESS_SOURCE, Sync.Status.LAST_SUCCESS_SOURCE); - map.put(Sync.Status.LAST_SUCCESS_TIME, Sync.Status.LAST_SUCCESS_TIME); - map.put(Sync.Status.LAST_FAILURE_SOURCE, Sync.Status.LAST_FAILURE_SOURCE); - map.put(Sync.Status.LAST_FAILURE_TIME, Sync.Status.LAST_FAILURE_TIME); - map.put(Sync.Status.LAST_FAILURE_MESG, Sync.Status.LAST_FAILURE_MESG); - map.put(Sync.Status.PENDING, Sync.Status.PENDING); - } - - private static final String[] STATS_ACCOUNT_PROJECTION = - new String[] { Sync.Stats.ACCOUNT }; - - private static final int MAX_HISTORY_EVENTS_TO_KEEP = 5000; - - private static final String SELECT_INITIAL_FAILURE_TIME_QUERY_STRING = "" - + "SELECT min(a) " - + "FROM (" - + " SELECT initialFailureTime AS a " - + " FROM status " - + " WHERE stats_id=? AND a IS NOT NULL " - + " UNION " - + " SELECT ? AS a" - + " )"; + /** Enum value for a local-initiated sync. */ + public static final int SOURCE_LOCAL = 1; + /** + * Enum value for a poll-based sync (e.g., upon connection to + * network) + */ + public static final int SOURCE_POLL = 2; + + /** Enum value for a user-initiated sync. */ + public static final int SOURCE_USER = 3; + + // TODO: i18n -- grab these out of resources. + /** String names for the sync source types. */ + public static final String[] SOURCES = { "SERVER", + "LOCAL", + "POLL", + "USER" }; + + // Error types + public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1; + public static final int ERROR_AUTHENTICATION = 2; + public static final int ERROR_IO = 3; + public static final int ERROR_PARSE = 4; + public static final int ERROR_CONFLICT = 5; + public static final int ERROR_TOO_MANY_DELETIONS = 6; + public static final int ERROR_TOO_MANY_RETRIES = 7; + public static final int ERROR_INTERNAL = 8; + + // The MESG column will contain one of these or one of the Error types. + public static final String MESG_SUCCESS = "success"; + public static final String MESG_CANCELED = "canceled"; + + public static final int CHANGE_SETTINGS = 1<<0; + public static final int CHANGE_PENDING = 1<<1; + public static final int CHANGE_ACTIVE = 1<<2; + public static final int CHANGE_STATUS = 1<<3; + public static final int CHANGE_ALL = 0x7fffffff; + + public static final int MAX_HISTORY = 15; + + private static final int MSG_WRITE_STATUS = 1; + private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes + + private static final int MSG_WRITE_STATISTICS = 2; + private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour + + public static class PendingOperation { + final String account; + final int syncSource; + final String authority; + final Bundle extras; // note: read-only. + + int authorityId; + byte[] flatExtras; + + PendingOperation(String account, int source, + String authority, Bundle extras) { + this.account = account; + this.syncSource = source; + this.authority = authority; + this.extras = extras != null ? new Bundle(extras) : extras; + this.authorityId = -1; + } + PendingOperation(PendingOperation other) { + this.account = other.account; + this.syncSource = other.syncSource; + this.authority = other.authority; + this.extras = other.extras; + this.authorityId = other.authorityId; + } + } + + static class AccountInfo { + final String account; + final HashMap<String, AuthorityInfo> authorities = + new HashMap<String, AuthorityInfo>(); + + AccountInfo(String account) { + this.account = account; + } + } + + public static class AuthorityInfo { + final String account; + final String authority; + final int ident; + boolean enabled; + + AuthorityInfo(String account, String authority, int ident) { + this.account = account; + this.authority = authority; + this.ident = ident; + enabled = true; + } + } + + public static class SyncHistoryItem { + int authorityId; + int historyId; + long eventTime; + long elapsedTime; + int source; + int event; + long upstreamActivity; + long downstreamActivity; + String mesg; + } + + public static class DayStats { + public final int day; + public int successCount; + public long successTime; + public int failureCount; + public long failureTime; + + public DayStats(int day) { + this.day = day; + } + } + + // Primary list of all syncable authorities. Also our global lock. + private final SparseArray<AuthorityInfo> mAuthorities = + new SparseArray<AuthorityInfo>(); + + private final HashMap<String, AccountInfo> mAccounts = + new HashMap<String, AccountInfo>(); + + private final ArrayList<PendingOperation> mPendingOperations = + new ArrayList<PendingOperation>(); + + private ActiveSyncInfo mActiveSync; + + private final SparseArray<SyncStatusInfo> mSyncStatus = + new SparseArray<SyncStatusInfo>(); + + private final ArrayList<SyncHistoryItem> mSyncHistory = + new ArrayList<SyncHistoryItem>(); + + private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners + = new RemoteCallbackList<ISyncStatusObserver>(); + + // We keep 4 weeks of stats. + private final DayStats[] mDayStats = new DayStats[7*4]; + private final Calendar mCal; + private int mYear; + private int mYearInDays; + + private final Context mContext; + private static volatile SyncStorageEngine sSyncStorageEngine = null; + + /** + * This file contains the core engine state: all accounts and the + * settings for them. It must never be lost, and should be changed + * infrequently, so it is stored as an XML file. + */ + private final AtomicFile mAccountInfoFile; + + /** + * This file contains the current sync status. We would like to retain + * it across boots, but its loss is not the end of the world, so we store + * this information as binary data. + */ + private final AtomicFile mStatusFile; + + /** + * This file contains sync statistics. This is purely debugging information + * so is written infrequently and can be thrown away at any time. + */ + private final AtomicFile mStatisticsFile; + + /** + * This file contains the pending sync operations. It is a binary file, + * which must be updated every time an operation is added or removed, + * so we have special handling of it. + */ + private final AtomicFile mPendingFile; + private static final int PENDING_FINISH_TO_WRITE = 4; + private int mNumPendingFinished = 0; + + private int mNextHistoryId = 0; + private boolean mListenForTickles = true; + private SyncStorageEngine(Context context) { mContext = context; - mOpenHelper = new SyncStorageEngine.DatabaseHelper(context); sSyncStorageEngine = this; + + mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); + + File dataDir = Environment.getDataDirectory(); + File systemDir = new File(dataDir, "system"); + File syncDir = new File(systemDir, "sync"); + mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml")); + mStatusFile = new AtomicFile(new File(syncDir, "status.bin")); + mPendingFile = new AtomicFile(new File(syncDir, "pending.bin")); + mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin")); + + readAccountInfoLocked(); + readStatusLocked(); + readPendingOperationsLocked(); + readStatisticsLocked(); + readLegacyAccountInfoLocked(); } public static SyncStorageEngine newTestInstance(Context context) { @@ -140,619 +297,1249 @@ public class SyncStorageEngine { return sSyncStorageEngine; } - private class DatabaseHelper extends SQLiteOpenHelper { - DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE pending (" - + "_id INTEGER PRIMARY KEY," - + "authority TEXT NOT NULL," - + "account TEXT NOT NULL," - + "extras BLOB NOT NULL," - + "source INTEGER NOT NULL" - + ");"); - - db.execSQL("CREATE TABLE stats (" + - "_id INTEGER PRIMARY KEY," + - "account TEXT, " + - "authority TEXT, " + - "syncdata TEXT, " + - "UNIQUE (account, authority)" + - ");"); - - db.execSQL("CREATE TABLE history (" + - "_id INTEGER PRIMARY KEY," + - "stats_id INTEGER," + - "eventTime INTEGER," + - "elapsedTime INTEGER," + - "source INTEGER," + - "event INTEGER," + - "upstreamActivity INTEGER," + - "downstreamActivity INTEGER," + - "mesg TEXT);"); - - db.execSQL("CREATE TABLE status (" - + "_id INTEGER PRIMARY KEY," - + "stats_id INTEGER NOT NULL," - + "totalElapsedTime INTEGER NOT NULL DEFAULT 0," - + "numSyncs INTEGER NOT NULL DEFAULT 0," - + "numSourcePoll INTEGER NOT NULL DEFAULT 0," - + "numSourceServer INTEGER NOT NULL DEFAULT 0," - + "numSourceLocal INTEGER NOT NULL DEFAULT 0," - + "numSourceUser INTEGER NOT NULL DEFAULT 0," - + "lastSuccessTime INTEGER," - + "lastSuccessSource INTEGER," - + "lastFailureTime INTEGER," - + "lastFailureSource INTEGER," - + "lastFailureMesg STRING," - + "initialFailureTime INTEGER," - + "pending INTEGER NOT NULL DEFAULT 0);"); - - db.execSQL("CREATE TABLE active (" - + "_id INTEGER PRIMARY KEY," - + "authority TEXT," - + "account TEXT," - + "startTime INTEGER);"); - - db.execSQL("CREATE INDEX historyEventTime ON history (eventTime)"); - - db.execSQL("CREATE TABLE settings (" + - "name TEXT PRIMARY KEY," + - "value TEXT);"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion == 9 && newVersion == 10) { - Log.w(TAG, "Upgrading database from version " + oldVersion + " to " - + newVersion + ", which will preserve old data"); - db.execSQL("ALTER TABLE status ADD COLUMN initialFailureTime INTEGER"); - return; + @Override public void handleMessage(Message msg) { + if (msg.what == MSG_WRITE_STATUS) { + synchronized (mAccounts) { + writeStatusLocked(); + } + } else if (msg.what == MSG_WRITE_STATISTICS) { + synchronized (mAccounts) { + writeStatisticsLocked(); } - - Log.w(TAG, "Upgrading database from version " + oldVersion + " to " - + newVersion + ", which will destroy all old data"); - db.execSQL("DROP TABLE IF EXISTS pending"); - db.execSQL("DROP TABLE IF EXISTS stats"); - db.execSQL("DROP TABLE IF EXISTS history"); - db.execSQL("DROP TABLE IF EXISTS settings"); - db.execSQL("DROP TABLE IF EXISTS active"); - db.execSQL("DROP TABLE IF EXISTS status"); - onCreate(db); } - - @Override - public void onOpen(SQLiteDatabase db) { - if (!db.isReadOnly()) { - db.delete("active", null, null); - db.insert("active", "account", null); + } + + public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { + synchronized (mAuthorities) { + mChangeListeners.register(callback, mask); + } + } + + public void removeStatusChangeListener(ISyncStatusObserver callback) { + synchronized (mAuthorities) { + mChangeListeners.unregister(callback); + } + } + + private void reportChange(int which) { + ArrayList<ISyncStatusObserver> reports = null; + synchronized (mAuthorities) { + int i = mChangeListeners.beginBroadcast(); + while (i > 0) { + i--; + Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i); + if ((which & mask.intValue()) == 0) { + continue; + } + if (reports == null) { + reports = new ArrayList<ISyncStatusObserver>(i); + } + reports.add(mChangeListeners.getBroadcastItem(i)); + } + } + + if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports); + + if (reports != null) { + int i = reports.size(); + while (i > 0) { + i--; + try { + reports.get(i).onStatusChanged(which); + } catch (RemoteException e) { + // The remote callback list will take care of this for us. + } + } + } + } + + public boolean getSyncProviderAutomatically(String account, String providerName) { + synchronized (mAuthorities) { + if (account != null) { + AuthorityInfo authority = getAuthorityLocked(account, providerName, + "getSyncProviderAutomatically"); + return authority != null ? authority.enabled : false; + } + + int i = mAuthorities.size(); + while (i > 0) { + i--; + AuthorityInfo authority = mAuthorities.get(i); + if (authority.authority.equals(providerName) + && authority.enabled) { + return true; + } } + return false; } } - protected void doDatabaseCleanup(String[] accounts) { - HashSet<String> currentAccounts = new HashSet<String>(); - for (String account : accounts) currentAccounts.add(account); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - Cursor cursor = db.query("stats", STATS_ACCOUNT_PROJECTION, - null /* where */, null /* where args */, Sync.Stats.ACCOUNT, - null /* having */, null /* order by */); - try { - while (cursor.moveToNext()) { - String account = cursor.getString(0); - if (TextUtils.isEmpty(account)) { - continue; + public void setSyncProviderAutomatically(String account, String providerName, boolean sync) { + synchronized (mAuthorities) { + if (account != null) { + AuthorityInfo authority = getAuthorityLocked(account, providerName, + "setSyncProviderAutomatically"); + if (authority != null) { + authority.enabled = sync; } - if (!currentAccounts.contains(account)) { - String where = Sync.Stats.ACCOUNT + "=?"; - int numDeleted; - numDeleted = db.delete("stats", where, new String[]{account}); - if (Config.LOGD) { - Log.d(TAG, "deleted " + numDeleted - + " records from stats table" - + " for account " + account); + } else { + int i = mAuthorities.size(); + while (i > 0) { + i--; + AuthorityInfo authority = mAuthorities.get(i); + if (authority.account.equals(account) + && authority.authority.equals(providerName)) { + authority.enabled = sync; } } } - } finally { - cursor.close(); + writeAccountInfoLocked(); } + + reportChange(CHANGE_SETTINGS); } - protected void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) { - if (activeSyncContext != null) { - updateActiveSync(activeSyncContext.mSyncOperation.account, - activeSyncContext.mSyncOperation.authority, activeSyncContext.mStartTime); - } else { - // we indicate that the sync is not active by passing null for all the parameters - updateActiveSync(null, null, null); + public void setListenForNetworkTickles(boolean flag) { + synchronized (mAuthorities) { + mListenForTickles = flag; + writeAccountInfoLocked(); } + reportChange(CHANGE_SETTINGS); } - private int updateActiveSync(String account, String authority, Long startTime) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put("account", account); - values.put("authority", authority); - values.put("startTime", startTime); - int numChanges = db.update("active", values, null, null); - if (numChanges > 0) { - mContext.getContentResolver().notifyChange(Sync.Active.CONTENT_URI, - null /* this change wasn't made through an observer */); + public boolean getListenForNetworkTickles() { + synchronized (mAuthorities) { + return mListenForTickles; } - return numChanges; } - - /** - * Implements the {@link ContentProvider#query} method - */ - public Cursor query(Uri url, String[] projectionIn, - String selection, String[] selectionArgs, String sort) { - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - - // Generate the body of the query - int match = sURLMatcher.match(url); - String groupBy = null; - switch (match) { - case STATS: - qb.setTables("stats"); - break; - case STATS_ID: - qb.setTables("stats"); - qb.appendWhere("_id="); - qb.appendWhere(url.getPathSegments().get(1)); - break; - case HISTORY: - // join the stats and history tables, so the caller can get - // the account and authority information as part of this query. - qb.setTables("stats, history"); - qb.setProjectionMap(HISTORY_PROJECTION_MAP); - qb.appendWhere("stats._id = history.stats_id"); - break; - case ACTIVE: - qb.setTables("active"); - qb.setProjectionMap(ACTIVE_PROJECTION_MAP); - qb.appendWhere("account is not null"); - break; - case PENDING: - qb.setTables("pending"); - qb.setProjectionMap(PENDING_PROJECTION_MAP); - groupBy = "account, authority"; - break; - case STATUS: - // join the stats and status tables, so the caller can get - // the account and authority information as part of this query. - qb.setTables("stats, status"); - qb.setProjectionMap(STATUS_PROJECTION_MAP); - qb.appendWhere("stats._id = status.stats_id"); - break; - case HISTORY_ID: - // join the stats and history tables, so the caller can get - // the account and authority information as part of this query. - qb.setTables("stats, history"); - qb.setProjectionMap(HISTORY_PROJECTION_MAP); - qb.appendWhere("stats._id = history.stats_id"); - qb.appendWhere("AND history._id="); - qb.appendWhere(url.getPathSegments().get(1)); - break; - case SETTINGS: - qb.setTables("settings"); - break; - default: - throw new IllegalArgumentException("Unknown URL " + url); - } - - if (match == SETTINGS) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, - "no permission to read the sync settings"); - } else { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, - "no permission to read the sync stats"); - } - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - Cursor c = qb.query(db, projectionIn, selection, selectionArgs, groupBy, null, sort); - c.setNotificationUri(mContext.getContentResolver(), url); - return c; + + public AuthorityInfo getAuthority(String account, String authority) { + synchronized (mAuthorities) { + return getAuthorityLocked(account, authority, null); + } } - + + public AuthorityInfo getAuthority(int authorityId) { + synchronized (mAuthorities) { + return mAuthorities.get(authorityId); + } + } + /** - * Implements the {@link ContentProvider#insert} method - * @param callerIsTheProvider true if this is being called via the - * {@link ContentProvider#insert} in method rather than directly. - * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't - * for the Settings table. + * Returns true if there is currently a sync operation for the given + * account or authority in the pending list, or actively being processed. */ - public Uri insert(boolean callerIsTheProvider, Uri url, ContentValues values) { - String table; - long rowID; - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - final int match = sURLMatcher.match(url); - checkCaller(callerIsTheProvider, match); - switch (match) { - case SETTINGS: - mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, - "no permission to write the sync settings"); - table = "settings"; - rowID = db.replace(table, null, values); - break; - default: - throw new IllegalArgumentException("Unknown URL " + url); + public boolean isSyncActive(String account, String authority) { + synchronized (mAuthorities) { + int i = mPendingOperations.size(); + while (i > 0) { + i--; + // TODO(fredq): this probably shouldn't be considering + // pending operations. + PendingOperation op = mPendingOperations.get(i); + if (op.account.equals(account) && op.authority.equals(authority)) { + return true; + } + } + + if (mActiveSync != null) { + AuthorityInfo ainfo = getAuthority(mActiveSync.authorityId); + if (ainfo != null && ainfo.account.equals(account) + && ainfo.authority.equals(authority)) { + return true; + } + } } - - - if (rowID > 0) { - mContext.getContentResolver().notifyChange(url, null /* observer */); - return Uri.parse("content://sync/" + table + "/" + rowID); + + return false; + } + + public PendingOperation insertIntoPending(PendingOperation op) { + synchronized (mAuthorities) { + if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account + + " auth=" + op.authority + + " src=" + op.syncSource + + " extras=" + op.extras); + + AuthorityInfo authority = getOrCreateAuthorityLocked(op.account, + op.authority, + -1 /* desired identifier */, + true /* write accounts to storage */); + if (authority == null) { + return null; + } + + op = new PendingOperation(op); + op.authorityId = authority.ident; + mPendingOperations.add(op); + appendPendingOperationLocked(op); + + SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); + status.pending = true; } + + reportChange(CHANGE_PENDING); + return op; + } - return null; + public boolean deleteFromPending(PendingOperation op) { + boolean res = false; + synchronized (mAuthorities) { + if (DEBUG) Log.v(TAG, "deleteFromPending: account=" + op.account + + " auth=" + op.authority + + " src=" + op.syncSource + + " extras=" + op.extras); + if (mPendingOperations.remove(op)) { + if (mPendingOperations.size() == 0 + || mNumPendingFinished >= PENDING_FINISH_TO_WRITE) { + writePendingOperationsLocked(); + mNumPendingFinished = 0; + } else { + mNumPendingFinished++; + } + + AuthorityInfo authority = getAuthorityLocked(op.account, op.authority, + "deleteFromPending"); + if (authority != null) { + if (DEBUG) Log.v(TAG, "removing - " + authority); + final int N = mPendingOperations.size(); + boolean morePending = false; + for (int i=0; i<N; i++) { + PendingOperation cur = mPendingOperations.get(i); + if (cur.account.equals(op.account) + && cur.authority.equals(op.authority)) { + morePending = true; + break; + } + } + + if (!morePending) { + if (DEBUG) Log.v(TAG, "no more pending!"); + SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); + status.pending = false; + } + } + + res = true; + } + } + + reportChange(CHANGE_PENDING); + return res; } - private static void checkCaller(boolean callerIsTheProvider, int match) { - if (callerIsTheProvider && match != SETTINGS) { - throw new UnsupportedOperationException( - "only the settings are modifiable via the ContentProvider interface"); + public int clearPending() { + int num; + synchronized (mAuthorities) { + if (DEBUG) Log.v(TAG, "clearPending"); + num = mPendingOperations.size(); + mPendingOperations.clear(); + final int N = mSyncStatus.size(); + for (int i=0; i<N; i++) { + mSyncStatus.get(i).pending = false; + } + writePendingOperationsLocked(); } + reportChange(CHANGE_PENDING); + return num; } /** - * Implements the {@link ContentProvider#delete} method - * @param callerIsTheProvider true if this is being called via the - * {@link ContentProvider#delete} in method rather than directly. - * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't - * for the Settings table. + * Return a copy of the current array of pending operations. The + * PendingOperation objects are the real objects stored inside, so that + * they can be used with deleteFromPending(). */ - public int delete(boolean callerIsTheProvider, Uri url, String where, String[] whereArgs) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - int match = sURLMatcher.match(url); - - int numRows; - switch (match) { - case SETTINGS: - mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, - "no permission to write the sync settings"); - numRows = db.delete("settings", where, whereArgs); - break; - default: - throw new UnsupportedOperationException("Cannot delete URL: " + url); + public ArrayList<PendingOperation> getPendingOperations() { + synchronized (mAuthorities) { + return new ArrayList<PendingOperation>(mPendingOperations); } - - if (numRows > 0) { - mContext.getContentResolver().notifyChange(url, null /* observer */); - } - return numRows; } - + /** - * Implements the {@link ContentProvider#update} method - * @param callerIsTheProvider true if this is being called via the - * {@link ContentProvider#update} in method rather than directly. - * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't - * for the Settings table. + * Return the number of currently pending operations. */ - public int update(boolean callerIsTheProvider, Uri url, ContentValues initialValues, - String where, String[] whereArgs) { - switch (sURLMatcher.match(url)) { - case SETTINGS: - throw new UnsupportedOperationException("updating url " + url - + " is not allowed, use insert instead"); - default: - throw new UnsupportedOperationException("Cannot update URL: " + url); + public int getPendingOperationCount() { + synchronized (mAuthorities) { + return mPendingOperations.size(); } } - + /** - * Implements the {@link ContentProvider#getType} method + * Called when the set of account has changed, given the new array of + * active accounts. */ - public String getType(Uri url) { - int match = sURLMatcher.match(url); - switch (match) { - case SETTINGS: - return "vnd.android.cursor.dir/sync-settings"; - default: - throw new IllegalArgumentException("Unknown URL"); + public void doDatabaseCleanup(String[] accounts) { + synchronized (mAuthorities) { + if (DEBUG) Log.w(TAG, "Updating for new accounts..."); + SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>(); + Iterator<AccountInfo> accIt = mAccounts.values().iterator(); + while (accIt.hasNext()) { + AccountInfo acc = accIt.next(); + if (!ArrayUtils.contains(accounts, acc.account)) { + // This account no longer exists... + if (DEBUG) Log.w(TAG, "Account removed: " + acc.account); + for (AuthorityInfo auth : acc.authorities.values()) { + removing.put(auth.ident, auth); + } + accIt.remove(); + } + } + + // Clean out all data structures. + int i = removing.size(); + if (i > 0) { + while (i > 0) { + i--; + int ident = removing.keyAt(i); + mAuthorities.remove(ident); + int j = mSyncStatus.size(); + while (j > 0) { + j--; + if (mSyncStatus.keyAt(j) == ident) { + mSyncStatus.remove(mSyncStatus.keyAt(j)); + } + } + j = mSyncHistory.size(); + while (j > 0) { + j--; + if (mSyncHistory.get(j).authorityId == ident) { + mSyncHistory.remove(j); + } + } + } + writeAccountInfoLocked(); + writeStatusLocked(); + writePendingOperationsLocked(); + writeStatisticsLocked(); + } } } - protected Uri insertIntoPending(ContentValues values) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - try { - db.beginTransaction(); - long rowId = db.insert("pending", Sync.Pending.ACCOUNT, values); - if (rowId < 0) return null; - String account = values.getAsString(Sync.Pending.ACCOUNT); - String authority = values.getAsString(Sync.Pending.AUTHORITY); - - long statsId = createStatsRowIfNecessary(account, authority); - createStatusRowIfNecessary(statsId); - - values.clear(); - values.put(Sync.Status.PENDING, 1); - int numUpdatesStatus = db.update("status", values, "stats_id=" + statsId, null); - - db.setTransactionSuccessful(); - - mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI, - null /* no observer initiated this change */); - if (numUpdatesStatus > 0) { - mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, - null /* no observer initiated this change */); + /** + * Called when the currently active sync is changing (there can only be + * one at a time). Either supply a valid ActiveSyncContext with information + * about the sync, or null to stop the currently active sync. + */ + public void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) { + synchronized (mAuthorities) { + if (activeSyncContext != null) { + if (DEBUG) Log.v(TAG, "setActiveSync: account=" + + activeSyncContext.mSyncOperation.account + + " auth=" + activeSyncContext.mSyncOperation.authority + + " src=" + activeSyncContext.mSyncOperation.syncSource + + " extras=" + activeSyncContext.mSyncOperation.extras); + if (mActiveSync != null) { + Log.w(TAG, "setActiveSync called with existing active sync!"); + } + AuthorityInfo authority = getAuthorityLocked( + activeSyncContext.mSyncOperation.account, + activeSyncContext.mSyncOperation.authority, + "setActiveSync"); + if (authority == null) { + return; + } + mActiveSync = new ActiveSyncInfo(authority.ident, + authority.account, authority.authority, + activeSyncContext.mStartTime); + } else { + if (DEBUG) Log.v(TAG, "setActiveSync: null"); + mActiveSync = null; } - return ContentUris.withAppendedId(Sync.Pending.CONTENT_URI, rowId); - } finally { - db.endTransaction(); } + + reportChange(CHANGE_ACTIVE); } - int deleteFromPending(long rowId) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - String account; - String authority; - Cursor c = db.query("pending", - new String[]{Sync.Pending.ACCOUNT, Sync.Pending.AUTHORITY}, - "_id=" + rowId, null, null, null, null); - try { - if (c.getCount() != 1) { - return 0; - } - c.moveToNext(); - account = c.getString(0); - authority = c.getString(1); - } finally { - c.close(); - } - db.delete("pending", "_id=" + rowId, null /* no where args */); - final String[] accountAuthorityWhereArgs = new String[]{account, authority}; - boolean isPending = 0 < DatabaseUtils.longForQuery(db, - "SELECT COUNT(*) FROM PENDING WHERE account=? AND authority=?", - accountAuthorityWhereArgs); - if (!isPending) { - long statsId = createStatsRowIfNecessary(account, authority); - db.execSQL("UPDATE status SET pending=0 WHERE stats_id=" + statsId); - } - db.setTransactionSuccessful(); - - mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI, - null /* no observer initiated this change */); - if (!isPending) { - mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, - null /* no observer initiated this change */); - } - return 1; - } finally { - db.endTransaction(); + /** + * To allow others to send active change reports, to poke clients. + */ + public void reportActiveChange() { + reportChange(CHANGE_ACTIVE); + } + + /** + * Note that sync has started for the given account and authority. + */ + public long insertStartSyncEvent(String accountName, String authorityName, + long now, int source) { + long id; + synchronized (mAuthorities) { + if (DEBUG) Log.v(TAG, "insertStartSyncEvent: account=" + accountName + + " auth=" + authorityName + " source=" + source); + AuthorityInfo authority = getAuthorityLocked(accountName, authorityName, + "insertStartSyncEvent"); + if (authority == null) { + return -1; + } + SyncHistoryItem item = new SyncHistoryItem(); + item.authorityId = authority.ident; + item.historyId = mNextHistoryId++; + if (mNextHistoryId < 0) mNextHistoryId = 0; + item.eventTime = now; + item.source = source; + item.event = EVENT_START; + mSyncHistory.add(0, item); + while (mSyncHistory.size() > MAX_HISTORY) { + mSyncHistory.remove(mSyncHistory.size()-1); + } + id = item.historyId; + if (DEBUG) Log.v(TAG, "returning historyId " + id); } + + reportChange(CHANGE_STATUS); + return id; } - int clearPending() { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - int numChanges = db.delete("pending", null, null /* no where args */); - if (numChanges > 0) { - db.execSQL("UPDATE status SET pending=0"); - mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI, - null /* no observer initiated this change */); - mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, - null /* no observer initiated this change */); - } - db.setTransactionSuccessful(); - return numChanges; - } finally { - db.endTransaction(); + public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage, + long downstreamActivity, long upstreamActivity) { + synchronized (mAuthorities) { + if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId); + SyncHistoryItem item = null; + int i = mSyncHistory.size(); + while (i > 0) { + i--; + item = mSyncHistory.get(i); + if (item.historyId == historyId) { + break; + } + item = null; + } + + if (item == null) { + Log.w(TAG, "stopSyncEvent: no history for id " + historyId); + return; + } + + item.elapsedTime = elapsedTime; + item.event = EVENT_STOP; + item.mesg = resultMessage; + item.downstreamActivity = downstreamActivity; + item.upstreamActivity = upstreamActivity; + + SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId); + + status.numSyncs++; + status.totalElapsedTime += elapsedTime; + switch (item.source) { + case SOURCE_LOCAL: + status.numSourceLocal++; + break; + case SOURCE_POLL: + status.numSourcePoll++; + break; + case SOURCE_USER: + status.numSourceUser++; + break; + case SOURCE_SERVER: + status.numSourceServer++; + break; + } + + boolean writeStatisticsNow = false; + int day = getCurrentDay(); + if (mDayStats[0] == null) { + mDayStats[0] = new DayStats(day); + } else if (day != mDayStats[0].day) { + System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1); + mDayStats[0] = new DayStats(day); + writeStatisticsNow = true; + } else if (mDayStats[0] == null) { + } + final DayStats ds = mDayStats[0]; + + final long lastSyncTime = (item.eventTime + elapsedTime); + boolean writeStatusNow = false; + if (MESG_SUCCESS.equals(resultMessage)) { + // - if successful, update the successful columns + if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) { + writeStatusNow = true; + } + status.lastSuccessTime = lastSyncTime; + status.lastSuccessSource = item.source; + status.lastFailureTime = 0; + status.lastFailureSource = -1; + status.lastFailureMesg = null; + status.initialFailureTime = 0; + ds.successCount++; + ds.successTime += elapsedTime; + } else if (!MESG_CANCELED.equals(resultMessage)) { + if (status.lastFailureTime == 0) { + writeStatusNow = true; + } + status.lastFailureTime = lastSyncTime; + status.lastFailureSource = item.source; + status.lastFailureMesg = resultMessage; + if (status.initialFailureTime == 0) { + status.initialFailureTime = lastSyncTime; + } + ds.failureCount++; + ds.failureTime += elapsedTime; + } + + if (writeStatusNow) { + writeStatusLocked(); + } else if (!hasMessages(MSG_WRITE_STATUS)) { + sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS), + WRITE_STATUS_DELAY); + } + if (writeStatisticsNow) { + writeStatisticsLocked(); + } else if (!hasMessages(MSG_WRITE_STATISTICS)) { + sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS), + WRITE_STATISTICS_DELAY); + } } + + reportChange(CHANGE_STATUS); } /** - * Returns a cursor over all the pending syncs in no particular order. This cursor is not - * "live", in that if changes are made to the pending table any observers on this cursor - * will not be notified. - * @param projection Return only these columns. If null then all columns are returned. - * @return the cursor of pending syncs + * Return the currently active sync information, or null if there is no + * active sync. Note that the returned object is the real, live active + * sync object, so be careful what you do with it. */ - public Cursor getPendingSyncsCursor(String[] projection) { - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - return db.query("pending", projection, null, null, null, null, null); + public ActiveSyncInfo getActiveSync() { + synchronized (mAuthorities) { + return mActiveSync; + } } - - // @VisibleForTesting - static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; - - private boolean purgeOldHistoryEvents(long now) { - // remove events that are older than MILLIS_IN_4WEEKS - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - int numDeletes = db.delete("history", "eventTime<" + (now - MILLIS_IN_4WEEKS), null); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - if (numDeletes > 0) { - Log.v(TAG, "deleted " + numDeletes + " old event(s) from the sync history"); + + /** + * Return an array of the current sync status for all authorities. Note + * that the objects inside the array are the real, live status objects, + * so be careful what you do with them. + */ + public ArrayList<SyncStatusInfo> getSyncStatus() { + synchronized (mAuthorities) { + final int N = mSyncStatus.size(); + ArrayList<SyncStatusInfo> ops = new ArrayList<SyncStatusInfo>(N); + for (int i=0; i<N; i++) { + ops.add(mSyncStatus.valueAt(i)); } + return ops; } - - // keep only the last MAX_HISTORY_EVENTS_TO_KEEP history events - numDeletes += db.delete("history", "eventTime < (select min(eventTime) from " - + "(select eventTime from history order by eventTime desc limit ?))", - new String[]{String.valueOf(MAX_HISTORY_EVENTS_TO_KEEP)}); - - return numDeletes > 0; } - - public long insertStartSyncEvent(String account, String authority, long now, int source) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - long statsId = createStatsRowIfNecessary(account, authority); - - purgeOldHistoryEvents(now); - ContentValues values = new ContentValues(); - values.put(Sync.History.STATS_ID, statsId); - values.put(Sync.History.EVENT_TIME, now); - values.put(Sync.History.SOURCE, source); - values.put(Sync.History.EVENT, Sync.History.EVENT_START); - long rowId = db.insert("history", null, values); - mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, null /* observer */); - mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, null /* observer */); - return rowId; + + /** + * Returns the status that matches the authority. If there are multiples accounts for + * the authority, the one with the latest "lastSuccessTime" status is returned. + * @param authority the authority whose row should be selected + * @return the SyncStatusInfo for the authority, or null if none exists + */ + public SyncStatusInfo getStatusByAuthority(String authority) { + synchronized (mAuthorities) { + SyncStatusInfo best = null; + final int N = mSyncStatus.size(); + for (int i=0; i<N; i++) { + SyncStatusInfo cur = mSyncStatus.get(i); + AuthorityInfo ainfo = mAuthorities.get(cur.authorityId); + if (ainfo != null && ainfo.authority.equals(authority)) { + if (best == null) { + best = cur; + } else if (best.lastSuccessTime > cur.lastSuccessTime) { + best = cur; + } + } + } + return best; + } } - - public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage, - long downstreamActivity, long upstreamActivity) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(Sync.History.ELAPSED_TIME, elapsedTime); - values.put(Sync.History.EVENT, Sync.History.EVENT_STOP); - values.put(Sync.History.MESG, resultMessage); - values.put(Sync.History.DOWNSTREAM_ACTIVITY, downstreamActivity); - values.put(Sync.History.UPSTREAM_ACTIVITY, upstreamActivity); - - int count = db.update("history", values, "_id=?", - new String[]{Long.toString(historyId)}); - // We think that count should always be 1 but don't want to change this until after - // launch. - if (count > 0) { - int source = (int) DatabaseUtils.longForQuery(db, - "SELECT source FROM history WHERE _id=" + historyId, null); - long eventTime = DatabaseUtils.longForQuery(db, - "SELECT eventTime FROM history WHERE _id=" + historyId, null); - long statsId = DatabaseUtils.longForQuery(db, - "SELECT stats_id FROM history WHERE _id=" + historyId, null); - - createStatusRowIfNecessary(statsId); - - // update the status table to reflect this sync - StringBuilder sb = new StringBuilder(); - ArrayList<String> bindArgs = new ArrayList<String>(); - sb.append("UPDATE status SET"); - sb.append(" numSyncs=numSyncs+1"); - sb.append(", totalElapsedTime=totalElapsedTime+" + elapsedTime); - switch (source) { - case Sync.History.SOURCE_LOCAL: - sb.append(", numSourceLocal=numSourceLocal+1"); - break; - case Sync.History.SOURCE_POLL: - sb.append(", numSourcePoll=numSourcePoll+1"); - break; - case Sync.History.SOURCE_USER: - sb.append(", numSourceUser=numSourceUser+1"); - break; - case Sync.History.SOURCE_SERVER: - sb.append(", numSourceServer=numSourceServer+1"); - break; + + /** + * Return true if the pending status is true of any matching authorities. + */ + public boolean isAuthorityPending(String account, String authority) { + synchronized (mAuthorities) { + final int N = mSyncStatus.size(); + for (int i=0; i<N; i++) { + SyncStatusInfo cur = mSyncStatus.get(i); + AuthorityInfo ainfo = mAuthorities.get(cur.authorityId); + if (ainfo == null) { + continue; + } + if (account != null && !ainfo.account.equals(account)) { + continue; + } + if (ainfo.authority.equals(authority) && cur.pending) { + return true; } - - final String statsIdString = String.valueOf(statsId); - final long lastSyncTime = (eventTime + elapsedTime); - if (Sync.History.MESG_SUCCESS.equals(resultMessage)) { - // - if successful, update the successful columns - sb.append(", lastSuccessTime=" + lastSyncTime); - sb.append(", lastSuccessSource=" + source); - sb.append(", lastFailureTime=null"); - sb.append(", lastFailureSource=null"); - sb.append(", lastFailureMesg=null"); - sb.append(", initialFailureTime=null"); - } else if (!Sync.History.MESG_CANCELED.equals(resultMessage)) { - sb.append(", lastFailureTime=" + lastSyncTime); - sb.append(", lastFailureSource=" + source); - sb.append(", lastFailureMesg=?"); - bindArgs.add(resultMessage); - long initialFailureTime = DatabaseUtils.longForQuery(db, - SELECT_INITIAL_FAILURE_TIME_QUERY_STRING, - new String[]{statsIdString, String.valueOf(lastSyncTime)}); - sb.append(", initialFailureTime=" + initialFailureTime); - } - sb.append(" WHERE stats_id=?"); - bindArgs.add(statsIdString); - db.execSQL(sb.toString(), bindArgs.toArray()); - db.setTransactionSuccessful(); - mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, - null /* observer */); - mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, - null /* observer */); } - } finally { - db.endTransaction(); + return false; } } /** + * Return an array of the current sync status for all authorities. Note + * that the objects inside the array are the real, live status objects, + * so be careful what you do with them. + */ + public ArrayList<SyncHistoryItem> getSyncHistory() { + synchronized (mAuthorities) { + final int N = mSyncHistory.size(); + ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N); + for (int i=0; i<N; i++) { + items.add(mSyncHistory.get(i)); + } + return items; + } + } + + /** + * Return an array of the current per-day statistics. Note + * that the objects inside the array are the real, live status objects, + * so be careful what you do with them. + */ + public DayStats[] getDayStatistics() { + synchronized (mAuthorities) { + DayStats[] ds = new DayStats[mDayStats.length]; + System.arraycopy(mDayStats, 0, ds, 0, ds.length); + return ds; + } + } + + /** * If sync is failing for any of the provider/accounts then determine the time at which it * started failing and return the earliest time over all the provider/accounts. If none are * failing then return 0. */ public long getInitialSyncFailureTime() { - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - // Join the settings for a provider with the status so that we can easily - // check if each provider is enabled for syncing. We also join in the overall - // enabled flag ("listen_for_tickles") to each row so that we don't need to - // make a separate DB lookup to access it. - Cursor c = db.rawQuery("" - + "SELECT initialFailureTime, s1.value, s2.value " - + "FROM status " - + "LEFT JOIN stats ON status.stats_id=stats._id " - + "LEFT JOIN settings as s1 ON 'sync_provider_' || authority=s1.name " - + "LEFT JOIN settings as s2 ON s2.name='listen_for_tickles' " - + "where initialFailureTime is not null " - + " AND lastFailureMesg!=" + Sync.History.ERROR_TOO_MANY_DELETIONS - + " AND lastFailureMesg!=" + Sync.History.ERROR_AUTHENTICATION - + " AND lastFailureMesg!=" + Sync.History.ERROR_SYNC_ALREADY_IN_PROGRESS - + " AND authority!='subscribedfeeds' " - + " ORDER BY initialFailureTime", null); + synchronized (mAuthorities) { + if (!mListenForTickles) { + return 0; + } + + long oldest = 0; + int i = mSyncStatus.size(); + while (i > 0) { + i--; + SyncStatusInfo stats = mSyncStatus.valueAt(i); + AuthorityInfo authority = mAuthorities.get(stats.authorityId); + if (authority != null && authority.enabled) { + if (oldest == 0 || stats.initialFailureTime < oldest) { + oldest = stats.initialFailureTime; + } + } + } + + return oldest; + } + } + + private int getCurrentDay() { + mCal.setTimeInMillis(System.currentTimeMillis()); + final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR); + if (mYear != mCal.get(Calendar.YEAR)) { + mYear = mCal.get(Calendar.YEAR); + mCal.clear(); + mCal.set(Calendar.YEAR, mYear); + mYearInDays = (int)(mCal.getTimeInMillis()/86400000); + } + return dayOfYear + mYearInDays; + } + + /** + * Retrieve an authority, returning null if one does not exist. + * + * @param accountName The name of the account for the authority. + * @param authorityName The name of the authority itself. + * @param tag If non-null, this will be used in a log message if the + * requested authority does not exist. + */ + private AuthorityInfo getAuthorityLocked(String accountName, String authorityName, + String tag) { + AccountInfo account = mAccounts.get(accountName); + if (account == null) { + if (tag != null) { + Log.w(TAG, tag + ": unknown account " + accountName); + } + return null; + } + AuthorityInfo authority = account.authorities.get(authorityName); + if (authority == null) { + if (tag != null) { + Log.w(TAG, tag + ": unknown authority " + authorityName); + } + return null; + } + + return authority; + } + + private AuthorityInfo getOrCreateAuthorityLocked(String accountName, + String authorityName, int ident, boolean doWrite) { + AccountInfo account = mAccounts.get(accountName); + if (account == null) { + account = new AccountInfo(accountName); + mAccounts.put(accountName, account); + } + AuthorityInfo authority = account.authorities.get(authorityName); + if (authority == null) { + if (ident < 0) { + // Look for a new identifier for this authority. + final int N = mAuthorities.size(); + ident = 0; + for (int i=0; i<N; i++) { + if (mAuthorities.valueAt(i).ident > ident) { + break; + } + ident++; + } + } + authority = new AuthorityInfo(accountName, authorityName, ident); + account.authorities.put(authorityName, authority); + mAuthorities.put(ident, authority); + if (doWrite) { + writeAccountInfoLocked(); + } + } + + return authority; + } + + private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) { + SyncStatusInfo status = mSyncStatus.get(authorityId); + if (status == null) { + status = new SyncStatusInfo(authorityId); + mSyncStatus.put(authorityId, status); + } + return status; + } + + /** + * Read all account information back in to the initial engine state. + */ + private void readAccountInfoLocked() { + FileInputStream fis = null; try { + fis = mAccountInfoFile.openRead(); + if (DEBUG_FILE) Log.v(TAG, "Reading " + mAccountInfoFile.getBaseFile()); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, null); + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG) { + eventType = parser.next(); + } + String tagName = parser.getName(); + if ("accounts".equals(tagName)) { + String listen = parser.getAttributeValue( + null, "listen-for-tickles"); + mListenForTickles = listen == null + || Boolean.parseBoolean(listen); + eventType = parser.next(); + do { + if (eventType == XmlPullParser.START_TAG + && parser.getDepth() == 2) { + tagName = parser.getName(); + if ("authority".equals(tagName)) { + int id = -1; + try { + id = Integer.parseInt(parser.getAttributeValue( + null, "id")); + } catch (NumberFormatException e) { + } catch (NullPointerException e) { + } + if (id >= 0) { + String accountName = parser.getAttributeValue( + null, "account"); + String authorityName = parser.getAttributeValue( + null, "authority"); + String enabled = parser.getAttributeValue( + null, "enabled"); + AuthorityInfo authority = mAuthorities.get(id); + if (DEBUG_FILE) Log.v(TAG, "Adding authority: account=" + + accountName + " auth=" + authorityName + + " enabled=" + enabled); + if (authority == null) { + if (DEBUG_FILE) Log.v(TAG, "Creating entry"); + authority = getOrCreateAuthorityLocked( + accountName, authorityName, id, false); + } + if (authority != null) { + authority.enabled = enabled == null + || Boolean.parseBoolean(enabled); + } else { + Log.w(TAG, "Failure adding authority: account=" + + accountName + " auth=" + authorityName + + " enabled=" + enabled); + } + } + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + } + } catch (XmlPullParserException e) { + Log.w(TAG, "Error reading accounts", e); + } catch (java.io.IOException e) { + if (fis == null) Log.i(TAG, "No initial accounts"); + else Log.w(TAG, "Error reading accounts", e); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (java.io.IOException e1) { + } + } + } + } + + /** + * Write all account information to the account file. + */ + private void writeAccountInfoLocked() { + if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile()); + FileOutputStream fos = null; + + try { + fos = mAccountInfoFile.startWrite(); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(fos, "utf-8"); + out.startDocument(null, true); + out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + out.startTag(null, "accounts"); + if (!mListenForTickles) { + out.attribute(null, "listen-for-tickles", "false"); + } + + final int N = mAuthorities.size(); + for (int i=0; i<N; i++) { + AuthorityInfo authority = mAuthorities.get(i); + out.startTag(null, "authority"); + out.attribute(null, "id", Integer.toString(authority.ident)); + out.attribute(null, "account", authority.account); + out.attribute(null, "authority", authority.authority); + if (!authority.enabled) { + out.attribute(null, "enabled", "false"); + } + out.endTag(null, "authority"); + } + + out.endTag(null, "accounts"); + + out.endDocument(); + + mAccountInfoFile.finishWrite(fos); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing accounts", e1); + if (fos != null) { + mAccountInfoFile.failWrite(fos); + } + } + } + + static int getIntColumn(Cursor c, String name) { + return c.getInt(c.getColumnIndex(name)); + } + + static long getLongColumn(Cursor c, String name) { + return c.getLong(c.getColumnIndex(name)); + } + + /** + * Load sync engine state from the old syncmanager database, and then + * erase it. Note that we don't deal with pending operations, active + * sync, or history. + */ + private void readLegacyAccountInfoLocked() { + // Look for old database to initialize from. + File file = mContext.getDatabasePath("syncmanager.db"); + if (!file.exists()) { + return; + } + String path = file.getPath(); + SQLiteDatabase db = null; + try { + db = SQLiteDatabase.openDatabase(path, null, + SQLiteDatabase.OPEN_READONLY); + } catch (SQLiteException e) { + } + + if (db != null) { + // Copy in all of the status information, as well as accounts. + if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db"); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables("stats, status"); + HashMap<String,String> map = new HashMap<String,String>(); + map.put("_id", "status._id as _id"); + map.put("account", "stats.account as account"); + map.put("authority", "stats.authority as authority"); + map.put("totalElapsedTime", "totalElapsedTime"); + map.put("numSyncs", "numSyncs"); + map.put("numSourceLocal", "numSourceLocal"); + map.put("numSourcePoll", "numSourcePoll"); + map.put("numSourceServer", "numSourceServer"); + map.put("numSourceUser", "numSourceUser"); + map.put("lastSuccessSource", "lastSuccessSource"); + map.put("lastSuccessTime", "lastSuccessTime"); + map.put("lastFailureSource", "lastFailureSource"); + map.put("lastFailureTime", "lastFailureTime"); + map.put("lastFailureMesg", "lastFailureMesg"); + map.put("pending", "pending"); + qb.setProjectionMap(map); + qb.appendWhere("stats._id = status.stats_id"); + Cursor c = qb.query(db, null, null, null, null, null, null); while (c.moveToNext()) { - // these settings default to true, so if they are null treat them as enabled - final String providerEnabledString = c.getString(1); - if (providerEnabledString != null && !Boolean.parseBoolean(providerEnabledString)) { - continue; + String accountName = c.getString(c.getColumnIndex("account")); + String authorityName = c.getString(c.getColumnIndex("authority")); + AuthorityInfo authority = this.getOrCreateAuthorityLocked( + accountName, authorityName, -1, false); + if (authority != null) { + int i = mSyncStatus.size(); + boolean found = false; + SyncStatusInfo st = null; + while (i > 0) { + i--; + st = mSyncStatus.get(i); + if (st.authorityId == authority.ident) { + found = true; + break; + } + } + if (!found) { + st = new SyncStatusInfo(authority.ident); + mSyncStatus.put(authority.ident, st); + } + st.totalElapsedTime = getLongColumn(c, "totalElapsedTime"); + st.numSyncs = getIntColumn(c, "numSyncs"); + st.numSourceLocal = getIntColumn(c, "numSourceLocal"); + st.numSourcePoll = getIntColumn(c, "numSourcePoll"); + st.numSourceServer = getIntColumn(c, "numSourceServer"); + st.numSourceUser = getIntColumn(c, "numSourceUser"); + st.lastSuccessSource = getIntColumn(c, "lastSuccessSource"); + st.lastSuccessTime = getLongColumn(c, "lastSuccessTime"); + st.lastFailureSource = getIntColumn(c, "lastFailureSource"); + st.lastFailureTime = getLongColumn(c, "lastFailureTime"); + st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg")); + st.pending = getIntColumn(c, "pending") != 0; } - final String allEnabledString = c.getString(2); - if (allEnabledString != null && !Boolean.parseBoolean(allEnabledString)) { - continue; + } + + c.close(); + + // Retrieve the settings. + qb = new SQLiteQueryBuilder(); + qb.setTables("settings"); + c = qb.query(db, null, null, null, null, null, null); + while (c.moveToNext()) { + String name = c.getString(c.getColumnIndex("name")); + String value = c.getString(c.getColumnIndex("value")); + if (name == null) continue; + if (name.equals("listen_for_tickles")) { + setListenForNetworkTickles(value == null + || Boolean.parseBoolean(value)); + } else if (name.startsWith("sync_provider_")) { + String provider = name.substring("sync_provider_".length(), + name.length()); + setSyncProviderAutomatically(null, provider, + value == null || Boolean.parseBoolean(value)); } - return c.getLong(0); } - } finally { + c.close(); + + db.close(); + + writeAccountInfoLocked(); + writeStatusLocked(); + (new File(path)).delete(); } - return 0; } - - private void createStatusRowIfNecessary(long statsId) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - boolean statusExists = 0 != DatabaseUtils.longForQuery(db, - "SELECT count(*) FROM status WHERE stats_id=" + statsId, null); - if (!statusExists) { - ContentValues values = new ContentValues(); - values.put("stats_id", statsId); - db.insert("status", null, values); + + public static final int STATUS_FILE_END = 0; + public static final int STATUS_FILE_ITEM = 100; + + /** + * Read all sync status back in to the initial engine state. + */ + private void readStatusLocked() { + if (DEBUG_FILE) Log.v(TAG, "Reading " + mStatusFile.getBaseFile()); + try { + byte[] data = mStatusFile.readFully(); + Parcel in = Parcel.obtain(); + in.unmarshall(data, 0, data.length); + in.setDataPosition(0); + int token; + while ((token=in.readInt()) != STATUS_FILE_END) { + if (token == STATUS_FILE_ITEM) { + SyncStatusInfo status = new SyncStatusInfo(in); + if (mAuthorities.indexOfKey(status.authorityId) >= 0) { + status.pending = false; + if (DEBUG_FILE) Log.v(TAG, "Adding status for id " + + status.authorityId); + mSyncStatus.put(status.authorityId, status); + } + } else { + // Ooops. + Log.w(TAG, "Unknown status token: " + token); + break; + } + } + } catch (java.io.IOException e) { + Log.i(TAG, "No initial status"); } } - - private long createStatsRowIfNecessary(String account, String authority) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - StringBuilder where = new StringBuilder(); - where.append(Sync.Stats.ACCOUNT + "= ?"); - where.append(" and " + Sync.Stats.AUTHORITY + "= ?"); - Cursor cursor = query(Sync.Stats.CONTENT_URI, - Sync.Stats.SYNC_STATS_PROJECTION, - where.toString(), new String[] { account, authority }, - null /* order */); + + /** + * Write all sync status to the sync status file. + */ + private void writeStatusLocked() { + if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile()); + + // The file is being written, so we don't need to have a scheduled + // write until the next change. + removeMessages(MSG_WRITE_STATUS); + + FileOutputStream fos = null; try { - long id; - if (cursor.moveToFirst()) { - id = cursor.getLong(cursor.getColumnIndexOrThrow(Sync.Stats._ID)); - } else { - ContentValues values = new ContentValues(); - values.put(Sync.Stats.ACCOUNT, account); - values.put(Sync.Stats.AUTHORITY, authority); - id = db.insert("stats", null, values); + fos = mStatusFile.startWrite(); + Parcel out = Parcel.obtain(); + final int N = mSyncStatus.size(); + for (int i=0; i<N; i++) { + SyncStatusInfo status = mSyncStatus.valueAt(i); + out.writeInt(STATUS_FILE_ITEM); + status.writeToParcel(out, 0); } - return id; + out.writeInt(STATUS_FILE_END); + fos.write(out.marshall()); + out.recycle(); + + mStatusFile.finishWrite(fos); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing status", e1); + if (fos != null) { + mStatusFile.failWrite(fos); + } + } + } + + public static final int PENDING_OPERATION_VERSION = 1; + + /** + * Read all pending operations back in to the initial engine state. + */ + private void readPendingOperationsLocked() { + if (DEBUG_FILE) Log.v(TAG, "Reading " + mPendingFile.getBaseFile()); + try { + byte[] data = mPendingFile.readFully(); + Parcel in = Parcel.obtain(); + in.unmarshall(data, 0, data.length); + in.setDataPosition(0); + final int SIZE = in.dataSize(); + while (in.dataPosition() < SIZE) { + int version = in.readInt(); + if (version != PENDING_OPERATION_VERSION) { + Log.w(TAG, "Unknown pending operation version " + + version + "; dropping all ops"); + break; + } + int authorityId = in.readInt(); + int syncSource = in.readInt(); + byte[] flatExtras = in.createByteArray(); + AuthorityInfo authority = mAuthorities.get(authorityId); + if (authority != null) { + Bundle extras = null; + if (flatExtras != null) { + extras = unflattenBundle(flatExtras); + } + PendingOperation op = new PendingOperation( + authority.account, syncSource, + authority.authority, extras); + op.authorityId = authorityId; + op.flatExtras = flatExtras; + if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account + + " auth=" + op.authority + + " src=" + op.syncSource + + " extras=" + op.extras); + mPendingOperations.add(op); + } + } + } catch (java.io.IOException e) { + Log.i(TAG, "No initial pending operations"); + } + } + + private void writePendingOperationLocked(PendingOperation op, Parcel out) { + out.writeInt(PENDING_OPERATION_VERSION); + out.writeInt(op.authorityId); + out.writeInt(op.syncSource); + if (op.flatExtras == null && op.extras != null) { + op.flatExtras = flattenBundle(op.extras); + } + out.writeByteArray(op.flatExtras); + } + + /** + * Write all currently pending ops to the pending ops file. + */ + private void writePendingOperationsLocked() { + final int N = mPendingOperations.size(); + FileOutputStream fos = null; + try { + if (N == 0) { + if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile()); + mPendingFile.truncate(); + return; + } + + if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile()); + fos = mPendingFile.startWrite(); + + Parcel out = Parcel.obtain(); + for (int i=0; i<N; i++) { + PendingOperation op = mPendingOperations.get(i); + writePendingOperationLocked(op, out); + } + fos.write(out.marshall()); + out.recycle(); + + mPendingFile.finishWrite(fos); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing pending operations", e1); + if (fos != null) { + mPendingFile.failWrite(fos); + } + } + } + + /** + * Append the given operation to the pending ops file; if unable to, + * write all pending ops. + */ + private void appendPendingOperationLocked(PendingOperation op) { + if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile()); + FileOutputStream fos = null; + try { + fos = mPendingFile.openAppend(); + } catch (java.io.IOException e) { + if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file"); + writePendingOperationsLocked(); + return; + } + + try { + Parcel out = Parcel.obtain(); + writePendingOperationLocked(op, out); + fos.write(out.marshall()); + out.recycle(); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing pending operations", e1); + } finally { + try { + fos.close(); + } catch (java.io.IOException e2) { + } + } + } + + static private byte[] flattenBundle(Bundle bundle) { + byte[] flatData = null; + Parcel parcel = Parcel.obtain(); + try { + bundle.writeToParcel(parcel, 0); + flatData = parcel.marshall(); + } finally { + parcel.recycle(); + } + return flatData; + } + + static private Bundle unflattenBundle(byte[] flatData) { + Bundle bundle; + Parcel parcel = Parcel.obtain(); + try { + parcel.unmarshall(flatData, 0, flatData.length); + parcel.setDataPosition(0); + bundle = parcel.readBundle(); + } catch (RuntimeException e) { + // A RuntimeException is thrown if we were unable to parse the parcel. + // Create an empty parcel in this case. + bundle = new Bundle(); } finally { - cursor.close(); + parcel.recycle(); + } + return bundle; + } + + public static final int STATISTICS_FILE_END = 0; + public static final int STATISTICS_FILE_ITEM_OLD = 100; + public static final int STATISTICS_FILE_ITEM = 101; + + /** + * Read all sync statistics back in to the initial engine state. + */ + private void readStatisticsLocked() { + try { + byte[] data = mStatisticsFile.readFully(); + Parcel in = Parcel.obtain(); + in.unmarshall(data, 0, data.length); + in.setDataPosition(0); + int token; + int index = 0; + while ((token=in.readInt()) != STATISTICS_FILE_END) { + if (token == STATISTICS_FILE_ITEM + || token == STATISTICS_FILE_ITEM_OLD) { + int day = in.readInt(); + if (token == STATISTICS_FILE_ITEM_OLD) { + day = day - 2009 + 14245; // Magic! + } + DayStats ds = new DayStats(day); + ds.successCount = in.readInt(); + ds.successTime = in.readLong(); + ds.failureCount = in.readInt(); + ds.failureTime = in.readLong(); + if (index < mDayStats.length) { + mDayStats[index] = ds; + index++; + } + } else { + // Ooops. + Log.w(TAG, "Unknown stats token: " + token); + break; + } + } + } catch (java.io.IOException e) { + Log.i(TAG, "No initial statistics"); + } + } + + /** + * Write all sync statistics to the sync status file. + */ + private void writeStatisticsLocked() { + if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatisticsFile.getBaseFile()); + + // The file is being written, so we don't need to have a scheduled + // write until the next change. + removeMessages(MSG_WRITE_STATISTICS); + + FileOutputStream fos = null; + try { + fos = mStatisticsFile.startWrite(); + Parcel out = Parcel.obtain(); + final int N = mDayStats.length; + for (int i=0; i<N; i++) { + DayStats ds = mDayStats[i]; + if (ds == null) { + break; + } + out.writeInt(STATISTICS_FILE_ITEM); + out.writeInt(ds.day); + out.writeInt(ds.successCount); + out.writeLong(ds.successTime); + out.writeInt(ds.failureCount); + out.writeLong(ds.failureTime); + } + out.writeInt(STATISTICS_FILE_END); + fos.write(out.marshall()); + out.recycle(); + + mStatisticsFile.finishWrite(fos); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing stats", e1); + if (fos != null) { + mStatisticsFile.failWrite(fos); + } } } } diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java index 63f6dff..23c0a7b 100644 --- a/core/java/android/os/RemoteCallbackList.java +++ b/core/java/android/os/RemoteCallbackList.java @@ -49,25 +49,35 @@ import java.util.HashMap; public class RemoteCallbackList<E extends IInterface> { /*package*/ HashMap<IBinder, Callback> mCallbacks = new HashMap<IBinder, Callback>(); - private IInterface[] mActiveBroadcast; + private Object[] mActiveBroadcast; private boolean mKilled = false; private final class Callback implements IBinder.DeathRecipient { final E mCallback; + final Object mCookie; - Callback(E callback) { + Callback(E callback, Object cookie) { mCallback = callback; + mCookie = cookie; } public void binderDied() { synchronized (mCallbacks) { mCallbacks.remove(mCallback.asBinder()); } - onCallbackDied(mCallback); + onCallbackDied(mCallback, mCookie); } } /** + * Simple version of {@link RemoteCallbackList#register(E, Object)} + * that does not take a cookie object. + */ + public boolean register(E callback) { + return register(callback, null); + } + + /** * Add a new callback to the list. This callback will remain in the list * until a corresponding call to {@link #unregister} or its hosting process * goes away. If the callback was already registered (determined by @@ -81,6 +91,8 @@ public class RemoteCallbackList<E extends IInterface> { * Most services will want to check for null before calling this with * an object given from a client, so that clients can't crash the * service with bad data. + * @param cookie Optional additional data to be associated with this + * callback. * * @return Returns true if the callback was successfully added to the list. * Returns false if it was not added, either because {@link #kill} had @@ -90,14 +102,14 @@ public class RemoteCallbackList<E extends IInterface> { * @see #kill * @see #onCallbackDied */ - public boolean register(E callback) { + public boolean register(E callback, Object cookie) { synchronized (mCallbacks) { if (mKilled) { return false; } IBinder binder = callback.asBinder(); try { - Callback cb = new Callback(callback); + Callback cb = new Callback(callback, cookie); binder.linkToDeath(cb, 0); mCallbacks.put(binder, cb); return true; @@ -154,17 +166,28 @@ public class RemoteCallbackList<E extends IInterface> { } /** + * Old version of {@link #onCallbackDied(E, Object)} that + * does not provide a cookie. + */ + public void onCallbackDied(E callback) { + } + + /** * Called when the process hosting a callback in the list has gone away. - * The default implementation does nothing. + * The default implementation calls {@link #onCallbackDied(E)} + * for backwards compatibility. * * @param callback The callback whose process has died. Note that, since * its process has died, you can not make any calls on to this interface. * You can, however, retrieve its IBinder and compare it with another * IBinder to see if it is the same object. + * @param cookie The cookie object original provided to + * {@link #register(E, Object)}. * * @see #register */ - public void onCallbackDied(E callback) { + public void onCallbackDied(E callback, Object cookie) { + onCallbackDied(callback); } /** @@ -203,13 +226,13 @@ public class RemoteCallbackList<E extends IInterface> { if (N <= 0) { return 0; } - IInterface[] active = mActiveBroadcast; + Object[] active = mActiveBroadcast; if (active == null || active.length < N) { - mActiveBroadcast = active = new IInterface[N]; + mActiveBroadcast = active = new Object[N]; } int i=0; for (Callback cb : mCallbacks.values()) { - active[i++] = cb.mCallback; + active[i++] = cb; } return i; } @@ -237,7 +260,17 @@ public class RemoteCallbackList<E extends IInterface> { * @see #beginBroadcast */ public E getBroadcastItem(int index) { - return (E)mActiveBroadcast[index]; + return ((Callback)mActiveBroadcast[index]).mCallback; + } + + /** + * Retrieve the cookie associated with the item + * returned by {@link #getBroadcastItem(int)}. + * + * @see #getBroadcastItem + */ + public Object getBroadcastCookie(int index) { + return ((Callback)mActiveBroadcast[index]).mCookie; } /** @@ -248,7 +281,7 @@ public class RemoteCallbackList<E extends IInterface> { * @see #beginBroadcast */ public void finishBroadcast() { - IInterface[] active = mActiveBroadcast; + Object[] active = mActiveBroadcast; if (active != null) { final int N = active.length; for (int i=0; i<N; i++) { diff --git a/core/java/android/provider/Sync.java b/core/java/android/provider/Sync.java deleted file mode 100644 index 628852f..0000000 --- a/core/java/android/provider/Sync.java +++ /dev/null @@ -1,633 +0,0 @@ -/* - * Copyright (C) 2007 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.provider; - -import android.content.ContentQueryMap; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; - -import java.util.Map; - -/** - * The Sync provider stores information used in managing the syncing of the device, - * including the history and pending syncs. - * - * @hide - */ -public final class Sync { - // utility class - private Sync() {} - - /** - * The content url for this provider. - */ - public static final Uri CONTENT_URI = Uri.parse("content://sync"); - - /** - * Columns from the stats table. - */ - public interface StatsColumns { - /** - * The sync account. - * <P>Type: TEXT</P> - */ - public static final String ACCOUNT = "account"; - - /** - * The content authority (contacts, calendar, etc.). - * <P>Type: TEXT</P> - */ - public static final String AUTHORITY = "authority"; - } - - /** - * Provides constants and utility methods to access and use the stats table. - */ - public static final class Stats implements BaseColumns, StatsColumns { - - // utility class - private Stats() {} - - /** - * The content url for this table. - */ - public static final Uri CONTENT_URI = - Uri.parse("content://sync/stats"); - - /** Projection for the _id column in the stats table. */ - public static final String[] SYNC_STATS_PROJECTION = {_ID}; - } - - /** - * Columns from the history table. - */ - public interface HistoryColumns { - /** - * The ID of the stats row corresponding to this event. - * <P>Type: INTEGER</P> - */ - public static final String STATS_ID = "stats_id"; - - /** - * The source of the sync event (LOCAL, POLL, USER, SERVER). - * <P>Type: INTEGER</P> - */ - public static final String SOURCE = "source"; - - /** - * The type of sync event (START, STOP). - * <P>Type: INTEGER</P> - */ - public static final String EVENT = "event"; - - /** - * The time of the event. - * <P>Type: INTEGER</P> - */ - public static final String EVENT_TIME = "eventTime"; - - /** - * How long this event took. This is only valid if the EVENT is EVENT_STOP. - * <P>Type: INTEGER</P> - */ - public static final String ELAPSED_TIME = "elapsedTime"; - - /** - * Any additional message associated with this event. - * <P>Type: TEXT</P> - */ - public static final String MESG = "mesg"; - - /** - * How much activity was performed sending data to the server. This is sync adapter - * specific, but usually is something like how many record update/insert/delete attempts - * were carried out. This is only valid if the EVENT is EVENT_STOP. - * <P>Type: INTEGER</P> - */ - public static final String UPSTREAM_ACTIVITY = "upstreamActivity"; - - /** - * How much activity was performed while receiving data from the server. - * This is sync adapter specific, but usually is something like how many - * records were received from the server. This is only valid if the - * EVENT is EVENT_STOP. - * <P>Type: INTEGER</P> - */ - public static final String DOWNSTREAM_ACTIVITY = "downstreamActivity"; - } - - /** - * Columns from the history table. - */ - public interface StatusColumns { - /** - * How many syncs were completed for this account and authority. - * <P>Type: INTEGER</P> - */ - public static final String NUM_SYNCS = "numSyncs"; - - /** - * How long all the events for this account and authority took. - * <P>Type: INTEGER</P> - */ - public static final String TOTAL_ELAPSED_TIME = "totalElapsedTime"; - - /** - * The number of syncs with SOURCE_POLL. - * <P>Type: INTEGER</P> - */ - public static final String NUM_SOURCE_POLL = "numSourcePoll"; - - /** - * The number of syncs with SOURCE_SERVER. - * <P>Type: INTEGER</P> - */ - public static final String NUM_SOURCE_SERVER = "numSourceServer"; - - /** - * The number of syncs with SOURCE_LOCAL. - * <P>Type: INTEGER</P> - */ - public static final String NUM_SOURCE_LOCAL = "numSourceLocal"; - - /** - * The number of syncs with SOURCE_USER. - * <P>Type: INTEGER</P> - */ - public static final String NUM_SOURCE_USER = "numSourceUser"; - - /** - * The time in ms that the last successful sync ended. Will be null if - * there are no successful syncs. A successful sync is defined as one having - * MESG=MESG_SUCCESS. - * <P>Type: INTEGER</P> - */ - public static final String LAST_SUCCESS_TIME = "lastSuccessTime"; - - /** - * The SOURCE of the last successful sync. Will be null if - * there are no successful syncs. A successful sync is defined - * as one having MESG=MESG_SUCCESS. - * <P>Type: INTEGER</P> - */ - public static final String LAST_SUCCESS_SOURCE = "lastSuccessSource"; - - /** - * The end time in ms of the last sync that failed since the last successful sync. - * Will be null if there are no syncs or if the last one succeeded. A failed - * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. - * <P>Type: INTEGER</P> - */ - public static final String LAST_FAILURE_TIME = "lastFailureTime"; - - /** - * The SOURCE of the last sync that failed since the last successful sync. - * Will be null if there are no syncs or if the last one succeeded. A failed - * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. - * <P>Type: INTEGER</P> - */ - public static final String LAST_FAILURE_SOURCE = "lastFailureSource"; - - /** - * The MESG of the last sync that failed since the last successful sync. - * Will be null if there are no syncs or if the last one succeeded. A failed - * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. - * <P>Type: STRING</P> - */ - public static final String LAST_FAILURE_MESG = "lastFailureMesg"; - - /** - * Is set to 1 if a sync is pending, 0 if not. - * <P>Type: INTEGER</P> - */ - public static final String PENDING = "pending"; - } - - /** - * Provides constants and utility methods to access and use the history - * table. - */ - public static class History implements BaseColumns, - StatsColumns, - HistoryColumns { - - /** - * The content url for this table. - */ - public static final Uri CONTENT_URI = - Uri.parse("content://sync/history"); - - /** Enum value for a sync start event. */ - public static final int EVENT_START = 0; - - /** Enum value for a sync stop event. */ - public static final int EVENT_STOP = 1; - - // TODO: i18n -- grab these out of resources. - /** String names for the sync event types. */ - public static final String[] EVENTS = { "START", "STOP" }; - - /** Enum value for a server-initiated sync. */ - public static final int SOURCE_SERVER = 0; - - /** Enum value for a local-initiated sync. */ - public static final int SOURCE_LOCAL = 1; - /** - * Enum value for a poll-based sync (e.g., upon connection to - * network) - */ - public static final int SOURCE_POLL = 2; - - /** Enum value for a user-initiated sync. */ - public static final int SOURCE_USER = 3; - - // TODO: i18n -- grab these out of resources. - /** String names for the sync source types. */ - public static final String[] SOURCES = { "SERVER", - "LOCAL", - "POLL", - "USER" }; - - // Error types - public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1; - public static final int ERROR_AUTHENTICATION = 2; - public static final int ERROR_IO = 3; - public static final int ERROR_PARSE = 4; - public static final int ERROR_CONFLICT = 5; - public static final int ERROR_TOO_MANY_DELETIONS = 6; - public static final int ERROR_TOO_MANY_RETRIES = 7; - public static final int ERROR_INTERNAL = 8; - - // The MESG column will contain one of these or one of the Error types. - public static final String MESG_SUCCESS = "success"; - public static final String MESG_CANCELED = "canceled"; - - private static final String FINISHED_SINCE_WHERE_CLAUSE = EVENT + "=" + EVENT_STOP - + " AND " + EVENT_TIME + ">? AND " + ACCOUNT + "=? AND " + AUTHORITY + "=?"; - - public static String mesgToString(String mesg) { - if (MESG_SUCCESS.equals(mesg)) return mesg; - if (MESG_CANCELED.equals(mesg)) return mesg; - switch (Integer.parseInt(mesg)) { - case ERROR_SYNC_ALREADY_IN_PROGRESS: return "already in progress"; - case ERROR_AUTHENTICATION: return "bad authentication"; - case ERROR_IO: return "network error"; - case ERROR_PARSE: return "parse error"; - case ERROR_CONFLICT: return "conflict detected"; - case ERROR_TOO_MANY_DELETIONS: return "too many deletions"; - case ERROR_TOO_MANY_RETRIES: return "too many retries"; - case ERROR_INTERNAL: return "internal error"; - default: return "unknown error"; - } - } - - // utility class - private History() {} - - /** - * returns a cursor that queries the sync history in descending event time order - * @param contentResolver the ContentResolver to use for the query - * @return the cursor on the History table - */ - public static Cursor query(ContentResolver contentResolver) { - return contentResolver.query(CONTENT_URI, null, null, null, EVENT_TIME + " desc"); - } - - public static boolean hasNewerSyncFinished(ContentResolver contentResolver, - String account, String authority, long when) { - Cursor c = contentResolver.query(CONTENT_URI, new String[]{_ID}, - FINISHED_SINCE_WHERE_CLAUSE, - new String[]{Long.toString(when), account, authority}, null); - try { - return c.getCount() > 0; - } finally { - c.close(); - } - } - } - - /** - * Provides constants and utility methods to access and use the authority history - * table, which contains information about syncs aggregated by account and authority. - * All the HistoryColumns except for EVENT are present, plus the AuthorityHistoryColumns. - */ - public static class Status extends History implements StatusColumns { - - /** - * The content url for this table. - */ - public static final Uri CONTENT_URI = Uri.parse("content://sync/status"); - - // utility class - private Status() {} - - /** - * returns a cursor that queries the authority sync history in descending event order of - * ACCOUNT, AUTHORITY - * @param contentResolver the ContentResolver to use for the query - * @return the cursor on the AuthorityHistory table - */ - public static Cursor query(ContentResolver contentResolver) { - return contentResolver.query(CONTENT_URI, null, null, null, ACCOUNT + ", " + AUTHORITY); - } - - public static class QueryMap extends ContentQueryMap { - public QueryMap(ContentResolver contentResolver, - boolean keepUpdated, - Handler handlerForUpdateNotifications) { - super(contentResolver.query(CONTENT_URI, null, null, null, null), - _ID, keepUpdated, handlerForUpdateNotifications); - } - - public ContentValues get(String account, String authority) { - Map<String, ContentValues> rows = getRows(); - for (ContentValues values : rows.values()) { - if (values.getAsString(ACCOUNT).equals(account) - && values.getAsString(AUTHORITY).equals(authority)) { - return values; - } - } - return null; - } - } - } - - /** - * Provides constants and utility methods to access and use the pending syncs table - */ - public static final class Pending implements BaseColumns, - StatsColumns { - - /** - * The content url for this table. - */ - public static final Uri CONTENT_URI = Uri.parse("content://sync/pending"); - - // utility class - private Pending() {} - - public static class QueryMap extends ContentQueryMap { - public QueryMap(ContentResolver contentResolver, boolean keepUpdated, - Handler handlerForUpdateNotifications) { - super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated, - handlerForUpdateNotifications); - } - - public boolean isPending(String account, String authority) { - Map<String, ContentValues> rows = getRows(); - for (ContentValues values : rows.values()) { - if (values.getAsString(ACCOUNT).equals(account) - && values.getAsString(AUTHORITY).equals(authority)) { - return true; - } - } - return false; - } - } - } - - /** - * Columns from the history table. - */ - public interface ActiveColumns { - /** - * The wallclock time of when the active sync started. - * <P>Type: INTEGER</P> - */ - public static final String START_TIME = "startTime"; - } - - /** - * Provides constants and utility methods to access and use the pending syncs table - */ - public static final class Active implements BaseColumns, - StatsColumns, - ActiveColumns { - - /** - * The content url for this table. - */ - public static final Uri CONTENT_URI = Uri.parse("content://sync/active"); - - // utility class - private Active() {} - - public static class QueryMap extends ContentQueryMap { - public QueryMap(ContentResolver contentResolver, boolean keepUpdated, - Handler handlerForUpdateNotifications) { - super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated, - handlerForUpdateNotifications); - } - - public ContentValues getActiveSyncInfo() { - Map<String, ContentValues> rows = getRows(); - for (ContentValues values : rows.values()) { - return values; - } - return null; - } - - public String getSyncingAccount() { - ContentValues values = getActiveSyncInfo(); - return (values == null) ? null : values.getAsString(ACCOUNT); - } - - public String getSyncingAuthority() { - ContentValues values = getActiveSyncInfo(); - return (values == null) ? null : values.getAsString(AUTHORITY); - } - - public long getSyncStartTime() { - ContentValues values = getActiveSyncInfo(); - return (values == null) ? -1 : values.getAsLong(START_TIME); - } - } - } - - /** - * Columns in the settings table, which holds key/value pairs of settings. - */ - public interface SettingsColumns { - /** - * The key of the setting - * <P>Type: TEXT</P> - */ - public static final String KEY = "name"; - - /** - * The value of the settings - * <P>Type: TEXT</P> - */ - public static final String VALUE = "value"; - } - - /** - * Provides constants and utility methods to access and use the settings - * table. - */ - public static final class Settings implements BaseColumns, SettingsColumns { - /** - * The Uri of the settings table. This table behaves a little differently than - * normal tables. Updates are not allowed, only inserts, and inserts cause a replace - * to be performed, which first deletes the row if it is already present. - */ - public static final Uri CONTENT_URI = Uri.parse("content://sync/settings"); - - /** controls whether or not the device listens for sync tickles */ - public static final String SETTING_LISTEN_FOR_TICKLES = "listen_for_tickles"; - - /** controls whether or not the individual provider is synced when tickles are received */ - public static final String SETTING_SYNC_PROVIDER_PREFIX = "sync_provider_"; - - /** query column project */ - private static final String[] PROJECTION = { KEY, VALUE }; - - /** - * Convenience function for updating a single settings value as a - * boolean. This will either create a new entry in the table if the - * given name does not exist, or modify the value of the existing row - * with that name. Note that internally setting values are always - * stored as strings, so this function converts the given value to a - * string before storing it. - * - * @param contentResolver the ContentResolver to use to access the settings table - * @param name The name of the setting to modify. - * @param val The new value for the setting. - */ - static private void putBoolean(ContentResolver contentResolver, String name, boolean val) { - ContentValues values = new ContentValues(); - values.put(KEY, name); - values.put(VALUE, Boolean.toString(val)); - // this insert is translated into an update by the underlying Sync provider - contentResolver.insert(CONTENT_URI, values); - } - - /** - * Convenience function for getting a setting value as a boolean without using the - * QueryMap for light-weight setting querying. - * @param contentResolver The ContentResolver for querying the setting. - * @param name The name of the setting to query - * @param def The default value for the setting. - * @return The value of the setting. - */ - static public boolean getBoolean(ContentResolver contentResolver, - String name, boolean def) { - Cursor cursor = contentResolver.query( - CONTENT_URI, - PROJECTION, - KEY + "=?", - new String[] { name }, - null); - try { - if (cursor != null && cursor.moveToFirst()) { - return Boolean.parseBoolean(cursor.getString(1)); - } - } finally { - if (cursor != null) cursor.close(); - } - return def; - } - - /** - * A convenience method to set whether or not the provider is synced when - * it receives a network tickle. - * - * @param contentResolver the ContentResolver to use to access the settings table - * @param providerName the provider whose behavior is being controlled - * @param sync true if the provider should be synced when tickles are received for it - */ - static public void setSyncProviderAutomatically(ContentResolver contentResolver, - String providerName, boolean sync) { - putBoolean(contentResolver, SETTING_SYNC_PROVIDER_PREFIX + providerName, sync); - } - - /** - * A convenience method to set whether or not the device should listen to tickles. - * - * @param contentResolver the ContentResolver to use to access the settings table - * @param flag true if it should listen. - */ - static public void setListenForNetworkTickles(ContentResolver contentResolver, - boolean flag) { - putBoolean(contentResolver, SETTING_LISTEN_FOR_TICKLES, flag); - } - - public static class QueryMap extends ContentQueryMap { - private ContentResolver mContentResolver; - - public QueryMap(ContentResolver contentResolver, boolean keepUpdated, - Handler handlerForUpdateNotifications) { - super(contentResolver.query(CONTENT_URI, null, null, null, null), KEY, keepUpdated, - handlerForUpdateNotifications); - mContentResolver = contentResolver; - } - - /** - * Check if the provider should be synced when a network tickle is received - * @param providerName the provider whose setting we are querying - * @return true of the provider should be synced when a network tickle is received - */ - public boolean getSyncProviderAutomatically(String providerName) { - return getBoolean(SETTING_SYNC_PROVIDER_PREFIX + providerName, true); - } - - /** - * Set whether or not the provider is synced when it receives a network tickle. - * - * @param providerName the provider whose behavior is being controlled - * @param sync true if the provider should be synced when tickles are received for it - */ - public void setSyncProviderAutomatically(String providerName, boolean sync) { - Settings.setSyncProviderAutomatically(mContentResolver, providerName, sync); - } - - /** - * Set whether or not the device should listen for tickles. - * - * @param flag true if it should listen. - */ - public void setListenForNetworkTickles(boolean flag) { - Settings.setListenForNetworkTickles(mContentResolver, flag); - } - - /** - * Check if the device should listen to tickles. - - * @return true if it should - */ - public boolean getListenForNetworkTickles() { - return getBoolean(SETTING_LISTEN_FOR_TICKLES, true); - } - - /** - * Convenience function for retrieving a single settings value - * as a boolean. - * - * @param name The name of the setting to retrieve. - * @param def Value to return if the setting is not defined. - * @return The setting's current value, or 'def' if it is not defined. - */ - private boolean getBoolean(String name, boolean def) { - ContentValues values = getValues(name); - return values != null ? values.getAsBoolean(VALUE) : def; - } - } - } -} diff --git a/core/java/com/android/internal/os/AtomicFile.java b/core/java/com/android/internal/os/AtomicFile.java new file mode 100644 index 0000000..ca0345f --- /dev/null +++ b/core/java/com/android/internal/os/AtomicFile.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2009 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 com.android.internal.os; + +import android.os.FileUtils; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Helper class for performing atomic operations on a file, by creating a + * backup file until a write has successfully completed. + */ +public class AtomicFile { + private final File mBaseName; + private final File mBackupName; + + public AtomicFile(File baseName) { + mBaseName = baseName; + mBackupName = new File(baseName.getPath() + ".bak"); + } + + public File getBaseFile() { + return mBaseName; + } + + public FileOutputStream startWrite() throws IOException { + // Rename the current file so it may be used as a backup during the next read + if (mBaseName.exists()) { + if (!mBaseName.renameTo(mBackupName)) { + mBackupName.delete(); + if (!mBaseName.renameTo(mBackupName)) { + Log.w("AtomicFile", "Couldn't rename file " + mBaseName + + " to backup file " + mBackupName); + } + } + } + FileOutputStream str = null; + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e) { + File parent = mBaseName.getParentFile(); + if (!parent.mkdir()) { + throw new IOException("Couldn't create directory " + mBaseName); + } + FileUtils.setPermissions( + parent.getPath(), + FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, + -1, -1); + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e2) { + throw new IOException("Couldn't create " + mBaseName); + } + } + return str; + } + + public void finishWrite(FileOutputStream str) { + if (str != null) { + try { + str.close(); + mBackupName.delete(); + } catch (IOException e) { + Log.w("AtomicFile", "finishWrite: Got exception:", e); + } + } + } + + public void failWrite(FileOutputStream str) { + if (str != null) { + try { + str.close(); + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } catch (IOException e) { + Log.w("AtomicFile", "failWrite: Got exception:", e); + } + } + } + + public FileOutputStream openAppend() throws IOException { + try { + return new FileOutputStream(mBaseName, true); + } catch (FileNotFoundException e) { + throw new IOException("Couldn't append " + mBaseName); + } + } + + public void truncate() throws IOException { + try { + FileOutputStream fos = new FileOutputStream(mBaseName); + fos.close(); + } catch (FileNotFoundException e) { + throw new IOException("Couldn't append " + mBaseName); + } catch (IOException e) { + } + } + + public FileInputStream openRead() throws FileNotFoundException { + if (mBackupName.exists()) { + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } + return new FileInputStream(mBaseName); + } + + public byte[] readFully() throws IOException { + FileInputStream stream = openRead(); + try { + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length-pos); + //Log.i("foo", "Read " + amt + " bytes at " + pos + // + " of avail " + data.length); + if (amt <= 0) { + //Log.i("foo", "**** FINISHED READING: pos=" + pos + // + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length-pos) { + byte[] newData = new byte[pos+avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } finally { + stream.close(); + } + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index aa8fd74..7ebc896 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1012,9 +1012,6 @@ android:excludeFromRecents="true"> </activity> - <provider android:name=".content.SyncProvider" - android:authorities="sync" android:multiprocess="false" /> - <service android:name="com.android.server.LoadAverageService" android:exported="true" /> diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java index 281828f..760b6b5 100755 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java @@ -380,35 +380,35 @@ public class MediaNames { public static final String META_DATA_MP3 [][] = { {"/sdcard/media_api/metaDataTestMedias/MP3/ID3V1_ID3V2.mp3", "1/10", "ID3V2.3 Album", "ID3V2.3 Artist", "ID3V2.3 Lyricist", "ID3V2.3 Composer", null, "Blues", - "ID3V2.3 Title", "1234", "321", "1"}, + "ID3V2.3 Title", "1234", "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/ID3V2.mp3", "1/10", "ID3V2.3 Album", "ID3V2.3 Artist", "ID3V2.3 Lyricist", "ID3V2.3 Composer", null, "Blues", - "ID3V2.3 Title", "1234", "313", "1"}, - {"/sdcard/media_api/metaDataTestMedias/MP3/ID3V1.mp3", null, "test ID3V1 Album", "test ID3V1 Artist", - null, null, null, null, "test ID3V1 Title", "1234", "231332", "1"}, + "ID3V2.3 Title", "1234", "287", "1"}, + {"/sdcard/media_api/metaDataTestMedias/MP3/ID3V1.mp3", "1", "test ID3V1 Album", "test ID3V1 Artist", + null, null, null, "255", "test ID3V1 Title", "1234", "231332", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V1.mp3" , null, null, null, null, null, null, null, null, null, "231330", "1"}, //The corrupted TALB field in id3v2 would not switch to id3v1 tag automatically {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TALB.mp3", "01", null, "ID3V2.3 Artist", "ID3V2.3 Lyricist", "ID3V2.3 Composer", null, - "Blues", "ID3V2.3 Title", "1234", "321", "1"}, + "Blues", "ID3V2.3 Title", "1234", "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TCOM.mp3", "01", "ID3V2.3 Album", "ID3V2.3 Artist", "ID3V2.3 Lyricist", null, null, - "Blues", "ID3V2.3 Title", "1234", "321", "1"}, + "Blues", "ID3V2.3 Title", "1234", "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TCOM_2.mp3", "01", "ID3V2.3 Album", - "ID3V2.3 Artist", null, null, null, "Blues", "ID3V2.3 Title", "1234", "321", "1"}, + "ID3V2.3 Artist", null, null, null, "Blues", "ID3V2.3 Title", "1234", "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TRCK.mp3", "dd", "ID3V2.3 Album", "ID3V2.3 Artist", "ID3V2.3 Lyricist", "ID3V2.3 Composer", null, - "Blues", "ID3V2.3 Title", "1234", "321", "1"}, + "Blues", "ID3V2.3 Title", "1234", "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TRCK_2.mp3", "01", "ID3V2.3 Album", - "ID3V2.3 Artist", null, null, null, null, "ID3V2.3 Title", null, "321", "1"}, + "ID3V2.3 Artist", null, null, null, "255", "ID3V2.3 Title", "1234", "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TYER.mp3", "01", "ID3V2.3 Album", - "ID3V2.3 Artist", null, null, null, null, "ID3V2.3 Title", "9999", "321", "1"}, + "ID3V2.3 Artist", null, null, null, null, "ID3V2.3 Title", "9999", "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TYER_2.mp3", "01", "ID3V2.3 Album", "ID3V2.3 Artist", "ID3V2.3 Lyricist", "ID3V2.3 Composer", null, - "Blues", "ID3V2.3 Title", null, "321", "1"}, + "Blues", "ID3V2.3 Title", null, "295", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP3/Corrupted_ID3V2_TIT.mp3", null, null, null, - null, null, null, null, null, null, "577", "1"} + null, null, null, null, null, null, "295", "1"} }; public static final String META_DATA_OTHERS [][] = { @@ -432,7 +432,7 @@ public class MediaNames { null, null, "2005", "231180", "1"}, {"/sdcard/media_api/metaDataTestMedias/MP4/kung_fu_panda_h264.mp4", null, "mp4 album Kung Fu Panda", "mp4 artist Kung Fu Panda", null, null, "20080517T091451.000Z", - "Kung Fu Panda", "Kung Fu Panda", "2008", "5667840", "2"}, + "41", "Kung Fu Panda", "2008", "5667840", "2"}, {"/sdcard/media_api/metaDataTestMedias/OGG/Ring_Classic_02.ogg", null, "Suspended Animation", "John Petrucci", null, null, "20070510T125223.000Z", null, null, "2005", "231180", "1"}, diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CodecTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CodecTest.java index caba47c..d9e17ea 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CodecTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CodecTest.java @@ -43,11 +43,13 @@ public class CodecTest { private static MediaPlayer mMediaPlayer; private MediaPlayer.OnPreparedListener mOnPreparedListener; - private static int WAIT_FOR_COMMAND_TO_COMPLETE = 10000; //10 seconds max. + private static int WAIT_FOR_COMMAND_TO_COMPLETE = 60000; //1 min max. private static boolean mInitialized = false; + private static boolean mPrepareReset = false; private static Looper mLooper = null; private static final Object lock = new Object(); private static final Object prepareDone = new Object(); + private static final Object videoSizeChanged = new Object(); private static boolean onPrepareSuccess = false; @@ -227,28 +229,84 @@ public class CodecTest { mp.pause(); mp.release(); } + + static MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener = + new MediaPlayer.OnVideoSizeChangedListener() { + public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { + synchronized (videoSizeChanged) { + Log.v(TAG, "sizechanged notification received ..."); + videoSizeChanged.notify(); + } + } + }; + //Register the videoSizeChanged listener public static int videoHeight(String filePath) throws Exception { Log.v(TAG, "videoHeight - " + filePath); - int videoHeight = 0; - MediaPlayer mp = new MediaPlayer(); - mp.setDataSource(filePath); - mp.setDisplay(MediaFrameworkTest.mSurfaceView.getHolder()); - mp.prepare(); - videoHeight = mp.getVideoHeight(); - mp.release(); + int videoHeight = 0; + synchronized (lock) { + initializeMessageLooper(); + try { + lock.wait(WAIT_FOR_COMMAND_TO_COMPLETE); + } catch(Exception e) { + Log.v(TAG, "looper was interrupted."); + return 0; + } + } + try { + mMediaPlayer.setDataSource(filePath); + mMediaPlayer.setDisplay(MediaFrameworkTest.mSurfaceView.getHolder()); + mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener); + synchronized (videoSizeChanged) { + try { + mMediaPlayer.prepare(); + mMediaPlayer.start(); + videoSizeChanged.wait(WAIT_FOR_COMMAND_TO_COMPLETE); + } catch (Exception e) { + Log.v(TAG, "wait was interrupted"); + } + } + videoHeight = mMediaPlayer.getVideoHeight(); + terminateMessageLooper(); + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + } + return videoHeight; } + //Register the videoSizeChanged listener public static int videoWidth(String filePath) throws Exception { Log.v(TAG, "videoWidth - " + filePath); int videoWidth = 0; - MediaPlayer mp = new MediaPlayer(); - mp.setDataSource(filePath); - mp.setDisplay(MediaFrameworkTest.mSurfaceView.getHolder()); - mp.prepare(); - videoWidth = mp.getVideoWidth(); - mp.release(); + + synchronized (lock) { + initializeMessageLooper(); + try { + lock.wait(WAIT_FOR_COMMAND_TO_COMPLETE); + } catch(Exception e) { + Log.v(TAG, "looper was interrupted."); + return 0; + } + } + try { + mMediaPlayer.setDataSource(filePath); + mMediaPlayer.setDisplay(MediaFrameworkTest.mSurfaceView.getHolder()); + mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener); + synchronized (videoSizeChanged) { + try { + mMediaPlayer.prepare(); + mMediaPlayer.start(); + videoSizeChanged.wait(WAIT_FOR_COMMAND_TO_COMPLETE); + } catch (Exception e) { + Log.v(TAG, "wait was interrupted"); + } + } + videoWidth = mMediaPlayer.getVideoWidth(); + terminateMessageLooper(); + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + } return videoWidth; } @@ -622,6 +680,10 @@ public class CodecTest { static MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { public void onPrepared(MediaPlayer mp) { synchronized (prepareDone) { + if(mPrepareReset){ + Log.v(TAG, "call Reset"); + mMediaPlayer.reset(); + } Log.v(TAG, "notify the prepare callback"); prepareDone.notify(); onPrepareSuccess = true; @@ -629,13 +691,15 @@ public class CodecTest { } }; - public static boolean prepareAsyncCallback(String filePath) throws Exception { - int videoWidth = 0; - int videoHeight = 0; - boolean checkVideoDimension = false; + public static boolean prepareAsyncCallback(String filePath, boolean reset) throws Exception { + //Added the PrepareReset flag which allow us to switch to different + //test case. + if (reset){ + mPrepareReset = true; + } - initializeMessageLooper(); synchronized (lock) { + initializeMessageLooper(); try { lock.wait(WAIT_FOR_COMMAND_TO_COMPLETE); } catch(Exception e) { @@ -651,14 +715,10 @@ public class CodecTest { synchronized (prepareDone) { try { prepareDone.wait(WAIT_FOR_COMMAND_TO_COMPLETE); - Log.v(TAG, "setPreview done"); } catch (Exception e) { Log.v(TAG, "wait was interrupted."); } - } - videoWidth = mMediaPlayer.getVideoWidth(); - videoHeight = mMediaPlayer.getVideoHeight(); - + } terminateMessageLooper(); }catch (Exception e){ Log.v(TAG,e.getMessage()); @@ -666,7 +726,7 @@ public class CodecTest { return onPrepareSuccess; } - + } diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaPlayerApiTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaPlayerApiTest.java index ee6a727..cfcc521 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaPlayerApiTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaPlayerApiTest.java @@ -432,14 +432,21 @@ public class MediaPlayerApiTest extends ActivityInstrumentationTestCase<MediaFra @LargeTest public void testLocalMp3PrepareAsyncCallback() throws Exception { boolean onPrepareSuccess = - CodecTest.prepareAsyncCallback(MediaNames.VIDEO_H263_AMR); + CodecTest.prepareAsyncCallback(MediaNames.VIDEO_H263_AMR, false); assertTrue("LocalMp3prepareAsyncCallback", onPrepareSuccess); } @LargeTest public void testStreamPrepareAsyncCallback() throws Exception { boolean onPrepareSuccess = - CodecTest.prepareAsyncCallback(MediaNames.STREAM_H264_480_360_1411k); + CodecTest.prepareAsyncCallback(MediaNames.STREAM_H264_480_360_1411k, false); + assertTrue("StreamH264PrepareAsyncCallback", onPrepareSuccess); + } + + @LargeTest + public void testStreamPrepareAsyncCallbackReset() throws Exception { + boolean onPrepareSuccess = + CodecTest.prepareAsyncCallback(MediaNames.STREAM_H264_480_360_1411k, true); assertTrue("StreamH264PrepareAsyncCallback", onPrepareSuccess); } } diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaRecorderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaRecorderTest.java index 261b4f4..ef0a3b1 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaRecorderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaRecorderTest.java @@ -148,8 +148,8 @@ public class MediaRecorderTest extends ActivityInstrumentationTestCase<MediaFram Log.v(TAG, "before getduration"); mOutputDuration = mediaPlayer.getDuration(); Log.v(TAG, "get video dimension"); - mOutputVideoHeight = mediaPlayer.getVideoHeight(); - mOutputVideoWidth = mediaPlayer.getVideoWidth(); + mOutputVideoHeight = CodecTest.videoHeight(outputFilePath); + mOutputVideoWidth = CodecTest.videoWidth(outputFilePath); mediaPlayer.release(); } catch (Exception e) { Log.v(TAG, e.toString()); diff --git a/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java b/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java index df599c7..8b3bedf 100644 --- a/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java +++ b/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java @@ -9,7 +9,6 @@ import android.util.Log; import android.util.Config; import android.util.EventLog; import android.app.IntentService; -import android.provider.Sync; import android.provider.SubscribedFeeds; import android.provider.SyncConstValue; import android.database.Cursor; @@ -17,7 +16,7 @@ import android.database.sqlite.SQLiteFullException; import android.app.AlarmManager; import android.app.PendingIntent; import android.os.Bundle; -import android.os.Debug; +import android.os.RemoteException; import android.text.TextUtils; import android.net.Uri; @@ -105,10 +104,6 @@ public class SubscribedFeedsIntentService extends IntentService { private void handleTickle(Context context, String account, String feed) { Cursor c = null; - Sync.Settings.QueryMap syncSettings = - new Sync.Settings.QueryMap(context.getContentResolver(), - false /* don't keep updated */, - null /* not needed since keep updated is false */); final String where = SubscribedFeeds.Feeds._SYNC_ACCOUNT + "= ? " + "and " + SubscribedFeeds.Feeds.FEED + "= ?"; try { @@ -124,9 +119,14 @@ public class SubscribedFeedsIntentService extends IntentService { String authority = c.getString(c.getColumnIndexOrThrow( SubscribedFeeds.Feeds.AUTHORITY)); EventLog.writeEvent(LOG_TICKLE, authority); - if (!syncSettings.getSyncProviderAutomatically(authority)) { - Log.d(TAG, "supressing tickle since provider " + authority - + " is configured to not sync automatically"); + try { + if (!ContentResolver.getContentService() + .getSyncProviderAutomatically(authority)) { + Log.d(TAG, "supressing tickle since provider " + authority + + " is configured to not sync automatically"); + continue; + } + } catch (RemoteException e) { continue; } Uri uri = Uri.parse("content://" + authority); @@ -137,7 +137,6 @@ public class SubscribedFeedsIntentService extends IntentService { } } finally { if (c != null) c.deactivate(); - syncSettings.close(); } } diff --git a/preloaded-classes b/preloaded-classes index 7d719e9..0520e41 100644 --- a/preloaded-classes +++ b/preloaded-classes @@ -76,8 +76,6 @@ android.content.ContentQueryMap android.content.ContentQueryMap$1 android.content.ContentResolver android.content.ContentResolver$CursorWrapperInner -android.content.ContentServiceNative -android.content.ContentServiceProxy android.content.ContentValues android.content.Context android.content.ContextWrapper @@ -85,6 +83,8 @@ android.content.DialogInterface android.content.DialogInterface$OnCancelListener android.content.DialogInterface$OnDismissListener android.content.IContentProvider +android.content.IContentService +android.content.IContentService$Stub android.content.Intent android.content.Intent$1 android.content.IntentFilter diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java index f5f3561..04cd53f 100644 --- a/services/java/com/android/server/BackupManagerService.java +++ b/services/java/com/android/server/BackupManagerService.java @@ -62,6 +62,7 @@ class BackupManagerService extends IBackupManager.Stub { private final Object mQueueLock = new Object(); private File mStateDir; + private File mDataDir; // ----- Handler that runs the actual backup process asynchronously ----- @@ -106,28 +107,41 @@ class BackupManagerService extends IBackupManager.Stub { Log.d(TAG, "invoking doBackup() on " + backupIntent); File savedStateName = new File(mStateDir, service.packageName); - File backupDataName = new File(mStateDir, service.packageName + ".data"); + File backupDataName = new File(mDataDir, service.packageName + ".data"); File newStateName = new File(mStateDir, service.packageName + ".new"); ParcelFileDescriptor savedState = ParcelFileDescriptor.open(savedStateName, ParcelFileDescriptor.MODE_READ_ONLY | ParcelFileDescriptor.MODE_CREATE); + + backupDataName.delete(); ParcelFileDescriptor backupData = ParcelFileDescriptor.open(backupDataName, ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE); + + newStateName.delete(); ParcelFileDescriptor newState = ParcelFileDescriptor.open(newStateName, ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE); - mTargetService.doBackup(savedState, backupData, newState); + // Run the target's backup pass + try { + mTargetService.doBackup(savedState, backupData, newState); + } finally { + savedState.close(); + backupData.close(); + newState.close(); + } // !!! TODO: Now propagate the newly-backed-up data to the transport - // !!! TODO: After successful transport, juggle the files so that - // next time the new state is used as the old state + // !!! TODO: After successful transport, delete the now-stale data + // and juggle the files so that next time the new state is passed + backupDataName.delete(); + newStateName.renameTo(savedStateName); } catch (FileNotFoundException fnf) { Log.d(TAG, "File not found on backup: "); @@ -173,9 +187,9 @@ class BackupManagerService extends IBackupManager.Stub { mPackageManager = context.getPackageManager(); // Set up our bookkeeping - File dataDir = Environment.getDataDirectory(); - mStateDir = new File(dataDir, "backup"); + mStateDir = new File(Environment.getDataDirectory(), "backup"); mStateDir.mkdirs(); + mDataDir = Environment.getDownloadCacheDirectory(); // Identify the backup participants // !!! TODO: also watch package-install to keep this up to date diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java index 760988d..493bd09 100644 --- a/services/java/com/android/server/ConnectivityService.java +++ b/services/java/com/android/server/ConnectivityService.java @@ -35,7 +35,6 @@ import android.os.Message; import android.os.ServiceManager; import android.os.SystemProperties; import android.provider.Settings; -import android.provider.Sync; import android.util.EventLog; import android.util.Log; diff --git a/services/jni/com_android_server_BatteryService.cpp b/services/jni/com_android_server_BatteryService.cpp index 6636a97..2524966 100644 --- a/services/jni/com_android_server_BatteryService.cpp +++ b/services/jni/com_android_server_BatteryService.cpp @@ -186,6 +186,9 @@ static void android_server_BatteryService_update(JNIEnv* env, jobject obj) if (readFromFile(BATTERY_STATUS_PATH, buf, SIZE) > 0) env->SetIntField(obj, gFieldIds.mBatteryStatus, getBatteryStatus(buf)); + else + env->SetIntField(obj, gFieldIds.mBatteryStatus, + gConstants.statusUnknown); if (readFromFile(BATTERY_HEALTH_PATH, buf, SIZE) > 0) env->SetIntField(obj, gFieldIds.mBatteryHealth, getBatteryHealth(buf)); diff --git a/test-runner/android/test/SyncBaseInstrumentation.java b/test-runner/android/test/SyncBaseInstrumentation.java index c1d2507..772d75c 100644 --- a/test-runner/android/test/SyncBaseInstrumentation.java +++ b/test-runner/android/test/SyncBaseInstrumentation.java @@ -18,12 +18,10 @@ package android.test; import android.content.ContentResolver; import android.content.Context; -import android.content.ContentValues; import android.os.Bundle; +import android.os.RemoteException; import android.os.SystemClock; -import android.provider.Sync; import android.net.Uri; -import java.util.Map; /** * If you would like to test sync a single provider with an @@ -75,11 +73,11 @@ public class SyncBaseInstrumentation extends InstrumentationTestCase { } protected void cancelSyncsandDisableAutoSync() { - Sync.Settings.QueryMap mSyncSettings = - new Sync.Settings.QueryMap(mContentResolver, true, null); - mSyncSettings.setListenForNetworkTickles(false); + try { + ContentResolver.getContentService().setListenForNetworkTickles(false); + } catch (RemoteException e) { + } mContentResolver.cancelSync(null); - mSyncSettings.close(); } /** @@ -88,34 +86,11 @@ public class SyncBaseInstrumentation extends InstrumentationTestCase { * @return */ private boolean isSyncActive(String account, String authority) { - Sync.Pending.QueryMap pendingQueryMap = null; - Sync.Active.QueryMap activeQueryMap = null; try { - pendingQueryMap = new Sync.Pending.QueryMap(mContentResolver, false, null); - activeQueryMap = new Sync.Active.QueryMap(mContentResolver, false, null); - - if (pendingQueryMap.isPending(account, authority)) { - return true; - } - if (isActiveInActiveQueryMap(activeQueryMap, account, authority)) { - return true; - } + return ContentResolver.getContentService().isSyncActive(account, + authority); + } catch (RemoteException e) { return false; - } finally { - activeQueryMap.close(); - pendingQueryMap.close(); - } - } - - private boolean isActiveInActiveQueryMap(Sync.Active.QueryMap activemap, String account, - String authority) { - Map<String, ContentValues> rows = activemap.getRows(); - for (ContentValues values : rows.values()) { - if (values.getAsString("account").equals(account) - && values.getAsString("authority").equals(authority)) { - return true; - } } - return false; } } diff --git a/tests/CoreTests/android/content/SyncStorageEngineTest.java b/tests/CoreTests/android/content/SyncStorageEngineTest.java index 36805b1..dee6e38 100644 --- a/tests/CoreTests/android/content/SyncStorageEngineTest.java +++ b/tests/CoreTests/android/content/SyncStorageEngineTest.java @@ -20,7 +20,6 @@ import android.test.AndroidTestCase; import android.test.RenamingDelegatingContext; import android.test.mock.MockContext; import android.test.mock.MockContentResolver; -import android.provider.Sync; public class SyncStorageEngineTest extends AndroidTestCase { @@ -39,7 +38,7 @@ public class SyncStorageEngineTest extends AndroidTestCase { long time0 = 1000; long historyId = engine.insertStartSyncEvent( - account, authority, time0, Sync.History.SOURCE_LOCAL); + account, authority, time0, SyncStorageEngine.SOURCE_LOCAL); long time1 = time0 + SyncStorageEngine.MILLIS_IN_4WEEKS * 2; engine.stopSyncEvent(historyId, time1 - time0, "yay", 0, 0); } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java index 727d6f2..cfab90a 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java @@ -17,7 +17,6 @@ package com.android.layoutlib.bridge; import android.content.ContentResolver; -import android.content.ContentServiceNative; import android.content.Context; import android.content.IContentProvider; import android.database.ContentObserver; @@ -51,9 +50,6 @@ public class BridgeContentResolver extends ContentResolver { /** * Stub for the layoutlib bridge content resolver. - * <p/> - * The super implementation accesses the {@link ContentServiceNative#getDefault()} - * which returns null and would make the call crash. Instead we do nothing. */ @Override public void registerContentObserver(Uri uri, boolean notifyForDescendents, @@ -63,9 +59,6 @@ public class BridgeContentResolver extends ContentResolver { /** * Stub for the layoutlib bridge content resolver. - * <p/> - * The super implementation accesses the {@link ContentServiceNative#getDefault()} - * which returns null and would make the call crash. Instead we do nothing. */ @Override public void unregisterContentObserver(ContentObserver observer) { @@ -74,9 +67,6 @@ public class BridgeContentResolver extends ContentResolver { /** * Stub for the layoutlib bridge content resolver. - * <p/> - * The super implementation accesses the {@link ContentServiceNative#getDefault()} - * which returns null and would make the call crash. Instead we do nothing. */ @Override public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { @@ -85,9 +75,6 @@ public class BridgeContentResolver extends ContentResolver { /** * Stub for the layoutlib bridge content resolver. - * <p/> - * The super implementation accesses the {@link ContentServiceNative#getDefault()} - * which returns null and would make the call crash. Instead we do nothing. */ @Override public void startSync(Uri uri, Bundle extras) { @@ -96,9 +83,6 @@ public class BridgeContentResolver extends ContentResolver { /** * Stub for the layoutlib bridge content resolver. - * <p/> - * The super implementation accesses the {@link ContentServiceNative#getDefault()} - * which returns null and would make the call crash. Instead we do nothing. */ @Override public void cancelSync(Uri uri) { |