diff --git a/apps/Development/AndroidManifest.xml b/apps/Development/AndroidManifest.xml
index 0bdd26e75..7f0f5944d 100644
--- a/apps/Development/AndroidManifest.xml
+++ b/apps/Development/AndroidManifest.xml
@@ -84,6 +84,14 @@
+
+
+
+
+
+
+
diff --git a/apps/Development/res/layout/account_list_view.xml b/apps/Development/res/layout/account_list_view.xml
new file mode 100644
index 000000000..d25567266
--- /dev/null
+++ b/apps/Development/res/layout/account_list_view.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/apps/Development/res/layout/sync_adapter_driver.xml b/apps/Development/res/layout/sync_adapter_driver.xml
new file mode 100644
index 000000000..58c0ebb34
--- /dev/null
+++ b/apps/Development/res/layout/sync_adapter_driver.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/Development/res/layout/sync_adapter_item.xml b/apps/Development/res/layout/sync_adapter_item.xml
new file mode 100644
index 000000000..d818c789a
--- /dev/null
+++ b/apps/Development/res/layout/sync_adapter_item.xml
@@ -0,0 +1,26 @@
+
+
+
diff --git a/apps/Development/res/values/strings.xml b/apps/Development/res/values/strings.xml
index ad70fbede..c8a3f8b85 100644
--- a/apps/Development/res/values/strings.xml
+++ b/apps/Development/res/values/strings.xml
@@ -155,4 +155,25 @@
Properties
authtoken type:
features:
+
+
+ bind
+ unbind
+ start sync
+ cancel sync
+ Registered Sync Adapters:
+ Starting a sync of account %s...
+ Got a RemoteException while starting the sync
+ Canceled the sync
+ Got a RemoteException while canceling the sync
+ Received heartbeat
+ Sync failed: %s
+ Sync succeeded: %s
+ Already bound to sync adapter
+ No selected sync adapter
+ Connected to Sync Adapter\n\tauthority: %s\n\taccount type: %s
+ Not connected to a sync adapter
+ Bind failed
+ Waiting for service to be connected...
+ Select account to sync
diff --git a/apps/Development/src/com/android/development/SyncAdapterDriver.java b/apps/Development/src/com/android/development/SyncAdapterDriver.java
new file mode 100644
index 000000000..eab37ecfa
--- /dev/null
+++ b/apps/Development/src/com/android/development/SyncAdapterDriver.java
@@ -0,0 +1,373 @@
+/*
+ * 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.development;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.Dialog;
+import android.app.AlertDialog;
+import android.content.res.TypedArray;
+import android.content.pm.RegisteredServicesCache;
+import android.content.pm.RegisteredServicesCacheListener;
+import android.content.SyncAdapterType;
+import android.content.ISyncAdapter;
+import android.content.ISyncContext;
+import android.content.ServiceConnection;
+import android.content.ComponentName;
+import android.content.SyncResult;
+import android.content.Intent;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.widget.ArrayAdapter;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.ListView;
+import android.util.AttributeSet;
+import android.provider.Settings;
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.view.View;
+import android.view.LayoutInflater;
+
+import java.util.Collection;
+
+public class SyncAdapterDriver extends Activity
+ implements RegisteredServicesCacheListener, AdapterView.OnItemClickListener {
+ private Spinner mSyncAdapterSpinner;
+
+ private Button mBindButton;
+ private Button mUnbindButton;
+ private TextView mBoundAdapterTextView;
+ private Button mStartSyncButton;
+ private Button mCancelSyncButton;
+ private TextView mStatusTextView;
+ private Object[] mSyncAdapters;
+ private SyncAdaptersCache mSyncAdaptersCache;
+ private final Object mSyncAdaptersLock = new Object();
+
+ private static final int DIALOG_ID_PICK_ACCOUNT = 1;
+ private ListView mAccountPickerView = null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSyncAdaptersCache = new SyncAdaptersCache(this);
+ setContentView(R.layout.sync_adapter_driver);
+
+ mSyncAdapterSpinner = (Spinner) findViewById(R.id.sync_adapters_spinner);
+ mBindButton = (Button) findViewById(R.id.bind_button);
+ mUnbindButton = (Button) findViewById(R.id.unbind_button);
+ mBoundAdapterTextView = (TextView) findViewById(R.id.bound_adapter_text_view);
+
+ mStartSyncButton = (Button) findViewById(R.id.start_sync_button);
+ mCancelSyncButton = (Button) findViewById(R.id.cancel_sync_button);
+
+ mStatusTextView = (TextView) findViewById(R.id.status_text_view);
+
+ getSyncAdapters();
+ mSyncAdaptersCache.setListener(this);
+ }
+
+ protected void onDestroy() {
+ mSyncAdaptersCache.close();
+ super.onDestroy();
+ }
+
+ private void getSyncAdapters() {
+ Collection> all =
+ mSyncAdaptersCache.getAllServices();
+ synchronized (mSyncAdaptersLock) {
+ mSyncAdapters = new Object[all.size()];
+ String[] names = new String[mSyncAdapters.length];
+ int i = 0;
+ for (RegisteredServicesCache.ServiceInfo item : all) {
+ mSyncAdapters[i] = item;
+ names[i] = item.type.authority + " - " + item.type.accountType;
+ i++;
+ }
+
+ ArrayAdapter adapter =
+ new ArrayAdapter(this,
+ R.layout.sync_adapter_item, names);
+ mSyncAdapterSpinner.setAdapter(adapter);
+ }
+ }
+
+ void updateUi() {
+ boolean isBound;
+ boolean hasServiceConnection;
+ synchronized (mServiceConnectionLock) {
+ hasServiceConnection = mActiveServiceConnection != null;
+ isBound = hasServiceConnection && mActiveServiceConnection.mBoundSyncAdapter != null;
+ }
+ mStartSyncButton.setEnabled(isBound);
+ mCancelSyncButton.setEnabled(isBound);
+ mBindButton.setEnabled(!hasServiceConnection);
+ mUnbindButton.setEnabled(hasServiceConnection);
+ }
+
+ public void startSyncSelected(View view) {
+ synchronized (mServiceConnectionLock) {
+ ISyncAdapter syncAdapter = null;
+ if (mActiveServiceConnection != null) {
+ syncAdapter = mActiveServiceConnection.mBoundSyncAdapter;
+ }
+
+ if (syncAdapter != null) {
+ removeDialog(DIALOG_ID_PICK_ACCOUNT);
+
+ mAccountPickerView = (ListView) LayoutInflater.from(this).inflate(
+ R.layout.account_list_view, null);
+ mAccountPickerView.setOnItemClickListener(this);
+ Account accounts[] = AccountManager.get(this).getAccountsByType(
+ mActiveServiceConnection.mSyncAdapter.type.accountType);
+ String[] accountNames = new String[accounts.length];
+ for (int i = 0; i < accounts.length; i++) {
+ accountNames[i] = accounts[i].name;
+ }
+ ArrayAdapter adapter =
+ new ArrayAdapter(SyncAdapterDriver.this,
+ android.R.layout.simple_list_item_1, accountNames);
+ mAccountPickerView.setAdapter(adapter);
+
+ showDialog(DIALOG_ID_PICK_ACCOUNT);
+ }
+ }
+ updateUi();
+ }
+
+ private void startSync(String accountName) {
+ synchronized (mServiceConnectionLock) {
+ ISyncAdapter syncAdapter = null;
+ if (mActiveServiceConnection != null) {
+ syncAdapter = mActiveServiceConnection.mBoundSyncAdapter;
+ }
+
+ if (syncAdapter != null) {
+ try {
+ mStatusTextView.setText(
+ getString(R.string.status_starting_sync_format, accountName));
+ Account account = new Account(accountName,
+ mActiveServiceConnection.mSyncAdapter.type.accountType);
+ syncAdapter.startSync(mActiveServiceConnection,
+ mActiveServiceConnection.mSyncAdapter.type.authority,
+ account, new Bundle());
+ } catch (RemoteException e) {
+ mStatusTextView.setText(
+ getString(R.string.status_remote_exception_while_starting_sync));
+ }
+ }
+ }
+ updateUi();
+ }
+
+ public void cancelSync(View view) {
+ synchronized (mServiceConnectionLock) {
+ ISyncAdapter syncAdapter = null;
+ if (mActiveServiceConnection != null) {
+ syncAdapter = mActiveServiceConnection.mBoundSyncAdapter;
+ }
+
+ if (syncAdapter != null) {
+ try {
+ mStatusTextView.setText(getString(R.string.status_canceled_sync));
+ syncAdapter.cancelSync(mActiveServiceConnection);
+ } catch (RemoteException e) {
+ mStatusTextView.setText(
+ getString(R.string.status_remote_exception_while_canceling_sync));
+ }
+ }
+ }
+ updateUi();
+ }
+
+ public void onRegisteredServicesCacheChanged() {
+ getSyncAdapters();
+ }
+
+ @Override
+ protected Dialog onCreateDialog(final int id) {
+ if (id == DIALOG_ID_PICK_ACCOUNT) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.select_account_to_sync);
+ builder.setInverseBackgroundForced(true);
+ builder.setView(mAccountPickerView);
+ return builder.create();
+ }
+ return super.onCreateDialog(id);
+ }
+
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ TextView item = (TextView) view;
+ final String accountName = item.getText().toString();
+ dismissDialog(DIALOG_ID_PICK_ACCOUNT);
+ startSync(accountName);
+ }
+
+ private class MyServiceConnection extends ISyncContext.Stub implements ServiceConnection {
+ private volatile ISyncAdapter mBoundSyncAdapter;
+ final RegisteredServicesCache.ServiceInfo mSyncAdapter;
+
+ public MyServiceConnection(
+ RegisteredServicesCache.ServiceInfo syncAdapter) {
+ mSyncAdapter = syncAdapter;
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mBoundSyncAdapter = ISyncAdapter.Stub.asInterface(service);
+ final SyncAdapterType type = mActiveServiceConnection.mSyncAdapter.type;
+ mBoundAdapterTextView.setText(getString(R.string.binding_connected_format,
+ type.authority, type.accountType));
+ updateUi();
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ mBoundAdapterTextView.setText(getString(R.string.binding_not_connected));
+ mBoundSyncAdapter = null;
+ updateUi();
+ }
+
+ public void sendHeartbeat() {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ uiThreadSendHeartbeat();
+ }
+ });
+ }
+
+ public void uiThreadSendHeartbeat() {
+ mStatusTextView.setText(getString(R.string.status_received_heartbeat));
+ }
+
+ public void uiThreadOnFinished(SyncResult result) {
+ if (result.hasError()) {
+ mStatusTextView.setText(
+ getString(R.string.status_sync_failed_format, result.toString()));
+ } else {
+ mStatusTextView.setText(
+ getString(R.string.status_sync_succeeded_format, result.toString()));
+ }
+ }
+
+ public void onFinished(final SyncResult result) throws RemoteException {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ uiThreadOnFinished(result);
+ }
+ });
+ }
+ }
+
+ final Object mServiceConnectionLock = new Object();
+ MyServiceConnection mActiveServiceConnection;
+
+ public void initiateBind(View view) {
+ synchronized (mServiceConnectionLock) {
+ if (mActiveServiceConnection != null) {
+ mStatusTextView.setText(getString(R.string.status_already_bound));
+ return;
+ }
+
+ RegisteredServicesCache.ServiceInfo syncAdapter =
+ getSelectedSyncAdapter();
+ if (syncAdapter == null) {
+ mStatusTextView.setText(getString(R.string.status_sync_adapter_not_selected));
+ return;
+ }
+
+ mActiveServiceConnection = new MyServiceConnection(syncAdapter);
+
+ Intent intent = new Intent();
+ intent.setAction("android.content.SyncAdapter");
+ intent.setComponent(syncAdapter.componentName);
+ intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
+ com.android.internal.R.string.sync_binding_label);
+ intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
+ this, 0, new Intent(Settings.ACTION_SYNC_SETTINGS), 0));
+ if (!bindService(intent, mActiveServiceConnection, Context.BIND_AUTO_CREATE)) {
+ mBoundAdapterTextView.setText(getString(R.string.binding_bind_failed));
+ mActiveServiceConnection = null;
+ return;
+ }
+ mBoundAdapterTextView.setText(getString(R.string.binding_waiting_for_connection));
+ }
+ updateUi();
+ }
+
+ public void initiateUnbind(View view) {
+ synchronized (mServiceConnectionLock) {
+ if (mActiveServiceConnection == null) {
+ return;
+ }
+ mBoundAdapterTextView.setText("");
+ unbindService(mActiveServiceConnection);
+ mActiveServiceConnection = null;
+ }
+ updateUi();
+ }
+
+ private RegisteredServicesCache.ServiceInfo getSelectedSyncAdapter() {
+ synchronized (mSyncAdaptersLock) {
+ final int position = mSyncAdapterSpinner.getSelectedItemPosition();
+ if (position == AdapterView.INVALID_POSITION) {
+ return null;
+ }
+ try {
+ //noinspection unchecked
+ return (RegisteredServicesCache.ServiceInfo)
+ mSyncAdapters[position];
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ }
+
+ static class SyncAdaptersCache extends RegisteredServicesCache {
+ private static final String SERVICE_INTERFACE = "android.content.SyncAdapter";
+ private static final String SERVICE_META_DATA = "android.content.SyncAdapter";
+ private static final String ATTRIBUTES_NAME = "sync-adapter";
+
+ SyncAdaptersCache(Context context) {
+ super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME);
+ }
+
+ public SyncAdapterType parseServiceAttributes(String packageName, AttributeSet attrs) {
+ TypedArray sa = mContext.getResources().obtainAttributes(attrs,
+ com.android.internal.R.styleable.SyncAdapter);
+ try {
+ final String authority =
+ sa.getString(com.android.internal.R.styleable.SyncAdapter_contentAuthority);
+ final String accountType =
+ sa.getString(com.android.internal.R.styleable.SyncAdapter_accountType);
+ if (authority == null || accountType == null) {
+ return null;
+ }
+ final boolean userVisible = sa.getBoolean(
+ com.android.internal.R.styleable.SyncAdapter_userVisible, true);
+ final boolean supportsUploading = sa.getBoolean(
+ com.android.internal.R.styleable.SyncAdapter_supportsUploading, true);
+ return new SyncAdapterType(authority, accountType, userVisible, supportsUploading);
+ } finally {
+ sa.recycle();
+ }
+ }
+ }
+}