create a test app that allows direct control of a sync adapter for testing purposes http://b/issue?id=2239791
diff --git a/apps/Development/AndroidManifest.xml b/apps/Development/AndroidManifest.xml index 0bdd26e..7f0f594 100644 --- a/apps/Development/AndroidManifest.xml +++ b/apps/Development/AndroidManifest.xml
@@ -84,6 +84,14 @@ </intent-filter> </activity> + <activity android:name="SyncAdapterDriver" android:label="Sync Tester" + android:theme="@android:style/Theme.Light"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.TEST" /> + </intent-filter> + </activity> + <activity android:name="DataList"> </activity> <activity android:name="Details">
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 0000000..d255672 --- /dev/null +++ b/apps/Development/res/layout/account_list_view.xml
@@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<ListView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent"/>
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 0000000..58c0ebb --- /dev/null +++ b/apps/Development/res/layout/sync_adapter_driver.xml
@@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <LinearLayout android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + <TextView android:id="@+id/sync_adapters_spinner_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="22dip" + android:text="@string/sync_adapters_spinner_label"/> + + <Spinner android:id="@+id/sync_adapters_spinner" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + <LinearLayout + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="52dip"> + <Button + android:id="@+id/bind_button" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:text="@string/bind_button" + android:onClick="initiateBind" + android:layout_weight="2"/> + + <Button + android:id="@+id/unbind_button" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:text="@string/unbind_button" + android:onClick="initiateUnbind" + android:layout_weight="2"/> + </LinearLayout> + + <TextView android:id="@+id/bound_adapter_text_view" + android:layout_width="wrap_content" + android:textSize="20dip" + android:layout_height="wrap_content"/> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="52dip"> + <Button + android:id="@+id/start_sync_button" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:text="@string/start_sync_button" + android:onClick="startSyncSelected" + android:layout_weight="2"/> + + <Button + android:id="@+id/cancel_sync_button" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:text="@string/cancel_sync_button" + android:onClick="cancelSync" + android:layout_weight="2"/> + </LinearLayout> + + <TextView android:id="@+id/status_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </LinearLayout> +</LinearLayout>
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 0000000..d818c78 --- /dev/null +++ b/apps/Development/res/layout/sync_adapter_item.xml
@@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** +** Copyright 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. +*/ +--> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + style="?android:attr/spinnerItemStyle" + android:singleLine="true" + android:layout_width="fill_parent" + android:layout_height="52dip" + android:ellipsize="marquee" />
diff --git a/apps/Development/res/values/strings.xml b/apps/Development/res/values/strings.xml index ad70fbe..c8a3f8b 100644 --- a/apps/Development/res/values/strings.xml +++ b/apps/Development/res/values/strings.xml
@@ -155,4 +155,25 @@ <string name="accounts_tester_edit_properties">Properties</string> <string name="accounts_tester_desired_authtokentype_label">authtoken type:</string> <string name="accounts_tester_desired_features_label">features:</string> + + <!-- SyncAdapterDriver --> + <string name="bind_button">bind</string> + <string name="unbind_button">unbind</string> + <string name="start_sync_button">start sync</string> + <string name="cancel_sync_button">cancel sync</string> + <string name="sync_adapters_spinner_label">Registered Sync Adapters:</string> + <string name="status_starting_sync_format">Starting a sync of account %s...</string> + <string name="status_remote_exception_while_starting_sync">Got a RemoteException while starting the sync</string> + <string name="status_canceled_sync">Canceled the sync</string> + <string name="status_remote_exception_while_canceling_sync">Got a RemoteException while canceling the sync</string> + <string name="status_received_heartbeat">Received heartbeat</string> + <string name="status_sync_failed_format">Sync failed: %s</string> + <string name="status_sync_succeeded_format">Sync succeeded: %s</string> + <string name="status_already_bound">Already bound to sync adapter</string> + <string name="status_sync_adapter_not_selected">No selected sync adapter</string> + <string name="binding_connected_format">Connected to Sync Adapter\n\tauthority: %s\n\taccount type: %s</string> + <string name="binding_not_connected">Not connected to a sync adapter</string> + <string name="binding_bind_failed">Bind failed</string> + <string name="binding_waiting_for_connection">Waiting for service to be connected...</string> + <string name="select_account_to_sync">Select account to sync</string> </resources>
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 0000000..eab37ec --- /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<RegisteredServicesCache.ServiceInfo<SyncAdapterType>> all = + mSyncAdaptersCache.getAllServices(); + synchronized (mSyncAdaptersLock) { + mSyncAdapters = new Object[all.size()]; + String[] names = new String[mSyncAdapters.length]; + int i = 0; + for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> item : all) { + mSyncAdapters[i] = item; + names[i] = item.type.authority + " - " + item.type.accountType; + i++; + } + + ArrayAdapter<String> adapter = + new ArrayAdapter<String>(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<String> adapter = + new ArrayAdapter<String>(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<SyncAdapterType> mSyncAdapter; + + public MyServiceConnection( + RegisteredServicesCache.ServiceInfo<SyncAdapterType> 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<SyncAdapterType> 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<SyncAdapterType> getSelectedSyncAdapter() { + synchronized (mSyncAdaptersLock) { + final int position = mSyncAdapterSpinner.getSelectedItemPosition(); + if (position == AdapterView.INVALID_POSITION) { + return null; + } + try { + //noinspection unchecked + return (RegisteredServicesCache.ServiceInfo<SyncAdapterType>) + mSyncAdapters[position]; + } catch (Exception e) { + return null; + } + } + } + + static class SyncAdaptersCache extends RegisteredServicesCache<SyncAdapterType> { + 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(); + } + } + } +}