summaryrefslogtreecommitdiff
path: root/src/com/android/im/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/im/app')
-rw-r--r--src/com/android/im/app/AccountActivity.java331
-rw-r--r--src/com/android/im/app/AddContactActivity.java237
-rw-r--r--src/com/android/im/app/BlockedContactView.java71
-rw-r--r--src/com/android/im/app/BlockedContactsActivity.java193
-rw-r--r--src/com/android/im/app/BrandingResources.java203
-rw-r--r--src/com/android/im/app/ChatBackgroundMaker.java62
-rw-r--r--src/com/android/im/app/ChatView.java1449
-rw-r--r--src/com/android/im/app/ChooseAccountActivity.java452
-rw-r--r--src/com/android/im/app/ContactListActivity.java447
-rw-r--r--src/com/android/im/app/ContactListFilterView.java135
-rw-r--r--src/com/android/im/app/ContactListTreeAdapter.java743
-rw-r--r--src/com/android/im/app/ContactListView.java486
-rw-r--r--src/com/android/im/app/ContactPresenceActivity.java130
-rw-r--r--src/com/android/im/app/ContactView.java286
-rw-r--r--src/com/android/im/app/ContactsPickerActivity.java181
-rw-r--r--src/com/android/im/app/Dashboard.java195
-rw-r--r--src/com/android/im/app/DatabaseUtils.java128
-rw-r--r--src/com/android/im/app/ErrorResUtils.java106
-rw-r--r--src/com/android/im/app/ImApp.java653
-rw-r--r--src/com/android/im/app/ImRingtonePreference.java82
-rw-r--r--src/com/android/im/app/ImUrlActivity.java184
-rw-r--r--src/com/android/im/app/ImageListAdapter.java131
-rw-r--r--src/com/android/im/app/ImpsAddressUtils.java27
-rw-r--r--src/com/android/im/app/IntTrie.java85
-rw-r--r--src/com/android/im/app/Markup.java116
-rw-r--r--src/com/android/im/app/MessageView.java169
-rw-r--r--src/com/android/im/app/NewChatActivity.java410
-rw-r--r--src/com/android/im/app/PreferenceActivity.java219
-rw-r--r--src/com/android/im/app/PresenceUtils.java98
-rw-r--r--src/com/android/im/app/ProviderDef.java36
-rw-r--r--src/com/android/im/app/ProviderListItem.java152
-rw-r--r--src/com/android/im/app/SettingActivity.java97
-rw-r--r--src/com/android/im/app/SigningInActivity.java268
-rw-r--r--src/com/android/im/app/SimpleAlertHandler.java135
-rw-r--r--src/com/android/im/app/SimpleInputActivity.java98
-rw-r--r--src/com/android/im/app/Ticker.java54
-rw-r--r--src/com/android/im/app/UserPresenceView.java257
-rw-r--r--src/com/android/im/app/adapter/ChatListenerAdapter.java70
-rw-r--r--src/com/android/im/app/adapter/ChatSessionListenerAdapter.java43
-rw-r--r--src/com/android/im/app/adapter/ConnectionListenerAdapter.java80
-rw-r--r--src/com/android/im/app/adapter/ContactListListenerAdapter.java67
41 files changed, 9366 insertions, 0 deletions
diff --git a/src/com/android/im/app/AccountActivity.java b/src/com/android/im/app/AccountActivity.java
new file mode 100644
index 0000000..fcd1ba3
--- /dev/null
+++ b/src/com/android/im/app/AccountActivity.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Im;
+import android.text.Editable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.LinkMovementMethod;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+public class AccountActivity extends Activity {
+ private static final String ACCOUNT_URI_KEY = "accountUri";
+
+ static final int REQUEST_SIGN_IN = RESULT_FIRST_USER + 1;
+
+ private static final String[] ACCOUNT_PROJECTION = {
+ Im.Account._ID,
+ Im.Account.PROVIDER,
+ Im.Account.USERNAME,
+ Im.Account.PASSWORD,
+ Im.Account.KEEP_SIGNED_IN,
+ };
+
+ private static final int ACCOUNT_PROVIDER_COLUMN = 1;
+ private static final int ACCOUNT_USERNAME_COLUMN = 2;
+ private static final int ACCOUNT_PASSWORD_COLUMN = 3;
+ private static final int ACCOUNT_KEEP_SIGNED_IN_COLUMN = 4;
+
+ Uri mAccountUri;
+
+ EditText mEditName;
+ EditText mEditPass;
+ CheckBox mRememberPass;
+ CheckBox mKeepSignIn;
+ Button mBtnSignIn;
+
+ String mToAddress;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+
+ setContentView(R.layout.account_activity);
+ mEditName = (EditText)findViewById(R.id.edtName);
+ mEditPass = (EditText)findViewById(R.id.edtPass);
+ mRememberPass = (CheckBox)findViewById(R.id.rememberPassword);
+ mKeepSignIn = (CheckBox)findViewById(R.id.keepSignIn);
+ mBtnSignIn = (Button)findViewById(R.id.btnSignIn);
+ mRememberPass.setOnCheckedChangeListener(new OnCheckedChangeListener(){
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ updateWidgetState();
+ }
+ });
+
+ ImApp app = ImApp.getApplication(this);
+ Intent i = getIntent();
+ String action = i.getAction();
+ mToAddress = i.getStringExtra(ImApp.EXTRA_INTENT_SEND_TO_USER);
+ final String origUserName;
+ final long providerId;
+ final ProviderDef provider;
+
+ if(Intent.ACTION_INSERT.equals(action)) {
+ origUserName = "";
+ providerId = ContentUris.parseId(i.getData());
+ provider = app.getProvider(providerId);
+ setTitle(getResources().getString(R.string.add_account, provider.mFullName));
+ } else if(Intent.ACTION_EDIT.equals(action)) {
+ ContentResolver cr = getContentResolver();
+ Uri uri = i.getData();
+
+ if ((uri == null) || !Im.Account.CONTENT_ITEM_TYPE.equals(cr.getType(uri))) {
+ Log.w(ImApp.LOG_TAG, "<AccountActivity>Bad data");
+ return;
+ }
+
+ Cursor cursor = cr.query(uri, ACCOUNT_PROJECTION, null, null, null);
+
+ if (cursor == null) {
+ finish();
+ return;
+ }
+
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ finish();
+ return;
+ }
+
+ setTitle(R.string.sign_in);
+
+ providerId = cursor.getLong(ACCOUNT_PROVIDER_COLUMN);
+ provider = app.getProvider(providerId);
+
+ origUserName = cursor.getString(ACCOUNT_USERNAME_COLUMN);
+ mEditName.setText(origUserName);
+ mEditPass.setText(cursor.getString(ACCOUNT_PASSWORD_COLUMN));
+
+ mRememberPass.setChecked(!cursor.isNull(ACCOUNT_PASSWORD_COLUMN));
+
+ boolean keepSignIn = cursor.getInt(ACCOUNT_KEEP_SIGNED_IN_COLUMN) == 1;
+ mKeepSignIn.setChecked(keepSignIn);
+
+ cursor.close();
+ } else {
+ Log.w(ImApp.LOG_TAG, "<AccountActivity> unknown intent action " + action);
+ finish();
+ return;
+ }
+
+ final BrandingResources brandingRes = app.getBrandingResource(providerId);
+ mKeepSignIn.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ CheckBox keepSignIn = (CheckBox) v;
+ if ( keepSignIn.isChecked() ) {
+ String msg = brandingRes.getString(BrandingResourceIDs.STRING_TOAST_CHECK_AUTO_SIGN_IN);
+ Toast.makeText(AccountActivity.this, msg, Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ mRememberPass.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ CheckBox keepSignIn = (CheckBox) v;
+ if ( keepSignIn.isChecked() ) {
+ String msg = brandingRes.getString(BrandingResourceIDs.STRING_TOAST_CHECK_SAVE_PASSWORD);
+ Toast.makeText(AccountActivity.this, msg, Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ Drawable logo = brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_LOGO);
+ getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, logo);
+
+ TextView labelUsername = (TextView)findViewById(R.id.label_username);
+ labelUsername.setText(brandingRes.getString(BrandingResourceIDs.STRING_LABEL_USERNAME));
+ mEditName.addTextChangedListener(mTextWatcher);
+ mEditPass.addTextChangedListener(mTextWatcher);
+
+ mBtnSignIn.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ String username = mEditName.getText().toString();
+ final String pass = mEditPass.getText().toString();
+ final boolean rememberPass = mRememberPass.isChecked();
+
+ ContentResolver cr = getContentResolver();
+
+ long accountId = ImApp.insertOrUpdateAccount(cr, providerId, username,
+ rememberPass ? pass : null);
+ mAccountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
+
+ if (!origUserName.equals(username) && shouldShowTermOfUse(brandingRes)) {
+ comfirmTermsOfUse(brandingRes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ signIn(rememberPass, pass);
+ }
+ });
+ } else {
+ signIn(rememberPass, pass);
+ }
+ }
+
+ void signIn(boolean rememberPass, String pass) {
+ Intent intent = new Intent(AccountActivity.this, SigningInActivity.class);
+ intent.setData(mAccountUri);
+ if (!rememberPass) {
+ intent.putExtra(ImApp.EXTRA_INTENT_PASSWORD, pass);
+ }
+
+ if (mToAddress != null) {
+ intent.putExtra(ImApp.EXTRA_INTENT_SEND_TO_USER, mToAddress);
+ }
+
+ startActivityForResult(intent, REQUEST_SIGN_IN);
+ }
+ });
+
+ // Make link for signing up.
+ String text = brandingRes.getString(BrandingResourceIDs.STRING_LABEL_SIGN_UP);
+ SpannableStringBuilder builder = new SpannableStringBuilder(text);
+ builder.setSpan(new URLSpan(provider.mSignUpUrl), 0, builder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ TextView signUp = (TextView)findViewById(R.id.signUp);
+ signUp.setText(builder);
+ signUp.setMovementMethod(LinkMovementMethod.getInstance());
+
+ updateWidgetState();
+ }
+
+ void comfirmTermsOfUse(BrandingResources res, DialogInterface.OnClickListener accept) {
+ SpannableString message = new SpannableString(
+ res.getString(BrandingResourceIDs.STRING_TOU_MESSAGE));
+ Linkify.addLinks(message, Linkify.ALL);
+
+ new AlertDialog.Builder(this)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(res.getString(BrandingResourceIDs.STRING_TOU_TITLE))
+ .setMessage(message)
+ .setPositiveButton(res.getString(BrandingResourceIDs.STRING_TOU_DECLINE), null)
+ .setNegativeButton(res.getString(BrandingResourceIDs.STRING_TOU_ACCEPT), accept)
+ .show();
+ }
+
+ boolean shouldShowTermOfUse(BrandingResources res) {
+ return !TextUtils.isEmpty(res.getString(BrandingResourceIDs.STRING_TOU_MESSAGE));
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mAccountUri = savedInstanceState.getParcelable(ACCOUNT_URI_KEY);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(ACCOUNT_URI_KEY, mAccountUri);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == REQUEST_SIGN_IN) {
+ if (resultCode == RESULT_OK) {
+ boolean keepSignIn = mKeepSignIn.isChecked();
+ updateKeepSignedIn(keepSignIn);
+ finish();
+ } else {
+ // sign in failed, disable keep sign in, clear the password.
+ mKeepSignIn.setChecked(false);
+ updateKeepSignedIn(false);
+ mEditPass.setText("");
+ ContentValues values = new ContentValues();
+ values.put(Im.Account.PASSWORD, (String) null);
+ getContentResolver().update(mAccountUri, values, null, null);
+ }
+ }
+ }
+
+ void updateKeepSignedIn(boolean keepSignIn) {
+ ContentValues values = new ContentValues();
+ values.put(Im.Account.KEEP_SIGNED_IN, keepSignIn ? 1 : 0);
+ getContentResolver().update(mAccountUri, values, null, null);
+ }
+
+ void updateWidgetState() {
+ boolean goodUsername = mEditName.getText().length() > 0;
+ boolean goodPassword = mEditPass.getText().length() > 0;
+ boolean hasNameAndPassword = goodUsername && goodPassword;
+
+ mEditPass.setEnabled(goodUsername);
+ mEditPass.setFocusable(goodUsername);
+ mEditPass.setFocusableInTouchMode(goodUsername);
+
+ // enable keep sign in only when remember password is checked.
+ boolean rememberPass = mRememberPass.isChecked();
+ if (rememberPass && !hasNameAndPassword) {
+ mRememberPass.setChecked(false);
+ rememberPass = false;
+ }
+ mRememberPass.setEnabled(hasNameAndPassword);
+ mRememberPass.setFocusable(hasNameAndPassword);
+
+ if (!rememberPass) {
+ mKeepSignIn.setChecked(false);
+ }
+ mKeepSignIn.setEnabled(rememberPass);
+ mKeepSignIn.setFocusable(rememberPass);
+
+ mBtnSignIn.setEnabled(hasNameAndPassword);
+ mBtnSignIn.setFocusable(hasNameAndPassword);
+ }
+
+ private final TextWatcher mTextWatcher = new TextWatcher() {
+ public void beforeTextChanged(CharSequence s, int start, int before, int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int after) {
+ updateWidgetState();
+ }
+
+ public void afterTextChanged(Editable s) {
+ }
+ };
+
+}
diff --git a/src/com/android/im/app/AddContactActivity.java b/src/com/android/im/app/AddContactActivity.java
new file mode 100644
index 0000000..a2b490e
--- /dev/null
+++ b/src/com/android/im/app/AddContactActivity.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import static android.provider.Contacts.ContactMethods.CONTENT_EMAIL_URI;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.provider.Contacts.ContactMethods;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.MultiAutoCompleteTextView;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+import com.android.im.IContactList;
+import com.android.im.IContactListManager;
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.plugin.BrandingResourceIDs;
+import com.android.im.plugin.ImpsConfigNames;
+import com.android.im.service.ImServiceConstants;
+
+import java.util.List;
+
+public class AddContactActivity extends Activity {
+
+ private MultiAutoCompleteTextView mAddressList;
+ Button mInviteButton;
+ ImApp mApp;
+ SimpleAlertHandler mHandler;
+
+ private long mProviderId;
+ private String mListName;
+ private String mDefaultDomain;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mApp = ImApp.getApplication(this);
+ mHandler = new SimpleAlertHandler(this);
+ resolveIntent(getIntent());
+
+ setContentView(R.layout.add_contact_activity);
+
+ BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
+ setTitle(brandingRes.getString(BrandingResourceIDs.STRING_ADD_CONTACT_TITLE));
+
+ TextView label = (TextView) findViewById(R.id.input_contact_label);
+ label.setText(brandingRes.getString(BrandingResourceIDs.STRING_LABEL_INPUT_CONTACT));
+
+ mAddressList = (MultiAutoCompleteTextView) findViewById(R.id.email);
+ mAddressList.setAdapter(new EmailAddressAdapter(this));
+ mAddressList.setTokenizer(new Rfc822Tokenizer());
+ mAddressList.addTextChangedListener(mTextWatcher);
+ mInviteButton = (Button) findViewById(R.id.invite);
+ mInviteButton.setText(brandingRes.getString(BrandingResourceIDs.STRING_BUTTON_ADD_CONTACT));
+ mInviteButton.setOnClickListener(mButtonHandler);
+ mInviteButton.setEnabled(false);
+ }
+
+ private void resolveIntent(Intent intent) {
+ mProviderId = intent.getLongExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, -1);
+ mListName = intent.getStringExtra(ImServiceConstants.EXTRA_INTENT_LIST_NAME);
+ mDefaultDomain = Im.ProviderSettings.getStringValue(getContentResolver(),
+ mProviderId, ImpsConfigNames.DEFAULT_DOMAIN);
+ }
+
+ void inviteBuddies() {
+ Rfc822Token[] recipients = Rfc822Tokenizer.tokenize(mAddressList.getText());
+ try {
+ IImConnection conn = mApp.getConnection(mProviderId);
+ IContactList list = getContactList(conn);
+ if (list == null) {
+ Log.e(ImApp.LOG_TAG, "<AddContactActivity> can't find given contact list:" + mListName);
+ finish();
+ } else {
+ boolean fail = false;
+ for (Rfc822Token recipient : recipients) {
+ String username = recipient.getAddress();
+ if (mDefaultDomain != null && username.indexOf('@') == -1) {
+ username = username + "@" + mDefaultDomain;
+ }
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("addContact:" + username);
+ }
+ int res = list.addContact(username);
+ if (res != ImErrorInfo.NO_ERROR) {
+ fail = true;
+ mHandler.showAlert(R.string.error,
+ ErrorResUtils.getErrorRes(getResources(), res, username));
+ }
+ }
+ // close the screen if there's no error.
+ if (!fail) {
+ finish();
+ }
+ }
+ } catch (RemoteException ex) {
+ Log.e(ImApp.LOG_TAG, "<AddContactActivity> inviteBuddies: caught " + ex);
+ }
+ }
+
+ private IContactList getContactList(IImConnection conn) {
+ if (conn == null) {
+ return null;
+ }
+
+ try {
+ IContactListManager contactListMgr = conn.getContactListManager();
+ if (!TextUtils.isEmpty(mListName)) {
+ return contactListMgr.getContactList(mListName);
+ } else {
+ // Use the default list
+ List<IBinder> lists = contactListMgr.getContactLists();
+ for (IBinder binder : lists) {
+ IContactList list = IContactList.Stub.asInterface(binder);
+ if (list.isDefault()) {
+ return list;
+ }
+ }
+ // No default list, use the first one as default list
+ if (!lists.isEmpty()) {
+ return IContactList.Stub.asInterface(lists.get(0));
+ }
+ return null;
+ }
+ } catch (RemoteException e) {
+ // If the service has died, there is no list for now.
+ return null;
+ }
+ }
+
+ private View.OnClickListener mButtonHandler = new View.OnClickListener() {
+ public void onClick(View v) {
+ mApp.callWhenServiceConnected(mHandler, new Runnable() {
+ public void run() {
+ inviteBuddies();
+ }
+ });
+ }
+ };
+
+ private TextWatcher mTextWatcher = new TextWatcher() {
+ public void afterTextChanged(Editable s) {
+ mInviteButton.setEnabled(s.length() != 0);
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // noop
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // noop
+ }
+ };
+
+ private static void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "<AddContactActivity> " + msg);
+ }
+
+ static class EmailAddressAdapter extends ResourceCursorAdapter {
+ public static final int DATA_INDEX = 1;
+
+ private static final String SORT_ORDER = "people.name, contact_methods.data";
+ private ContentResolver mContentResolver;
+
+ private static final String[] PROJECTION = {
+ ContactMethods._ID, // 0
+ ContactMethods.DATA // 1
+ };
+
+ public EmailAddressAdapter(Context context) {
+ super(context, android.R.layout.simple_dropdown_item_1line, null);
+ mContentResolver = context.getContentResolver();
+ }
+
+ @Override
+ public final String convertToString(Cursor cursor) {
+ return cursor.getString(DATA_INDEX);
+ }
+
+ @Override
+ public final void bindView(View view, Context context, Cursor cursor) {
+ ((TextView) view).setText(cursor.getString(DATA_INDEX));
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String where = null;
+
+ if (constraint != null) {
+ String filter = DatabaseUtils.sqlEscapeString(constraint.toString() + '%');
+
+ StringBuilder s = new StringBuilder();
+ s.append("(people.name LIKE ");
+ s.append(filter);
+ s.append(") OR (contact_methods.data LIKE ");
+ s.append(filter);
+ s.append(")");
+
+ where = s.toString();
+ }
+
+ return mContentResolver.query(CONTENT_EMAIL_URI, PROJECTION, where, null, SORT_ORDER);
+ }
+ }
+}
diff --git a/src/com/android/im/app/BlockedContactView.java b/src/com/android/im/app/BlockedContactView.java
new file mode 100644
index 0000000..ac482ce
--- /dev/null
+++ b/src/com/android/im/app/BlockedContactView.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
+
+public class BlockedContactView extends LinearLayout {
+ private ImageView mAvatar;
+ private ImageView mBlockedIcon;
+ private TextView mLine1;
+ private TextView mLine2;
+
+ public BlockedContactView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mAvatar = (ImageView) findViewById(R.id.avatar);
+ mBlockedIcon = (ImageView)findViewById(R.id.blocked);
+ mLine1 = (TextView) findViewById(R.id.line1);
+ mLine2 = (TextView) findViewById(R.id.line2);
+ }
+
+ public void bind(Cursor cursor) {
+ long providerId = cursor.getLong(BlockedContactsActivity.PROVIDER_COLUMN);
+ String username = cursor.getString(BlockedContactsActivity.USERNAME_COLUMN);
+ String nickname = cursor.getString(BlockedContactsActivity.NICKNAME_COLUMN);
+
+ Drawable avatar = DatabaseUtils.getAvatarFromCursor(cursor,
+ BlockedContactsActivity.AVATAR_COLUMN);
+
+ if (avatar != null) {
+ mAvatar.setImageDrawable(avatar);
+ } else {
+ mAvatar.setImageResource(R.drawable.avatar_unknown);
+ }
+ ImApp app = ImApp.getApplication((Activity)mContext);
+ BrandingResources brandingRes = app.getBrandingResource(providerId);
+ mBlockedIcon.setImageDrawable(brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_BLOCK));
+ mLine1.setText(nickname);
+ mLine2.setText(ImpsAddressUtils.getDisplayableAddress(username));
+ }
+}
diff --git a/src/com/android/im/app/BlockedContactsActivity.java b/src/com/android/im/app/BlockedContactsActivity.java
new file mode 100644
index 0000000..014f596
--- /dev/null
+++ b/src/com/android/im/app/BlockedContactsActivity.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.ResourceCursorAdapter;
+
+import com.android.im.IContactListManager;
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
+
+public class BlockedContactsActivity extends ListActivity {
+ ImApp mApp;
+ SimpleAlertHandler mHandler;
+
+ private static final String[] PROJECTION = {
+ Im.BlockedList._ID,
+ Im.BlockedList.ACCOUNT,
+ Im.BlockedList.PROVIDER,
+ Im.BlockedList.NICKNAME,
+ Im.BlockedList.USERNAME,
+ Im.BlockedList.AVATAR_DATA,
+ };
+
+ static final int ACCOUNT_COLUMN = 1;
+ static final int PROVIDER_COLUMN = 2;
+ static final int NICKNAME_COLUMN = 3;
+ static final int USERNAME_COLUMN = 4;
+ static final int AVATAR_COLUMN = 5;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ requestWindowFeature(Window.FEATURE_LEFT_ICON);
+
+ setContentView(R.layout.blocked_contacts_activity);
+ mHandler = new SimpleAlertHandler(this);
+
+ mApp = ImApp.getApplication(this);
+ mApp.startImServiceIfNeed();
+ if (!resolveIntent()) {
+ finish();
+ return;
+ }
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ Cursor c = (Cursor) l.getAdapter().getItem(position);
+ if (c == null) {
+ mHandler.showAlert(R.string.error, R.string.select_contact);
+ return;
+ }
+ long providerId = c.getLong(PROVIDER_COLUMN);
+ String username = c.getString(USERNAME_COLUMN);
+ String nickname = c.getString(NICKNAME_COLUMN);
+ mApp.callWhenServiceConnected(mHandler, new UnblockAction(providerId, username, nickname));
+ }
+
+ private boolean resolveIntent() {
+ Intent i = getIntent();
+ Uri uri = i.getData();
+ if (uri == null) {
+ warning("No data to show");
+ return false;
+ }
+
+ long accountId = ContentUris.parseId(uri);
+ Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
+ Cursor accountCursor = getContentResolver().query(accountUri, null, null, null, null);
+ if (accountCursor == null) {
+ warning("Bad account");
+ return false;
+ }
+ if (!accountCursor.moveToFirst()) {
+ warning("Bad account");
+ accountCursor.close();
+ return false;
+ }
+
+ long providerId = accountCursor.getLong(
+ accountCursor.getColumnIndexOrThrow(Im.Account.PROVIDER));
+ String username = accountCursor.getString(
+ accountCursor.getColumnIndexOrThrow(Im.Account.USERNAME));
+
+ BrandingResources brandingRes = mApp.getBrandingResource(providerId);
+ getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON,
+ brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_LOGO));
+
+ setTitle(getResources().getString(R.string.blocked_list_title, username));
+ accountCursor.close();
+
+ Cursor c = managedQuery(uri, PROJECTION, null, Im.BlockedList.DEFAULT_SORT_ORDER);
+ if (c == null) {
+ warning("Database error when query " + uri);
+ return false;
+ }
+
+ ListAdapter adapter = new BlockedContactsAdapter(c, this);
+ setListAdapter(adapter);
+ return true;
+ }
+
+ private static void warning(String msg) {
+ Log.w(ImApp.LOG_TAG, "<BlockContactsActivity> " + msg);
+ }
+
+ private static class BlockedContactsAdapter extends ResourceCursorAdapter {
+ public BlockedContactsAdapter(Cursor c, Context context) {
+ super(context, R.layout.blocked_contact_view, c);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ if (view instanceof BlockedContactView) {
+ ((BlockedContactView) view).bind(cursor);
+ }
+ }
+ }
+
+ private class UnblockAction implements Runnable {
+ private long mProviderId;
+ String mUserName;
+ private String mNickName;
+
+ public UnblockAction(long providerId, String userName, String nickName) {
+ mProviderId = providerId;
+ mUserName = userName;
+ mNickName = nickName;
+ }
+
+ public void run() {
+ final IImConnection conn = mApp.getConnection(mProviderId);
+ if (conn == null) {
+ mHandler.showAlert(R.string.error, R.string.disconnected);
+ return;
+ }
+ DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
+ public void onClick(DialogInterface dialog, int whichButton) {
+ try {
+ IContactListManager manager = conn.getContactListManager();
+ manager.unBlockContact(mUserName);
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ };
+ Resources r = getResources();
+
+ new AlertDialog.Builder(BlockedContactsActivity.this)
+ .setTitle(R.string.confirm)
+ .setMessage(r.getString(R.string.confirm_unblock_contact, mNickName))
+ .setPositiveButton(R.string.yes, confirmListener) // default button
+ .setNegativeButton(R.string.no, null)
+ .setCancelable(false)
+ .show();
+ }
+
+ }
+}
diff --git a/src/com/android/im/app/BrandingResources.java b/src/com/android/im/app/BrandingResources.java
new file mode 100644
index 0000000..97dd698
--- /dev/null
+++ b/src/com/android/im/app/BrandingResources.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.plugin.IImPlugin;
+import com.android.im.plugin.ImPluginInfo;
+import dalvik.system.PathClassLoader;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * The provider specific branding resources.
+ */
+public class BrandingResources {
+ private static final String TAG = ImApp.LOG_TAG;
+
+ private Map<Integer, Integer> mResMapping;
+ private Resources mPackageRes;
+ private int[] mSmileyIcons;
+
+ private BrandingResources mDefaultRes;
+
+ /**
+ * Creates a new BrandingResource of a specific plug-in. The resources will
+ * be retrieved from the plug-in package.
+ *
+ * @param context The current application context.
+ * @param pluginInfo The info about the plug-in.
+ * @param defaultRes The default branding resources. If the resource is not
+ * found in the plug-in, the default resource will be returned.
+ */
+ public BrandingResources(Context context, ImPluginInfo pluginInfo,
+ BrandingResources defaultRes) {
+ mDefaultRes = defaultRes;
+
+ PackageManager pm = context.getPackageManager();
+ try {
+ mPackageRes = pm.getResourcesForApplication(pluginInfo.mPackageName);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Can not load resources from package: " + pluginInfo.mPackageName);
+ }
+ // Load the plug-in directly from the apk instead of binding the service
+ // and calling through the IPC binder API. It's more effective in this way
+ // and we can avoid the async behaviors of binding service.
+ PathClassLoader classLoader = new PathClassLoader(pluginInfo.mSrcPath,
+ context.getClassLoader());
+ try {
+ Class cls = classLoader.loadClass(pluginInfo.mClassName);
+ Method m = cls.getMethod("onBind", Intent.class);
+ IImPlugin plugin = (IImPlugin)m.invoke(cls.newInstance(), new Object[]{null});
+ mResMapping = plugin.getResourceMap();
+ mSmileyIcons = plugin.getSmileyIconIds();
+ } catch (ClassNotFoundException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ } catch (InstantiationException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ } catch (SecurityException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed load the plugin resource map", e);
+ }
+ }
+
+ /**
+ * Creates a BrandingResource with application context and the resource ID map.
+ * The resource will be retrieved from the context directly instead from the plug-in package.
+ *
+ * @param context
+ * @param resMapping
+ */
+ public BrandingResources(Context context, Map<Integer, Integer> resMapping,
+ BrandingResources defaultRes) {
+ mPackageRes = context.getResources();
+ mResMapping = resMapping;
+ mDefaultRes = defaultRes;
+ }
+
+ /**
+ * Gets a drawable object associated with a particular resource ID defined
+ * in {@link com.android.im.plugin.BrandingResourceIDs}
+ *
+ * @param id The ID defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ * @return Drawable An object that can be used to draw this resource.
+ */
+ public Drawable getDrawable(int id) {
+ int resId = getPackageResourceId(id);
+ if (resId != 0) {
+ return mPackageRes.getDrawable(resId);
+ } else if (mDefaultRes != null){
+ return mDefaultRes.getDrawable(id);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets an array of the IDs of the supported smiley of the provider. Use
+ * {@link #getSmileyIcon(int)} to get the drawable object of the smiley.
+ *
+ * @return An array of the IDs of the supported smileys.
+ */
+ public int[] getSmileyIcons() {
+ return mSmileyIcons;
+ }
+
+ /**
+ * Gets the drawable associated with particular smiley ID.
+ *
+ * @param smileyId The ID of the smiley returned in
+ * {@link #getSmileyIcons()}
+ * @return Drawable An object that can be used to draw this smiley.
+ */
+ public Drawable getSmileyIcon(int smileyId){
+ if (mPackageRes == null) {
+ return null;
+ }
+ return mPackageRes.getDrawable(smileyId);
+ }
+
+ /**
+ * Gets the string value associated with a particular resource ID defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ *
+ * @param id The ID of the string resource defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ * @param formatArgs The format arguments that will be used for
+ * substitution.
+ * @return The string data associated with the resource
+ */
+ public String getString(int id, Object... formatArgs) {
+ int resId = getPackageResourceId(id);
+ if (resId != 0) {
+ return mPackageRes.getString(resId, formatArgs);
+ } else if (mDefaultRes != null){
+ return mDefaultRes.getString(id, formatArgs);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the string array associated with a particular resource ID defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ *
+ * @param id The ID of the string resource defined in
+ * {@link com.android.im.plugin.BrandingResourceIDs}
+ * @return The string array associated with the resource.
+ */
+ public String[] getStringArray(int id) {
+ int resId = getPackageResourceId(id);
+ if (resId != 0) {
+ return mPackageRes.getStringArray(resId);
+ } else if (mDefaultRes != null){
+ return mDefaultRes.getStringArray(id);
+ } else {
+ return null;
+ }
+ }
+
+ private int getPackageResourceId(int id) {
+ if (mResMapping == null || mPackageRes == null) {
+ return 0;
+ }
+ Integer resId = mResMapping.get(id);
+ return resId == null ? 0 : resId;
+ }
+
+}
diff --git a/src/com/android/im/app/ChatBackgroundMaker.java b/src/com/android/im/app/ChatBackgroundMaker.java
new file mode 100644
index 0000000..fe340e5
--- /dev/null
+++ b/src/com/android/im/app/ChatBackgroundMaker.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.provider.Im;
+import android.view.View;
+
+public class ChatBackgroundMaker {
+ private final Drawable mIncomingBg;
+ private final Drawable mDivider;
+ private final Rect mPadding;
+
+ public ChatBackgroundMaker(Context context) {
+ Resources res = context.getResources();
+ mIncomingBg = res.getDrawable(R.drawable.textfield_im_received);
+ mDivider = res.getDrawable(R.drawable.text_divider_horizontal);
+ mPadding = new Rect();
+ mIncomingBg.getPadding(mPadding);
+ }
+
+ public void setBackground(MessageView view, String contact, int type) {
+ View msgText = view.findViewById(R.id.message);
+
+ switch (type) {
+ case Im.MessageType.INCOMING:
+ // TODO: set color according different contact
+ msgText.setBackgroundDrawable(mIncomingBg);
+ break;
+
+ case Im.MessageType.OUTGOING:
+ case Im.MessageType.POSTPONED:
+ msgText.setBackgroundDrawable(null);
+ msgText.setPadding(mPadding.left, mPadding.top, mPadding.right,
+ mPadding.bottom);
+ break;
+
+ default:
+ msgText.setBackgroundDrawable(mDivider);
+ }
+ }
+}
diff --git a/src/com/android/im/app/ChatView.java b/src/com/android/im/app/ChatView.java
new file mode 100644
index 0000000..58521a4
--- /dev/null
+++ b/src/com/android/im/app/ChatView.java
@@ -0,0 +1,1449 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Map;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.CursorIndexOutOfBoundsException;
+import android.database.DataSetObserver;
+import android.database.CharArrayBuffer;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+import android.text.style.URLSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.android.im.IChatListener;
+import com.android.im.IChatSession;
+import com.android.im.IChatSessionListener;
+import com.android.im.IChatSessionManager;
+import com.android.im.IContactList;
+import com.android.im.IContactListListener;
+import com.android.im.IContactListManager;
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.app.adapter.ChatListenerAdapter;
+import com.android.im.app.adapter.ChatSessionListenerAdapter;
+import com.android.im.engine.Contact;
+import com.android.im.engine.ImConnection;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.plugin.BrandingResourceIDs;
+
+public class ChatView extends LinearLayout {
+ // This projection and index are set for the query of active chats
+ static final String[] CHAT_PROJECTION = {
+ Im.Contacts._ID,
+ Im.Contacts.ACCOUNT,
+ Im.Contacts.PROVIDER,
+ Im.Contacts.USERNAME,
+ Im.Contacts.NICKNAME,
+ Im.Contacts.TYPE,
+ Im.Presence.PRESENCE_STATUS,
+ Im.Chats.LAST_UNREAD_MESSAGE,
+ };
+ static final int CONTACT_ID_COLUMN = 0;
+ static final int ACCOUNT_COLUMN = 1;
+ static final int PROVIDER_COLUMN = 2;
+ static final int USERNAME_COLUMN = 3;
+ static final int NICKNAME_COLUMN = 4;
+ static final int TYPE_COLUMN = 5;
+ static final int PRESENCE_STATUS_COLUMN = 6;
+ static final int LAST_UNREAD_MESSAGE_COLUMN = 7;
+
+ static final String[] INVITATION_PROJECT = {
+ Im.Invitation._ID,
+ Im.Invitation.PROVIDER,
+ Im.Invitation.SENDER,
+ };
+ static final int INVITATION_ID_COLUMN = 0;
+ static final int INVITATION_PROVIDER_COLUMN = 1;
+ static final int INVITATION_SENDER_COLUMN = 2;
+
+ static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
+
+ Markup mMarkup;
+
+ Activity mScreen;
+ ImApp mApp;
+ SimpleAlertHandler mHandler;
+ Cursor mCursor;
+
+ private ImageView mStatusIcon;
+ private TextView mTitle;
+ /*package*/ListView mHistory;
+ EditText mEdtInput;
+ private Button mSendButton;
+ private View mStatusWarningView;
+ private ImageView mWarningIcon;
+ private TextView mWarningText;
+
+ private MessageAdapter mMessageAdapter;
+ private IChatSessionManager mChatSessionMgr;
+ private IChatSessionListener mChatSessionListener;
+
+ private IChatSession mChatSession;
+ private long mChatId;
+ int mType;
+ String mNickName;
+ String mUserName;
+ long mProviderId;
+ long mAccountId;
+ long mInvitationId;
+ private int mPresenceStatus;
+
+ private int mViewType;
+
+ private static final int VIEW_TYPE_CHAT = 1;
+ private static final int VIEW_TYPE_INVITATION = 2;
+ private static final int VIEW_TYPE_SUBSCRIPTION = 3;
+
+ private static final long SHOW_TIME_STAMP_INTERVAL = 60 * 1000; // 1 minute
+ private static final int QUERY_TOKEN = 10;
+
+ // Async QueryHandler
+ private final class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(Context context) {
+ super(context.getContentResolver());
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor c) {
+ Cursor cursor = new DeltaCursor(c);
+
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("onQueryComplete: cursor.count=" + cursor.getCount());
+ }
+
+ mMessageAdapter.changeCursor(cursor);
+ }
+ }
+ private QueryHandler mQueryHandler;
+
+ private class RequeryCallback implements Runnable {
+ public void run() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("RequeryCallback");
+ }
+ requeryCursor();
+ }
+ }
+ private RequeryCallback mRequeryCallback = null;
+
+ private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (!(view instanceof MessageView)) {
+ return;
+ }
+ URLSpan[] links = ((MessageView)view).getMessageLinks();
+ if (links.length == 0){
+ return;
+ }
+
+ final ArrayList<String> linkUrls = new ArrayList<String>(links.length);
+ for (URLSpan u : links) {
+ linkUrls.add(u.getURL());
+ }
+ ArrayAdapter<String> a = new ArrayAdapter<String>(mScreen,
+ android.R.layout.select_dialog_item, linkUrls);
+ AlertDialog.Builder b = new AlertDialog.Builder(mScreen);
+ b.setTitle(R.string.select_link_title);
+ b.setCancelable(true);
+ b.setAdapter(a, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ Uri uri = Uri.parse(linkUrls.get(which));
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+ mScreen.startActivity(intent);
+ }
+ });
+ b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ b.show();
+ }
+ };
+
+ private IChatListener mChatListener = new ChatListenerAdapter() {
+ @Override
+ public void onIncomingMessage(IChatSession ses,
+ com.android.im.engine.Message msg) {
+ scheduleRequery(0);
+ }
+
+ @Override
+ public void onContactJoined(IChatSession ses, Contact contact) {
+ scheduleRequery(0);
+ }
+
+ @Override
+ public void onContactLeft(IChatSession ses, Contact contact) {
+ scheduleRequery(0);
+ }
+
+ @Override
+ public void onSendMessageError(IChatSession ses,
+ com.android.im.engine.Message msg, ImErrorInfo error) {
+ scheduleRequery(0);
+ }
+ };
+
+ private Runnable mUpdateChatCallback = new Runnable() {
+ public void run() {
+ if (mCursor.requery() && mCursor.moveToFirst()) {
+ updateChat();
+ }
+ }
+ };
+ private IContactListListener mContactListListener = new IContactListListener.Stub () {
+ public void onAllContactListsLoaded() {
+ }
+
+ public void onContactChange(int type, IContactList list, Contact contact){
+ }
+
+ public void onContactError(int errorType, ImErrorInfo error,
+ String listName, Contact contact) {
+ }
+
+ public void onContactsPresenceUpdate(Contact[] contacts) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("onContactsPresenceUpdate()");
+ }
+ for (Contact c : contacts) {
+ if (c.getAddress().getFullName().equals(mUserName)) {
+ mHandler.post(mUpdateChatCallback);
+ scheduleRequery(0);
+ break;
+ }
+ }
+ }
+ };
+
+ static final void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "<ChatView> " +msg);
+ }
+
+ public ChatView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mScreen = (Activity) context;
+ mApp = ImApp.getApplication(mScreen);
+ mHandler = new ChatViewHandler();
+ }
+
+ void registerForConnEvents() {
+ mApp.registerForConnEvents(mHandler);
+ }
+
+ void unregisterForConnEvents() {
+ mApp.unregisterForConnEvents(mHandler);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mStatusIcon = (ImageView) findViewById(R.id.statusIcon);
+ mTitle = (TextView) findViewById(R.id.title);
+ mHistory = (ListView) findViewById(R.id.history);
+ mEdtInput = (EditText) findViewById(R.id.edtInput);
+ mSendButton = (Button)findViewById(R.id.btnSend);
+ mHistory.setOnItemClickListener(mOnItemClickListener);
+
+ mStatusWarningView = findViewById(R.id.warning);
+ mWarningIcon = (ImageView)findViewById(R.id.warningIcon);
+ mWarningText = (TextView)findViewById(R.id.warningText);
+
+ Button acceptInvitation = (Button)findViewById(R.id.btnAccept);
+ Button declineInvitation= (Button)findViewById(R.id.btnDecline);
+
+ Button approveSubscription = (Button)findViewById(R.id.btnApproveSubscription);
+ Button declineSubscription = (Button)findViewById(R.id.btnDeclineSubscription);
+
+ acceptInvitation.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ acceptInvitation();
+ }
+ });
+ declineInvitation.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ declineInvitation();
+ }
+ });
+
+ approveSubscription.setOnClickListener(new OnClickListener(){
+ public void onClick(View v) {
+ approveSubscription();
+ }
+ });
+ declineSubscription.setOnClickListener(new OnClickListener(){
+ public void onClick(View v) {
+ declineSubscription();
+ }
+ });
+
+ mEdtInput.setOnKeyListener(new OnKeyListener(){
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ sendMessage();
+ return true;
+
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.isAltPressed()) {
+ mEdtInput.append("\n");
+ } else {
+ sendMessage();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+ mSendButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ sendMessage();
+ }
+ });
+ }
+
+ public void onResume(){
+ if (mViewType == VIEW_TYPE_CHAT) {
+ Cursor cursor = getMessageCursor();
+ if (cursor == null) {
+ startQuery();
+ } else {
+ requeryCursor();
+ }
+ updateWarningView();
+ }
+ registerChatListener();
+ registerForConnEvents();
+ }
+
+ public void onPause(){
+ Cursor cursor = getMessageCursor();
+ if (cursor != null) {
+ cursor.deactivate();
+ }
+ cancelRequery();
+ if (mViewType == VIEW_TYPE_CHAT) {
+ markAsRead();
+ }
+ unregisterChatListener();
+ unregisterForConnEvents();
+ unregisterChatSessionListener();
+ }
+
+ void updateChat() {
+ setViewType(VIEW_TYPE_CHAT);
+
+ long oldChatId = mChatId;
+
+ updateContactInfo();
+
+ setStatusIcon();
+ setTitle();
+
+ IImConnection conn = mApp.getConnection(mProviderId);
+ if (conn == null) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
+ mScreen.finish();
+ return;
+ }
+
+ BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
+ mHistory.setBackgroundDrawable(
+ brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));
+
+ if (mMarkup == null) {
+ mMarkup = new Markup(brandingRes);
+ }
+
+ if (mMessageAdapter == null) {
+ mMessageAdapter = new MessageAdapter(mScreen, null);
+ mHistory.setAdapter(mMessageAdapter);
+ }
+
+ // only change the message adapter when we switch to another chat
+ if (mChatId != oldChatId) {
+ startQuery();
+ mEdtInput.setText("");
+ }
+
+ updateWarningView();
+ }
+
+ private void updateContactInfo() {
+ mChatId = mCursor.getLong(CONTACT_ID_COLUMN);
+ mProviderId = mCursor.getLong(PROVIDER_COLUMN);
+ mAccountId = mCursor.getLong(ACCOUNT_COLUMN);
+ mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN);
+ mType = mCursor.getInt(TYPE_COLUMN);
+ mUserName = mCursor.getString(USERNAME_COLUMN);
+ mNickName = mCursor.getString(NICKNAME_COLUMN);
+ }
+
+ private void setTitle() {
+ if (mType == Im.Contacts.TYPE_GROUP) {
+ final String[] projection = {Im.GroupMembers.NICKNAME};
+ Uri memberUri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, mChatId);
+ ContentResolver cr = mScreen.getContentResolver();
+ Cursor c = cr.query(memberUri, projection, null, null, null);
+ StringBuilder buf = new StringBuilder();
+ if(c != null) {
+ while(c.moveToNext()) {
+ buf.append(c.getString(0));
+ if(!c.isLast()) {
+ buf.append(',');
+ }
+ }
+ c.close();
+ }
+ mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
+ } else {
+ mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
+ }
+ }
+
+ private void setStatusIcon() {
+ if (mType == Im.Contacts.TYPE_GROUP) {
+ // hide the status icon for group chat.
+ mStatusIcon.setVisibility(GONE);
+ } else {
+ mStatusIcon.setVisibility(VISIBLE);
+ BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
+ int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
+ mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
+ }
+ }
+
+ public void bindChat(long chatId) {
+ if (mCursor != null) {
+ mCursor.deactivate();
+ }
+ Uri contactUri = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, chatId);
+ mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null);
+ if (mCursor == null || !mCursor.moveToFirst()) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("Failed to query chat: " + chatId);
+ }
+ mScreen.finish();
+ return;
+ } else {
+ mChatSession = getChatSession(mCursor);
+ updateChat();
+ registerChatListener();
+ }
+ }
+
+ public void bindInvitation(long invitationId) {
+ Uri uri = ContentUris.withAppendedId(Im.Invitation.CONTENT_URI, invitationId);
+ ContentResolver cr = mScreen.getContentResolver();
+ Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null);
+ if (cursor == null || !cursor.moveToFirst()) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("Failed to query invitation: " + invitationId);
+ }
+ mScreen.finish();
+ } else {
+ setViewType(VIEW_TYPE_INVITATION);
+
+ mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
+ mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
+ String sender = cursor.getString(INVITATION_SENDER_COLUMN);
+
+ TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation);
+ mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender));
+ mTitle.setText(mContext.getString(R.string.chat_with, sender));
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ public void bindSubscription(long providerId, String from) {
+ mProviderId = providerId;
+ mUserName = from;
+
+ setViewType(VIEW_TYPE_SUBSCRIPTION);
+
+ TextView text = (TextView)findViewById(R.id.txtSubscription);
+ String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from);
+ text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr));
+ mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr));
+ }
+
+ void acceptInvitation() {
+ try {
+
+ IImConnection conn = mApp.getConnection(mProviderId);
+ if (conn != null) {
+ // register a chat session listener and wait for a group chat
+ // session to be created after we accept the invitation.
+ registerChatSessionListener();
+ conn.acceptInvitation(mInvitationId);
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+
+ void declineInvitation() {
+ try {
+ IImConnection conn = mApp.getConnection(mProviderId);
+ if (conn != null) {
+ conn.rejectInvitation(mInvitationId);
+ }
+ mScreen.finish();
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+
+ void approveSubscription() {
+ IImConnection conn = mApp.getConnection(mProviderId);
+ try {
+ IContactListManager manager = conn.getContactListManager();
+ manager.approveSubscription(mUserName);
+ } catch (RemoteException ex) {
+ mHandler.showServiceErrorAlert();
+ }
+ mScreen.finish();
+ }
+
+ void declineSubscription() {
+ IImConnection conn = mApp.getConnection(mProviderId);
+ try {
+ IContactListManager manager = conn.getContactListManager();
+ manager.declineSubscription(mUserName);
+ } catch (RemoteException ex) {
+ mHandler.showServiceErrorAlert();
+ }
+ mScreen.finish();
+ }
+
+ private void setViewType(int type) {
+ mViewType = type;
+ if (type == VIEW_TYPE_CHAT) {
+ findViewById(R.id.invitationPanel).setVisibility(GONE);
+ findViewById(R.id.subscription).setVisibility(GONE);
+ setChatViewEnabled(true);
+ } else if (type == VIEW_TYPE_INVITATION) {
+ setChatViewEnabled(false);
+ findViewById(R.id.invitationPanel).setVisibility(VISIBLE);
+ findViewById(R.id.btnAccept).requestFocus();
+ } else if (type == VIEW_TYPE_SUBSCRIPTION) {
+ setChatViewEnabled(false);
+ findViewById(R.id.subscription).setVisibility(VISIBLE);
+ findViewById(R.id.btnApproveSubscription).requestFocus();
+ }
+ }
+
+ private void setChatViewEnabled(boolean enabled) {
+ mEdtInput.setEnabled(enabled);
+ mSendButton.setEnabled(enabled);
+ if (enabled) {
+ mEdtInput.requestFocus();
+ } else {
+ mHistory.setAdapter(null);
+ }
+ }
+
+ private void markAsRead() {
+ ContentValues values = new ContentValues(1);
+ values.put(Im.Chats.LAST_UNREAD_MESSAGE, (String)null);
+
+ ContentResolver cr = mContext.getContentResolver();
+ Uri uri = ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId);
+ cr.update(uri, values, null, null);
+ }
+
+ private void startQuery() {
+ if (mQueryHandler == null) {
+ mQueryHandler = new QueryHandler(mContext);
+ } else {
+ // Cancel any pending queries
+ mQueryHandler.cancelOperation(QUERY_TOKEN);
+ }
+
+ Uri uri;
+ if (Im.Contacts.TYPE_GROUP == mType) {
+ uri = ContentUris.withAppendedId(Im.GroupMessages.CONTENT_URI_GROUP_MESSAGES_BY, mChatId);
+ } else {
+ uri = Im.Messages.getContentUriByContact(mProviderId, mAccountId, mUserName);
+ }
+
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("queryCursor: uri=" + uri);
+ }
+
+ mQueryHandler.startQuery(QUERY_TOKEN, null,
+ uri,
+ null,
+ null /* selection */,
+ null /* selection args */,
+ null);
+ }
+
+ void scheduleRequery(long interval) {
+ if (mRequeryCallback == null) {
+ mRequeryCallback = new RequeryCallback();
+ } else {
+ mHandler.removeCallbacks(mRequeryCallback);
+ }
+
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("scheduleRequery");
+ }
+ mHandler.postDelayed(mRequeryCallback, interval);
+ }
+
+ void cancelRequery() {
+ if (mRequeryCallback != null) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("cancelRequery");
+ }
+ mHandler.removeCallbacks(mRequeryCallback);
+ mRequeryCallback = null;
+ }
+ }
+
+ void requeryCursor() {
+ if (mMessageAdapter.isScrolling()) {
+ mMessageAdapter.setNeedRequeryCursor(true);
+ return;
+ }
+ // TODO: async query?
+ Cursor cursor = getMessageCursor();
+ if (cursor != null) {
+ cursor.requery();
+ }
+ }
+
+ private Cursor getMessageCursor() {
+ return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
+ }
+
+ public void insertSmiley(String smiley) {
+ mEdtInput.append(mMarkup.applyEmoticons(smiley));
+ }
+
+ public void closeChatSession() {
+ if (mChatSession != null) {
+ try {
+ mChatSession.leave();
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ } else {
+ // the conversation is already closed, clear data in database
+ ContentResolver cr = mContext.getContentResolver();
+ cr.delete(ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId),
+ null, null);
+ }
+ mScreen.finish();
+ }
+
+ public void viewProfile() {
+ Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, mChatId);
+ Intent intent = new Intent(Intent.ACTION_VIEW, data);
+ mScreen.startActivity(intent);
+ }
+
+ public void blockContact() {
+ // TODO: unify with codes in ContactListView
+ DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
+ public void onClick(DialogInterface dialog, int whichButton) {
+ try {
+ IImConnection conn = mApp.getConnection(mProviderId);
+ IContactListManager manager = conn.getContactListManager();
+ manager.blockContact(mUserName);
+ mScreen.finish();
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ };
+
+ Resources r = getResources();
+
+ // The positive button is deliberately set as no so that
+ // the no is the default value
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.confirm)
+ .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
+ .setPositiveButton(R.string.no, null) // default button
+ .setNegativeButton(R.string.yes, confirmListener)
+ .setCancelable(false)
+ .show();
+ }
+
+ public long getProviderId() {
+ return mProviderId;
+ }
+
+ public long getAccountId() {
+ return mAccountId;
+ }
+
+ public String getUserName() {
+ return mUserName;
+ }
+
+ public long getChatId () {
+ try {
+ return mChatSession == null ? -1 : mChatSession.getId();
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ return -1;
+ }
+ }
+
+ public IChatSession getCurrentChatSession() {
+ return mChatSession;
+ }
+
+ private IChatSessionManager getChatSessionManager(long providerId) {
+ if (mChatSessionMgr == null) {
+ IImConnection conn = mApp.getConnection(providerId);
+ if (conn != null) {
+ try {
+ mChatSessionMgr = conn.getChatSessionManager();
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ }
+ return mChatSessionMgr;
+ }
+
+ private IChatSession getChatSession(Cursor cursor) {
+ long providerId = cursor.getLong(PROVIDER_COLUMN);
+ String username = cursor.getString(USERNAME_COLUMN);
+
+ IChatSessionManager sessionMgr = getChatSessionManager(providerId);
+ if (sessionMgr != null) {
+ try {
+ return sessionMgr.getChatSession(username);
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ return null;
+ }
+
+ boolean isGroupChat() {
+ return Im.Contacts.TYPE_GROUP == mType;
+ }
+
+ void sendMessage() {
+ String msg = mEdtInput.getText().toString();
+ if (mChatSession != null && !TextUtils.isEmpty(msg.trim())) {
+ try {
+ mChatSession.sendMessage(msg);
+ mEdtInput.setText("");
+ mEdtInput.requestFocus();
+ requeryCursor();
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ }
+
+ void registerChatListener() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("registerChatListener");
+ }
+ try {
+ if (mChatSession != null) {
+ mChatSession.registerChatListener(mChatListener);
+ }
+ IImConnection conn = mApp.getConnection(mProviderId);
+ if (conn != null) {
+ IContactListManager listMgr = conn.getContactListManager();
+ listMgr.registerContactListListener(mContactListListener);
+ }
+ mApp.dismissNotifications(mProviderId);
+ } catch (RemoteException e) {
+ Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
+ }
+ }
+
+ void unregisterChatListener() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("unregisterChatListener");
+ }
+ try {
+ if (mChatSession != null) {
+ mChatSession.unregisterChatListener(mChatListener);
+ }
+ IImConnection conn = mApp.getConnection(mProviderId);
+ if (conn != null) {
+ IContactListManager listMgr = conn.getContactListManager();
+ listMgr.unregisterContactListListener(mContactListListener);
+ }
+ } catch (RemoteException e) {
+ Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
+ }
+ }
+
+ void registerChatSessionListener() {
+ IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
+ if (sessionMgr != null) {
+ mChatSessionListener = new ChatSessionListener();
+ try {
+ sessionMgr.registerChatSessionListener(mChatSessionListener);
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ }
+
+ void unregisterChatSessionListener() {
+ if (mChatSessionListener != null) {
+ try {
+ IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
+ sessionMgr.unregisterChatSessionListener(mChatSessionListener);
+ // We unregister the listener when the chat session we are
+ // waiting for has been created or the activity is stopped.
+ // Clear the listener so that we won't unregister the listener
+ // twice.
+ mChatSessionListener = null;
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ }
+
+ void updateWarningView() {
+ int visibility = View.GONE;
+ int iconVisibility = View.GONE;
+ String message = null;
+ boolean isConnected;
+
+ try {
+ IImConnection conn = mApp.getConnection(mProviderId);
+ isConnected = (conn == null) ? false
+ : conn.getState() != ImConnection.SUSPENDED;
+ } catch (RemoteException e) {
+ // do nothing
+ return;
+ }
+
+ if (isConnected) {
+ if (mType == Im.Contacts.TYPE_TEMPORARY) {
+ visibility = View.VISIBLE;
+ message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
+ } else if (mPresenceStatus == Im.Presence.OFFLINE) {
+ visibility = View.VISIBLE;
+ message = mContext.getString(R.string.contact_offline_warning, mNickName);
+ }
+ } else {
+ visibility = View.VISIBLE;
+ iconVisibility = View.VISIBLE;
+ message = mContext.getString(R.string.disconnected_warning);
+ }
+
+ mStatusWarningView.setVisibility(visibility);
+ if (visibility == View.VISIBLE) {
+ mWarningIcon.setVisibility(iconVisibility);
+ mWarningText.setText(message);
+ }
+ }
+
+ private final class ChatViewHandler extends SimpleAlertHandler {
+ public ChatViewHandler() {
+ super(mScreen);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ long providerId = ((long)msg.arg1 << 32) | msg.arg2;
+ if (providerId != mProviderId) {
+ return;
+ }
+
+ switch(msg.what) {
+ case ImApp.EVENT_CONNECTION_LOGGED_IN:
+ log("Connection resumed");
+ updateWarningView();
+ return;
+ case ImApp.EVENT_CONNECTION_SUSPENDED:
+ log("Connection suspended");
+ updateWarningView();
+ return;
+ }
+
+ super.handleMessage(msg);
+ }
+ }
+
+ class ChatSessionListener extends ChatSessionListenerAdapter {
+ @Override
+ public void onChatSessionCreated(IChatSession session) {
+ try {
+ if (session.isGroupChatSession()) {
+ final long id = session.getId();
+ unregisterChatSessionListener();
+ mHandler.post(new Runnable() {
+ public void run() {
+ bindChat(id);
+ }});
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ }
+
+ public static class DeltaCursor implements Cursor {
+ static final String DELTA_COLUMN_NAME = "delta";
+
+ private Cursor mCursor;
+ private String[] mColumnNames;
+ private int mDateColumn = -1;
+ private int mDeltaColumn = -1;
+
+ DeltaCursor(Cursor cursor) {
+ mCursor = cursor;
+
+ String[] columnNames = cursor.getColumnNames();
+ int len = columnNames.length;
+
+ mColumnNames = new String[len + 1];
+
+ for (int i = 0 ; i < len ; i++) {
+ mColumnNames[i] = columnNames[i];
+ if (mColumnNames[i].equals(Im.BaseMessageColumns.DATE)) {
+ mDateColumn = i;
+ }
+ }
+
+ mDeltaColumn = len;
+ mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
+
+ //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
+ // mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
+ }
+
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ public int getPosition() {
+ return mCursor.getPosition();
+ }
+
+ public boolean move(int offset) {
+ return mCursor.move(offset);
+ }
+
+ public boolean moveToPosition(int position) {
+ return mCursor.moveToPosition(position);
+ }
+
+ public boolean moveToFirst() {
+ return mCursor.moveToFirst();
+ }
+
+ public boolean moveToLast() {
+ return mCursor.moveToLast();
+ }
+
+ public boolean moveToNext() {
+ return mCursor.moveToNext();
+ }
+
+ public boolean moveToPrevious() {
+ return mCursor.moveToPrevious();
+ }
+
+ public boolean isFirst() {
+ return mCursor.isFirst();
+ }
+
+ public boolean isLast() {
+ return mCursor.isLast();
+ }
+
+ public boolean isBeforeFirst() {
+ return mCursor.isBeforeFirst();
+ }
+
+ public boolean isAfterLast() {
+ return mCursor.isAfterLast();
+ }
+
+ public boolean deleteRow() {
+ return mCursor.deleteRow();
+ }
+
+ public int getColumnIndex(String columnName) {
+ if (DELTA_COLUMN_NAME.equals(columnName)) {
+ return mDeltaColumn;
+ }
+
+ int columnIndex = mCursor.getColumnIndex(columnName);
+ return columnIndex;
+ }
+
+ public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
+ if (DELTA_COLUMN_NAME.equals(columnName)) {
+ return mDeltaColumn;
+ }
+
+ return mCursor.getColumnIndexOrThrow(columnName);
+ }
+
+ public String getColumnName(int columnIndex) {
+ if (columnIndex == mDeltaColumn) {
+ return DELTA_COLUMN_NAME;
+ }
+
+ return mCursor.getColumnName(columnIndex);
+ }
+
+ public int getColumnCount() {
+ return mCursor.getColumnCount() + 1;
+ }
+
+ public boolean supportsUpdates() {
+ return mCursor.supportsUpdates();
+ }
+
+ public boolean hasUpdates() {
+ return mCursor.hasUpdates();
+ }
+
+ public boolean updateBlob(int columnIndex, byte[] value) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateBlob(columnIndex, value);
+ }
+
+ public boolean updateString(int columnIndex, String value) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateString(columnIndex, value);
+ }
+
+ public boolean updateShort(int columnIndex, short value) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateShort(columnIndex, value);
+ }
+
+ public boolean updateInt(int columnIndex, int value) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateInt(columnIndex, value);
+ }
+
+ public boolean updateLong(int columnIndex, long value) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateLong(columnIndex, value);
+ }
+
+ public boolean updateFloat(int columnIndex, float value) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateFloat(columnIndex, value);
+ }
+
+ public boolean updateDouble(int columnIndex, double value) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateDouble(columnIndex, value);
+ }
+
+ public boolean updateToNull(int columnIndex) {
+ if (columnIndex == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.updateToNull(columnIndex);
+ }
+
+ public boolean commitUpdates() {
+ return mCursor.commitUpdates();
+ }
+
+ public boolean commitUpdates(Map<? extends Long,
+ ? extends Map<String,Object>> values) {
+ return mCursor.commitUpdates(values);
+ }
+
+ public void abortUpdates() {
+ mCursor.abortUpdates();
+ }
+
+ public void deactivate() {
+ mCursor.deactivate();
+ }
+
+ public boolean requery() {
+ return mCursor.requery();
+ }
+
+ public void close() {
+ mCursor.close();
+ }
+
+ public boolean isClosed() {
+ return mCursor.isClosed();
+ }
+
+ public void registerContentObserver(ContentObserver observer) {
+ mCursor.registerContentObserver(observer);
+ }
+
+ public void unregisterContentObserver(ContentObserver observer) {
+ mCursor.unregisterContentObserver(observer);
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mCursor.registerDataSetObserver(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mCursor.unregisterDataSetObserver(observer);
+ }
+
+ public void setNotificationUri(ContentResolver cr, Uri uri) {
+ mCursor.setNotificationUri(cr, uri);
+ }
+
+ public boolean getWantsAllOnMoveCalls() {
+ return mCursor.getWantsAllOnMoveCalls();
+ }
+
+ public Bundle getExtras() {
+ return mCursor.getExtras();
+ }
+
+ public Bundle respond(Bundle extras) {
+ return mCursor.respond(extras);
+ }
+
+ public String[] getColumnNames() {
+ return mColumnNames;
+ }
+
+ private void checkPosition() {
+ int pos = mCursor.getPosition();
+ int count = mCursor.getCount();
+
+ if (-1 == pos || count == pos) {
+ throw new CursorIndexOutOfBoundsException(pos, count);
+ }
+ }
+
+ public byte[] getBlob(int column) {
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ return null;
+ }
+
+ return mCursor.getBlob(column);
+ }
+
+ public String getString(int column) {
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ long value = getDeltaValue();
+ return Long.toString(value);
+ }
+
+ return mCursor.getString(column);
+ }
+
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+ checkPosition();
+
+ if (columnIndex == mDeltaColumn) {
+ long value = getDeltaValue();
+ String strValue = Long.toString(value);
+ int len = strValue.length();
+ char[] data = buffer.data;
+ if (data == null || data.length < len) {
+ buffer.data = strValue.toCharArray();
+ } else {
+ strValue.getChars(0, len, data, 0);
+ }
+ buffer.sizeCopied = strValue.length();
+ } else {
+ mCursor.copyStringToBuffer(columnIndex, buffer);
+ }
+ }
+
+ public short getShort(int column) {
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ return (short)getDeltaValue();
+ }
+
+ return mCursor.getShort(column);
+ }
+
+ public int getInt(int column) {
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ return (int)getDeltaValue();
+ }
+
+ return mCursor.getInt(column);
+ }
+
+ public long getLong(int column) {
+ //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ return getDeltaValue();
+ }
+
+ return mCursor.getLong(column);
+ }
+
+ public float getFloat(int column) {
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ return getDeltaValue();
+ }
+
+ return mCursor.getFloat(column);
+ }
+
+ public double getDouble(int column) {
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ return getDeltaValue();
+ }
+
+ return mCursor.getDouble(column);
+ }
+
+ public boolean isNull(int column) {
+ checkPosition();
+
+ if (column == mDeltaColumn) {
+ return false;
+ }
+
+ return mCursor.isNull(column);
+ }
+
+ private long getDeltaValue() {
+ int pos = mCursor.getPosition();
+ //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
+
+ long t2, t1;
+
+ if (pos == getCount()-1) {
+ t1 = mCursor.getLong(mDateColumn);
+ t2 = System.currentTimeMillis();
+ } else {
+ mCursor.moveToPosition(pos + 1);
+ t2 = mCursor.getLong(mDateColumn);
+ mCursor.moveToPosition(pos);
+ t1 = mCursor.getLong(mDateColumn);
+ }
+
+ return t2 - t1;
+ }
+ }
+
+ private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
+ private int mScrollState;
+ private boolean mNeedRequeryCursor;
+
+ private int mContactColumn;
+ private int mBodyColumn;
+ private int mDateColumn;
+ private int mTypeColumn;
+ private int mErrCodeColumn;
+ private int mDeltaColumn;
+ private ChatBackgroundMaker mBgMaker;
+
+ private LayoutInflater mInflater;
+
+ public MessageAdapter(Activity context, Cursor c) {
+ super(context, c, false);
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mBgMaker = new ChatBackgroundMaker(context);
+ if (c != null) {
+ resolveColumnIndex(c);
+ }
+ }
+
+ private void resolveColumnIndex(Cursor c) {
+ mContactColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.CONTACT);
+ mBodyColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.BODY);
+ mDateColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.DATE);
+ mTypeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.TYPE);
+ mErrCodeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.ERROR_CODE);
+ mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
+ }
+
+ @Override
+ public void changeCursor(Cursor cursor) {
+ super.changeCursor(cursor);
+ if (cursor != null) {
+ resolveColumnIndex(cursor);
+ }
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.new_message_item, parent, false);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ MessageView chatMsgView = (MessageView) view;
+
+ int type = cursor.getInt(mTypeColumn);
+ String contact = isGroupChat() ? cursor.getString(mContactColumn) : mNickName;
+ String body = cursor.getString(mBodyColumn);
+ long delta = cursor.getLong(mDeltaColumn);
+ boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
+ Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;
+
+ switch (type) {
+ case Im.MessageType.INCOMING:
+ chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
+ break;
+
+ case Im.MessageType.OUTGOING:
+ case Im.MessageType.POSTPONED:
+ int errCode = cursor.getInt(mErrCodeColumn);
+ if (errCode != 0) {
+ chatMsgView.bindErrorMessage(errCode);
+ } else {
+ chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
+ }
+ break;
+
+ default:
+ chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
+ }
+ if (!isScrolling()) {
+ mBgMaker.setBackground(chatMsgView, contact, type);
+ }
+
+ // if showTimeStamp is false for the latest message, then set a timer to query the
+ // cursor again in a minute, so we can update the last message timestamp if no new
+ // message is received
+ if (cursor.getPosition() == cursor.getCount()-1) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("delta = " + delta + ", showTs=" + showTimeStamp);
+ }
+ if (!showTimeStamp) {
+ scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
+ } else {
+ cancelRequery();
+ }
+ }
+ }
+
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ // do nothing
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ int oldState = mScrollState;
+ mScrollState = scrollState;
+ if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
+ if (mNeedRequeryCursor) {
+ requeryCursor();
+ } else {
+ notifyDataSetChanged();
+ }
+ }
+ }
+
+ boolean isScrolling() {
+ return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
+ }
+
+ void setNeedRequeryCursor(boolean requeryCursor) {
+ mNeedRequeryCursor = requeryCursor;
+ }
+ }
+}
diff --git a/src/com/android/im/app/ChooseAccountActivity.java b/src/com/android/im/app/ChooseAccountActivity.java
new file mode 100644
index 0000000..f9e9ad7
--- /dev/null
+++ b/src/com/android/im/app/ChooseAccountActivity.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import android.app.ListActivity;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.util.Log;
+import android.util.AttributeSet;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.CursorAdapter;
+
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.engine.ImConnection;
+import com.android.im.plugin.BrandingResourceIDs;
+import com.android.im.service.ImServiceConstants;
+
+public class ChooseAccountActivity extends ListActivity implements
+ View.OnCreateContextMenuListener {
+
+ private static final int ID_SIGN_IN = Menu.FIRST + 1;
+ private static final int ID_SIGN_OUT = Menu.FIRST + 2;
+ private static final int ID_EDIT_ACCOUNT = Menu.FIRST + 3;
+ private static final int ID_REMOVE_ACCOUNT = Menu.FIRST + 4;
+ private static final int ID_SIGN_OUT_ALL = Menu.FIRST + 5;
+ private static final int ID_ADD_ACCOUNT = Menu.FIRST + 6;
+ private static final int ID_VIEW_CONTACT_LIST = Menu.FIRST + 7;
+ private static final int ID_SETTINGS = Menu.FIRST + 8;
+
+ ImApp mApp;
+
+ MyHandler mHandler;
+ private ProviderAdapter mAdapter;
+ private Cursor mProviderCursor;
+
+ private static final String[] PROVIDER_PROJECTION = {
+ Im.Provider._ID,
+ Im.Provider.NAME,
+ Im.Provider.FULLNAME,
+ Im.Provider.ACTIVE_ACCOUNT_ID,
+ Im.Provider.ACTIVE_ACCOUNT_USERNAME,
+ Im.Provider.ACTIVE_ACCOUNT_PW,
+ };
+
+ static final int PROVIDER_ID_COLUMN = 0;
+ static final int PROVIDER_NAME_COLUMN = 1;
+ static final int PROVIDER_FULLNAME_COLUMN = 2;
+ static final int ACTIVE_ACCOUNT_ID_COLUMN = 3;
+ static final int ACTIVE_ACCOUNT_USERNAME_COLUMN = 4;
+ static final int ACTIVE_ACCOUNT_PW_COLUMN = 5;
+
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setTitle(R.string.choose_account_title);
+
+ mApp = ImApp.getApplication(this);
+
+ mHandler = new MyHandler();
+ mApp.registerForBroadcastEvent(ImApp.EVENT_SERVICE_CONNECTED, mHandler);
+ mApp.registerForConnEvents(mHandler);
+
+ mApp.startImServiceIfNeed();
+
+ // (for open source) for now exclude GTalk on the landing page until we can load
+ // it in an abstract way
+ String selection = "providers.name != ?";
+ String[] selectionArgs = new String[] { Im.ProviderNames.GTALK };
+ mProviderCursor = managedQuery(Im.Provider.CONTENT_URI_WITH_ACCOUNT,
+ PROVIDER_PROJECTION,
+ selection,
+ selectionArgs,
+ Im.Provider.DEFAULT_SORT_ORDER);
+ mAdapter = new ProviderAdapter(this, mProviderCursor);
+ mApp.callWhenServiceConnected(mHandler, new Runnable() {
+ public void run() {
+ setListAdapter(mAdapter);
+ }
+ });
+ registerForContextMenu(getListView());
+ }
+
+ private boolean allAccountsSignedOut() {
+ if (!mProviderCursor.moveToFirst()) return true;
+
+ do {
+ long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+ if (isSignedIn(accountId)) return false;
+ } while (mProviderCursor.moveToNext()) ;
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(ID_SIGN_OUT_ALL).setVisible(!allAccountsSignedOut());
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, ID_SIGN_OUT_ALL, 0, R.string.menu_sign_out_all)
+ .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case ID_SIGN_OUT_ALL:
+ // Sign out MSN/AIM/YAHOO account
+ if (mApp.serviceConnected()) {
+ for (IImConnection conn : mApp.getActiveConnections()) {
+ try {
+ conn.logout();
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ } catch (ClassCastException e) {
+ Log.e(ImApp.LOG_TAG, "bad menuInfo", e);
+ return;
+ }
+
+ Cursor providerCursor = (Cursor) getListAdapter().getItem(info.position);
+ menu.setHeaderTitle(providerCursor.getString(PROVIDER_FULLNAME_COLUMN));
+
+ if (providerCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) {
+ menu.add(0, ID_ADD_ACCOUNT, 0, R.string.menu_add_account);
+ return;
+ }
+ long providerId = providerCursor.getLong(PROVIDER_ID_COLUMN);
+ long accountId = providerCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+ boolean isLoggingIn = isSigningIn(accountId);
+ boolean isLoggedIn = isSignedIn(accountId);
+
+ if (!isLoggedIn) {
+ menu.add(0, ID_SIGN_IN, 0, R.string.sign_in)
+ .setIcon(R.drawable.ic_menu_login);
+ } else {
+ BrandingResources brandingRes = mApp.getBrandingResource(providerId);
+ menu.add(0, ID_VIEW_CONTACT_LIST, 0,
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_CONTACT_LIST));
+ menu.add(0, ID_SIGN_OUT, 0, R.string.menu_sign_out)
+ .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ }
+
+ if (!isLoggingIn && !isLoggedIn) {
+ menu.add(0, ID_EDIT_ACCOUNT, 0, R.string.menu_edit_account)
+ .setIcon(android.R.drawable.ic_menu_edit);
+ menu.add(0, ID_REMOVE_ACCOUNT, 0, R.string.menu_remove_account)
+ .setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ // always add a settings menu item
+ menu.add(0, ID_SETTINGS, 0, R.string.menu_settings);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ } catch (ClassCastException e) {
+ Log.e(ImApp.LOG_TAG, "bad menuInfo", e);
+ return false;
+ }
+ int position = info.position;
+ long providerId = info.id;
+
+ switch (item.getItemId()) {
+ case ID_EDIT_ACCOUNT:
+ {
+ Cursor c = (Cursor)mAdapter.getItem(position);
+ if (c != null) {
+ Intent i = new Intent(ChooseAccountActivity.this, AccountActivity.class);
+ i.setAction(Intent.ACTION_EDIT);
+ i.setData(ContentUris.withAppendedId(Im.Account.CONTENT_URI,
+ c.getLong(ACTIVE_ACCOUNT_ID_COLUMN)));
+ c.close();
+ startActivity(i);
+ }
+ return true;
+ }
+
+ case ID_REMOVE_ACCOUNT:
+ {
+ Cursor c = (Cursor)mAdapter.getItem(position);
+ if (c != null) {
+ long accountId = c.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+ Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
+ getContentResolver().delete(accountUri, null, null);
+ // Requery the cursor to force refreshing screen
+ c.requery();
+ }
+ return true;
+ }
+
+ case ID_VIEW_CONTACT_LIST:
+ case ID_ADD_ACCOUNT:
+ case ID_SIGN_IN:
+ Intent i = mAdapter.intentForPosition(position);
+ if (i != null) {
+ startActivity(i);
+ }
+ return true;
+
+ case ID_SIGN_OUT:
+ // TODO: progress bar
+ IImConnection conn = mApp.getConnection(providerId);
+ if (conn != null) {
+ try {
+ conn.logout();
+ } catch (RemoteException e) {
+ }
+ }
+ return true;
+
+ case ID_SETTINGS:
+ Intent settingsIntent = mAdapter.settingsIntentForPosition(position);
+ if (settingsIntent != null) {
+ startActivity(settingsIntent);
+ }
+ return true;
+
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ startActivityAtPosition(position);
+ }
+
+ void startActivityAtPosition(int position) {
+ Intent i = mAdapter.intentForPosition(position);
+ if (i != null) {
+ startActivity(i);
+ }
+ }
+
+ static void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "[ChooseAccount]" + msg);
+ }
+
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+
+ mApp.startImServiceIfNeed();
+ mApp.registerForConnEvents(mHandler);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ mApp.unregisterForConnEvents(mHandler);
+ mApp.unregisterForBroadcastEvent(ImApp.EVENT_SERVICE_CONNECTED, mHandler);
+ mApp.stopImServiceIfInactive();
+ }
+
+ boolean isSigningIn(long accountId) {
+ IImConnection conn = mApp.getConnectionByAccount(accountId);
+ try {
+ return (conn == null) ? false : (conn.getState() == ImConnection.LOGGING_IN);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ boolean isSignedIn(long accountId) {
+ try {
+ IImConnection conn = mApp.getConnectionByAccount(accountId);
+ if (conn == null) {
+ return false;
+ }
+ int state = conn.getState();
+ return state == ImConnection.LOGGED_IN || state == ImConnection.SUSPENDED;
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+
+
+ private final class MyHandler extends SimpleAlertHandler {
+ public MyHandler() {
+ super(ChooseAccountActivity.this);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case ImApp.EVENT_CONNECTION_DISCONNECTED:
+ promptDisconnectedEvent(msg);
+ // fall through
+ case ImApp.EVENT_SERVICE_CONNECTED:
+ case ImApp.EVENT_CONNECTION_CREATED:
+ case ImApp.EVENT_CONNECTION_LOGGING_IN:
+ case ImApp.EVENT_CONNECTION_LOGGED_IN:
+ getListView().invalidateViews();
+ return;
+ }
+ super.handleMessage(msg);
+ }
+ }
+
+ private class ProviderListItemFactory implements LayoutInflater.Factory {
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ if (name != null && name.equals(ProviderListItem.class.getName())) {
+ return new ProviderListItem(context, ChooseAccountActivity.this);
+ }
+ return null;
+ }
+ }
+
+ private final class ProviderAdapter extends CursorAdapter {
+ private LayoutInflater mInflater;
+
+ public ProviderAdapter(Context context, Cursor c) {
+ super(context, c);
+ mInflater = LayoutInflater.from(context).cloneInContext(context);
+ mInflater.setFactory(new ProviderListItemFactory());
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ // create a custom view, so we can manage it ourselves. Mainly, we want to
+ // initialize the widget views (by calling getViewById()) in newView() instead of in
+ // bindView(), which can be called more often.
+ ProviderListItem view = (ProviderListItem) mInflater.inflate(
+ R.layout.account_view, parent, false);
+ view.init(cursor);
+ return view;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ((ProviderListItem) view).bindView(cursor);
+ }
+
+ public Intent intentForPosition(int position) {
+ Intent intent = null;
+
+ if (mCursor == null) {
+ return null;
+ }
+
+ mCursor.moveToPosition(position);
+ long providerId = mCursor.getLong(PROVIDER_ID_COLUMN);
+
+ if (mCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) {
+ // add account
+ intent = new Intent(ChooseAccountActivity.this, AccountActivity.class);
+ intent.setAction(Intent.ACTION_INSERT);
+ intent.setData(ContentUris.withAppendedId(Im.Provider.CONTENT_URI, providerId));
+ } else {
+ long accountId = mCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
+
+ IImConnection conn = mApp.getConnection(providerId);
+ int state = getConnState(conn);
+ if (state < ImConnection.LOGGED_IN ) {
+ Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
+ if (mCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) {
+ // no password, edit the account
+ intent = new Intent(ChooseAccountActivity.this, AccountActivity.class);
+ intent.setAction(Intent.ACTION_EDIT);
+ intent.setData(accountUri);
+ } else {
+ // intent for sign in
+ intent = new Intent(ChooseAccountActivity.this, SigningInActivity.class);
+ intent.setData(accountUri);
+ }
+ } else if (state == ImConnection.LOGGED_IN || state == ImConnection.SUSPENDED) {
+ intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClass(ChooseAccountActivity.this, ContactListActivity.class);
+ intent.putExtra(ImServiceConstants.EXTRA_INTENT_ACCOUNT_ID, accountId);
+ }
+ }
+ return intent;
+ }
+
+ public Intent settingsIntentForPosition(int position) {
+ Intent intent = null;
+
+ if (mCursor == null) {
+ return null;
+ }
+
+ mCursor.moveToPosition(position);
+ Long providerId = mCursor.getLong(PROVIDER_ID_COLUMN);
+ intent = new Intent(ChooseAccountActivity.this, SettingActivity.class);
+ intent.putExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, providerId);
+
+ return intent;
+ }
+
+ private int getConnState(IImConnection conn) {
+ try {
+ return conn == null ? ImConnection.DISCONNECTED : conn.getState();
+ } catch (RemoteException e) {
+ return ImConnection.DISCONNECTED;
+ }
+ }
+ }
+}
diff --git a/src/com/android/im/app/ContactListActivity.java b/src/com/android/im/app/ContactListActivity.java
new file mode 100644
index 0000000..e0e62fe
--- /dev/null
+++ b/src/com/android/im/app/ContactListActivity.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
+import com.android.im.service.ImServiceConstants;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+import java.util.Observable;
+import java.util.Observer;
+
+public class ContactListActivity extends Activity implements View.OnCreateContextMenuListener{
+
+ private static final int MENU_START_CONVERSATION = Menu.FIRST;
+ private static final int MENU_VIEW_PROFILE = Menu.FIRST + 1;
+ private static final int MENU_BLOCK_CONTACT = Menu.FIRST + 2;
+ private static final int MENU_DELETE_CONTACT = Menu.FIRST + 3;
+ private static final int MENU_END_CONVERSATION = Menu.FIRST + 4;
+
+ private static final String FILTER_STATE_KEY = "Filtering";
+
+ ImApp mApp;
+
+ long mProviderId;
+ long mAccountId;
+ IImConnection mConn;
+ ContactListView mContactListView;
+ ContactListFilterView mFilterView;
+ SimpleAlertHandler mHandler;
+
+ ContextMenuHandler mContextMenuHandler;
+
+ boolean mIsFiltering;
+
+ Im.ProviderSettings.QueryMap mSettingMap;
+ boolean mDestroyed;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+
+ LayoutInflater inflate = getLayoutInflater();
+ mContactListView = (ContactListView) inflate.inflate(
+ R.layout.contact_list_view, null);
+
+ setContentView(mContactListView);
+
+ Intent intent = getIntent();
+ mAccountId = intent.getLongExtra(ImServiceConstants.EXTRA_INTENT_ACCOUNT_ID, -1);
+ if (mAccountId == -1) {
+ finish();
+ return;
+ }
+ mApp = ImApp.getApplication(this);
+
+ ContentResolver cr = getContentResolver();
+ Cursor c = cr.query(ContentUris.withAppendedId(Im.Account.CONTENT_URI, mAccountId),
+ null, null, null, null);
+ if (c == null) {
+ finish();
+ return;
+ }
+ if (!c.moveToFirst()) {
+ c.close();
+ finish();
+ return;
+ }
+
+ mProviderId = c.getLong(c.getColumnIndexOrThrow(Im.Account.PROVIDER));
+ mHandler = new MyHandler(this);
+ String username = c.getString(c.getColumnIndexOrThrow(Im.Account.USERNAME));
+
+ BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
+ setTitle(brandingRes.getString(BrandingResourceIDs.STRING_BUDDY_LIST_TITLE, username));
+ getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON,
+ brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_LOGO));
+
+ mSettingMap = new Im.ProviderSettings.QueryMap(getContentResolver(), mProviderId, true, null);
+
+ mApp.callWhenServiceConnected(mHandler, new Runnable(){
+ public void run() {
+ if (!mDestroyed) {
+ mApp.dismissNotifications(mProviderId);
+ mConn = mApp.getConnection(mProviderId);
+ mContactListView.setConnection(mConn);
+ mContactListView.setHideOfflineContacts(mSettingMap.getHideOfflineContacts());
+ }
+ }
+ });
+
+ mContextMenuHandler = new ContextMenuHandler();
+ mContactListView.getListView().setOnCreateContextMenuListener(this);
+
+ mSettingMap.addObserver(new Observer() {
+ public void update(Observable observed, Object updateData) {
+ if (!mDestroyed) {
+ mContactListView.setHideOfflineContacts(mSettingMap.getHideOfflineContacts());
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.contact_list_menu, menu);
+
+ BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
+ menu.findItem(R.id.menu_invite_user).setTitle(
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_ADD_CONTACT));
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_invite_user:
+ Intent i = new Intent(ContactListActivity.this, AddContactActivity.class);
+ i.putExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, mProviderId);
+ i.putExtra(ImServiceConstants.EXTRA_INTENT_LIST_NAME,
+ mContactListView.getSelectedContactList());
+ startActivity(i);
+ return true;
+
+ case R.id.menu_blocked_contacts:
+ Uri.Builder builder = Im.BlockedList.CONTENT_URI.buildUpon();
+ ContentUris.appendId(builder, mProviderId);
+ ContentUris.appendId(builder, mAccountId);
+ startActivity(new Intent(Intent.ACTION_VIEW, builder.build()));
+ return true;
+
+ case R.id.menu_view_accounts:
+ Intent intent = new Intent(this, ChooseAccountActivity.class);
+ startActivity(intent);
+ finish();
+ return true;
+
+ case R.id.menu_settings:
+ intent = new Intent(this, SettingActivity.class);
+ intent.putExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, mProviderId);
+ startActivity(intent);
+ return true;
+
+ case R.id.menu_sign_out:
+ try {
+ mConn.logout();
+ } catch (RemoteException e) {
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(FILTER_STATE_KEY, mIsFiltering);
+
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ boolean isFiltering = savedInstanceState.getBoolean(FILTER_STATE_KEY);
+ if (isFiltering) {
+ showFilterView();
+ }
+ super.onRestoreInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+
+ boolean handled = false;
+ if (mIsFiltering) {
+ handled = mFilterView.dispatchKeyEvent(event);
+ if (!handled && (KeyEvent.KEYCODE_BACK == keyCode)
+ && (KeyEvent.ACTION_DOWN == event.getAction())) {
+ showContactListView();
+ handled = true;
+ }
+ } else {
+ handled = mContactListView.dispatchKeyEvent(event);
+ if (!handled && isReadable(keyCode, event)
+ && (KeyEvent.ACTION_DOWN == event.getAction())) {
+ showFilterView();
+ handled = mFilterView.dispatchKeyEvent(event);
+ }
+ }
+
+ if (!handled) {
+ handled = super.dispatchKeyEvent(event);
+ }
+
+ return handled;
+ }
+
+ private static boolean isReadable(int keyCode, KeyEvent event) {
+ if (KeyEvent.isModifierKey(keyCode) || event.isSystem()) {
+ return false;
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_ENTER:
+ return false;
+ }
+
+ return true;
+ }
+
+ private void showFilterView() {
+ if (mFilterView == null ) {
+ mFilterView = (ContactListFilterView)getLayoutInflater().inflate(
+ R.layout.contact_list_filter_view, null);
+ mFilterView.getListView().setOnCreateContextMenuListener(this);
+ }
+ Uri uri = mSettingMap.getHideOfflineContacts() ? Im.Contacts.CONTENT_URI_ONLINE_CONTACTS_BY
+ : Im.Contacts.CONTENT_URI_CONTACTS_BY;
+ uri = ContentUris.withAppendedId(uri, mProviderId);
+ uri = ContentUris.withAppendedId(uri, mAccountId);
+ mFilterView.doFilter(uri, null);
+
+ setContentView(mFilterView);
+ mFilterView.requestFocus();
+ mIsFiltering = true;
+ }
+
+ void showContactListView() {
+ if (mIsFiltering) {
+ setContentView(mContactListView);
+ mContactListView.requestFocus();
+ mContactListView.invalidate();
+ mIsFiltering = false;
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mApp.unregisterForConnEvents(mHandler);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mApp.registerForConnEvents(mHandler);
+ }
+
+ @Override
+ protected void onDestroy() {
+ mDestroyed = true;
+ // set connection to null to unregister listeners.
+ mContactListView.setConnection(null);
+ if (mSettingMap != null) {
+ mSettingMap.close();
+ }
+ super.onDestroy();
+ }
+
+ static void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "<ContactListActivity> " +msg);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ boolean chatSelected = false;
+ boolean contactSelected = false;
+ Cursor contactCursor;
+ if (mIsFiltering) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+ mContextMenuHandler.mPosition = info.position;
+ contactSelected = true;
+ contactCursor = mFilterView.getContactAtPosition(info.position);
+ } else {
+ ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
+ mContextMenuHandler.mPosition = info.packedPosition;
+ contactSelected = mContactListView.isContactAtPosition(info.packedPosition);
+ chatSelected = mContactListView.isConversationAtPosition(info.packedPosition);
+ contactCursor = mContactListView.getContactAtPosition(info.packedPosition);
+ }
+
+ boolean allowBlock = true;
+ if (contactCursor != null) {
+ //XXX HACK: Yahoo! doesn't allow to block a friend. We can only block a temporary contact.
+ ProviderDef provider = mApp.getProvider(mProviderId);
+ if (Im.ProviderNames.YAHOO.equals(provider.mName)) {
+ int type = contactCursor.getInt(contactCursor.getColumnIndexOrThrow(Im.Contacts.TYPE));
+ allowBlock = (type == Im.Contacts.TYPE_TEMPORARY);
+ }
+
+ int nickNameIndex = contactCursor.getColumnIndexOrThrow(Im.Contacts.NICKNAME);
+
+ menu.setHeaderTitle(contactCursor.getString(nickNameIndex));
+ }
+
+ BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
+ String menu_end_conversation = brandingRes.getString(
+ BrandingResourceIDs.STRING_MENU_END_CHAT);
+ String menu_view_profile = brandingRes.getString(
+ BrandingResourceIDs.STRING_MENU_VIEW_PROFILE);
+ String menu_block_contact = brandingRes.getString(
+ BrandingResourceIDs.STRING_MENU_BLOCK_CONTACT);
+ String menu_start_conversation = brandingRes.getString(
+ BrandingResourceIDs.STRING_MENU_START_CHAT);
+ String menu_delete_contact = brandingRes.getString(
+ BrandingResourceIDs.STRING_MENU_DELETE_CONTACT);
+
+ if (chatSelected) {
+ menu.add(0, MENU_END_CONVERSATION, 0, menu_end_conversation)
+ .setIcon(R.drawable.ic_menu_end_conversation)
+ .setOnMenuItemClickListener(mContextMenuHandler);
+ menu.add(0, MENU_VIEW_PROFILE, 0, menu_view_profile)
+ .setIcon(R.drawable.ic_menu_my_profile)
+ .setOnMenuItemClickListener(mContextMenuHandler);
+ if (allowBlock) {
+ menu.add(0, MENU_BLOCK_CONTACT, 0, menu_block_contact)
+ .setIcon(R.drawable.ic_menu_block)
+ .setOnMenuItemClickListener(mContextMenuHandler);
+ }
+ } else if (contactSelected) {
+ menu.add(0, MENU_START_CONVERSATION, 0, menu_start_conversation)
+ .setIcon(R.drawable.ic_menu_start_conversation)
+ .setOnMenuItemClickListener(mContextMenuHandler);
+ menu.add(0, MENU_VIEW_PROFILE, 0, menu_view_profile)
+ .setIcon(R.drawable.ic_menu_view_profile)
+ .setOnMenuItemClickListener(mContextMenuHandler);
+ if (allowBlock) {
+ menu.add(0, MENU_BLOCK_CONTACT, 0, menu_block_contact)
+ .setIcon(R.drawable.ic_menu_block)
+ .setOnMenuItemClickListener(mContextMenuHandler);
+ }
+ menu.add(0, MENU_DELETE_CONTACT, 0, menu_delete_contact)
+ .setIcon(android.R.drawable.ic_menu_delete)
+ .setOnMenuItemClickListener(mContextMenuHandler);
+ }
+ }
+
+ final class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
+ long mPosition;
+
+ public boolean onMenuItemClick(MenuItem item) {
+ Cursor c;
+ if (mIsFiltering) {
+ c = mFilterView.getContactAtPosition((int)mPosition);
+ } else {
+ c = mContactListView.getContactAtPosition(mPosition);
+ }
+
+ switch (item.getItemId()) {
+ case MENU_START_CONVERSATION:
+ mContactListView.startChat(c);
+ break;
+ case MENU_VIEW_PROFILE:
+ mContactListView.viewContactPresence(c);
+ break;
+ case MENU_BLOCK_CONTACT:
+ mContactListView.blockContact(c);
+ break;
+ case MENU_DELETE_CONTACT:
+ mContactListView.removeContact(c);
+ break;
+ case MENU_END_CONVERSATION:
+ mContactListView.endChat(c);
+ break;
+ default:
+ return false;
+ }
+
+ if (mIsFiltering) {
+ showContactListView();
+ }
+ return true;
+ }
+ }
+
+ final class MyHandler extends SimpleAlertHandler {
+ public MyHandler(Activity activity) {
+ super(activity);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == ImApp.EVENT_CONNECTION_DISCONNECTED) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("Handle event connection disconnected.");
+ }
+ promptDisconnectedEvent(msg);
+ long providerId = ((long)msg.arg1 << 32) | msg.arg2;
+ if (providerId == mProviderId) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("Current connection disconnected, finish");
+ }
+ finish();
+ }
+ return;
+ }
+ super.handleMessage(msg);
+ }
+ }
+}
diff --git a/src/com/android/im/app/ContactListFilterView.java b/src/com/android/im/app/ContactListFilterView.java
new file mode 100644
index 0000000..5607cde
--- /dev/null
+++ b/src/com/android/im/app/ContactListFilterView.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.provider.Im;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Filter;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ResourceCursorAdapter;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.android.im.R;
+
+public class ContactListFilterView extends LinearLayout {
+
+ private ListView mContactListView;
+ private Filter mFilter;
+ private ContactAdapter mContactAdapter;
+
+ private Uri mUri;
+
+ public ContactListFilterView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+
+ mContactListView = (ListView) findViewById(R.id.filteredList);
+ mContactListView.setTextFilterEnabled(true);
+
+ mContactListView.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView parent, View view, int position,
+ long id) {
+ if (mContext instanceof ContactListActivity) {
+ ContactListActivity list = (ContactListActivity) mContext;
+ mContactListView.setSelection(position);
+ Cursor c = (Cursor) mContactListView.getSelectedItem();
+ list.mContactListView.startChat(c);
+ list.showContactListView();
+ }
+ }
+ });
+ }
+
+ public ListView getListView() {
+ return mContactListView;
+ }
+
+ public Cursor getContactAtPosition(int position) {
+ return (Cursor) mContactAdapter.getItem(position);
+ }
+
+ public void doFilter(Uri uri, String filterString) {
+ if (!uri.equals(mUri)) {
+ mUri = uri;
+
+ Cursor contactCursor = runQuery(filterString);
+
+ if (mContactAdapter == null) {
+ mContactAdapter = new ContactAdapter(mContext, contactCursor);
+ mFilter = mContactAdapter.getFilter();
+ mContactListView.setAdapter(mContactAdapter);
+ } else {
+ mContactAdapter.changeCursor(contactCursor);
+ }
+ } else {
+ mFilter.filter(filterString);
+ }
+ }
+
+ Cursor runQuery(CharSequence constraint) {
+ StringBuilder buf = new StringBuilder();
+
+ // exclude chatting contact
+ buf.append(Im.Chats.LAST_MESSAGE_DATE);
+ buf.append(" IS NULL");
+
+ if (constraint != null) {
+ buf.append(" AND ");
+ buf.append(Im.Contacts.NICKNAME);
+ buf.append(" LIKE ");
+ DatabaseUtils.appendValueToSql(buf, "%" + constraint + "%");
+ }
+
+ return mContext.getContentResolver().query(mUri, ContactView.CONTACT_PROJECTION,
+ buf == null ? null : buf.toString(), null, Im.Contacts.DEFAULT_SORT_ORDER);
+ }
+
+ private class ContactAdapter extends ResourceCursorAdapter {
+ private String mSearchString;
+
+ public ContactAdapter(Context context, Cursor cursor) {
+ super(context, R.layout.contact_view, cursor);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ContactView v = (ContactView) view;
+ v.setPadding(0, 0, 0, 0);
+ v.bind(cursor, mSearchString, false);
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ if (constraint != null) {
+ mSearchString = constraint.toString();
+ }
+ return ContactListFilterView.this.runQuery(constraint);
+ }
+ }
+
+}
diff --git a/src/com/android/im/app/ContactListTreeAdapter.java b/src/com/android/im/app/ContactListTreeAdapter.java
new file mode 100644
index 0000000..be8b2d9
--- /dev/null
+++ b/src/com/android/im/app/ContactListTreeAdapter.java
@@ -0,0 +1,743 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentQueryMap;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CursorTreeAdapter;
+import android.widget.TextView;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
+
+import java.util.ArrayList;
+import java.util.Observable;
+import java.util.Observer;
+
+public class ContactListTreeAdapter extends BaseExpandableListAdapter
+ implements AbsListView.OnScrollListener{
+
+ private static final String[] CONTACT_LIST_PROJECTION = {
+ Im.ContactList._ID,
+ Im.ContactList.NAME,
+ };
+
+ private static final int COLUMN_CONTACT_LIST_ID = 0;
+ private static final int COLUMN_CONTACT_LIST_NAME = 1;
+
+ Activity mActivity;
+ SimpleAlertHandler mHandler;
+ private LayoutInflater mInflate;
+ private long mProviderId;
+ long mAccountId;
+ Cursor mOngoingConversations;
+ Cursor mSubscriptions;
+ boolean mDataValid;
+ ListTreeAdapter mAdapter;
+ private boolean mHideOfflineContacts;
+
+ final MyContentObserver mContentObserver;
+ final MyDataSetObserver mDataSetObserver;
+
+ private ArrayList<Integer> mExpandedGroups;
+
+ private static final int TOKEN_CONTACT_LISTS = -1;
+ private static final int TOKEN_ONGOING_CONVERSATION = -2;
+ private static final int TOKEN_SUBSCRITPTION = -3;
+
+ private static final String NON_CHAT_AND_BLOCKED_CONTACTS = "("
+ + Im.Contacts.LAST_MESSAGE_DATE + " IS NULL) AND ("
+ + Im.Contacts.TYPE + "!=" + Im.Contacts.TYPE_BLOCKED + ")";
+
+ private static final String CONTACTS_SELECTION = Im.Contacts.CONTACTLIST
+ + "=? AND " + NON_CHAT_AND_BLOCKED_CONTACTS;
+
+ private static final String ONLINE_CONTACT_SELECTION = CONTACTS_SELECTION
+ + " AND "+ Im.Contacts.PRESENCE_STATUS + " != " + Im.Presence.OFFLINE;
+
+ static final void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "<ContactListAdapter>" + msg);
+ }
+
+ static final String[] CONTACT_COUNT_PROJECTION = {
+ Im.Contacts.CONTACTLIST,
+ Im.Contacts._COUNT,
+ };
+
+ ContentQueryMap mOnlineContactsCountMap;
+
+ // Async QueryHandler
+ private final class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(Context context) {
+ super(context.getContentResolver());
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor c) {
+ if(Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("onQueryComplete:token=" + token);
+ }
+
+ if (token == TOKEN_CONTACT_LISTS) {
+ mDataValid = true;
+ mAdapter.setGroupCursor(c);
+ } else if (token == TOKEN_ONGOING_CONVERSATION) {
+ setOngoingConversations(c);
+ notifyDataSetChanged();
+ } else if (token == TOKEN_SUBSCRITPTION) {
+ setSubscriptions(c);
+ notifyDataSetChanged();
+ } else {
+ int count = mAdapter.getGroupCount();
+ for (int pos = 0; pos < count; pos++) {
+ long listId = mAdapter.getGroupId(pos);
+ if (listId == token) {
+ mAdapter.setChildrenCursor(pos, c);
+ break;
+ }
+ }
+ }
+ }
+ }
+ private QueryHandler mQueryHandler;
+
+ private int mScrollState;
+
+ private boolean mAutoRequery;
+ private boolean mRequeryPending;
+
+ public ContactListTreeAdapter(IImConnection conn, Activity activity) {
+ mActivity = activity;
+ mInflate = activity.getLayoutInflater();
+ mHandler = new SimpleAlertHandler(activity);
+
+ mAdapter = new ListTreeAdapter(null);
+
+ mContentObserver = new MyContentObserver();
+ mDataSetObserver = new MyDataSetObserver();
+ mExpandedGroups = new ArrayList<Integer>();
+ mQueryHandler = new QueryHandler(activity);
+
+ changeConnection(conn);
+ }
+
+ public void changeConnection(IImConnection conn) {
+ mQueryHandler.cancelOperation(TOKEN_ONGOING_CONVERSATION);
+ mQueryHandler.cancelOperation(TOKEN_SUBSCRITPTION);
+ mQueryHandler.cancelOperation(TOKEN_CONTACT_LISTS);
+
+ synchronized (this) {
+ if (mOngoingConversations != null) {
+ mOngoingConversations.close();
+ mOngoingConversations = null;
+ }
+ if (mSubscriptions != null) {
+ mSubscriptions.close();
+ mSubscriptions = null;
+ }
+ if (mOnlineContactsCountMap != null) {
+ mOnlineContactsCountMap.close();
+ }
+ }
+
+ mAdapter.notifyDataSetChanged();
+ if (conn != null) {
+ try {
+ mProviderId = conn.getProviderId();
+ mAccountId = conn.getAccountId();
+ startQueryOngoingConversations();
+ startQueryContactLists();
+ startQuerySubscriptions();
+ } catch (RemoteException e) {
+ // Service died!
+ }
+ }
+ }
+
+ public void setHideOfflineContacts(boolean hide) {
+ if (mHideOfflineContacts != hide) {
+ mHideOfflineContacts = hide;
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void startAutoRequery() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("startAutoRequery()");
+ }
+ mAutoRequery = true;
+ if (mRequeryPending) {
+ mRequeryPending = false;
+ startQueryOngoingConversations();
+ }
+ }
+
+ private void startQueryContactLists() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("startQueryContactLists()");
+ }
+
+ Uri uri = Im.ContactList.CONTENT_URI;
+ uri = ContentUris.withAppendedId(uri, mProviderId);
+ uri = ContentUris.withAppendedId(uri, mAccountId);
+
+ mQueryHandler.startQuery(TOKEN_CONTACT_LISTS, null, uri, CONTACT_LIST_PROJECTION,
+ null, null, Im.ContactList.DEFAULT_SORT_ORDER);
+ }
+
+ void startQueryOngoingConversations() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("startQueryOngoingConversations()");
+ }
+
+ Uri uri = Im.Contacts.CONTENT_URI_CHAT_CONTACTS_BY;
+ uri = ContentUris.withAppendedId(uri, mProviderId);
+ uri = ContentUris.withAppendedId(uri, mAccountId);
+
+ mQueryHandler.startQuery(TOKEN_ONGOING_CONVERSATION, null, uri,
+ ContactView.CONTACT_PROJECTION, null, null, Im.Contacts.DEFAULT_SORT_ORDER);
+ }
+
+ void startQuerySubscriptions() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("startQuerySubscriptions()");
+ }
+
+ Uri uri = Im.Contacts.CONTENT_URI_CONTACTS_BY;
+ uri = ContentUris.withAppendedId(uri, mProviderId);
+ uri = ContentUris.withAppendedId(uri, mAccountId);
+
+ mQueryHandler.startQuery(TOKEN_SUBSCRITPTION, null, uri,
+ ContactView.CONTACT_PROJECTION,
+ String.format("%s=%d AND %s=%d",
+ Im.Contacts.SUBSCRIPTION_STATUS, Im.Contacts.SUBSCRIPTION_STATUS_SUBSCRIBE_PENDING,
+ Im.Contacts.SUBSCRIPTION_TYPE, Im.Contacts.SUBSCRIPTION_TYPE_FROM),
+ null,Im.Contacts.DEFAULT_SORT_ORDER);
+ }
+
+ void startQueryContacts(long listId) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("startQueryContacts - listId=" + listId);
+ }
+
+ String selection = mHideOfflineContacts ? ONLINE_CONTACT_SELECTION : CONTACTS_SELECTION;
+ String[] args = { Long.toString(listId) };
+ int token = (int)listId;
+ mQueryHandler.startQuery(token, null, Im.Contacts.CONTENT_URI,
+ ContactView.CONTACT_PROJECTION, selection, args, Im.Contacts.DEFAULT_SORT_ORDER);
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ if (isPosForOngoingConversation(groupPosition)) {
+ // No cursor exists for the "Empty" TextView item
+ if (getOngoingConversationCount() == 0) return null;
+ return moveTo(getOngoingConversations(), childPosition);
+ } else if (isPosForSubscription(groupPosition)) {
+ return moveTo(getSubscriptions(), childPosition);
+ } else {
+ return mAdapter.getChild(getChildAdapterPosition(groupPosition), childPosition);
+ }
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ if (isPosForOngoingConversation(groupPosition)) {
+ // No cursor id exists for the "Empty" TextView item
+ if (getOngoingConversationCount() == 0) return 0;
+ return getId(getOngoingConversations(), childPosition);
+ } else if (isPosForSubscription(groupPosition)) {
+ return getId(getSubscriptions(), childPosition);
+ } else {
+ return mAdapter.getChildId(getChildAdapterPosition(groupPosition), childPosition);
+ }
+ }
+
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ boolean isOngoingConversation = isPosForOngoingConversation(groupPosition);
+ boolean displayEmpty = isOngoingConversation && (getOngoingConversationCount() == 0);
+ if (isOngoingConversation || isPosForSubscription(groupPosition)) {
+ View view = null;
+ if (convertView != null) {
+ // use the convert view if it matches the type required by displayEmpty
+ if (displayEmpty && (convertView instanceof TextView)) {
+ view = convertView;
+ ((TextView) view).setText(mActivity.getText(R.string.empty_conversation_group));
+ } else if (!displayEmpty && (convertView instanceof ContactView)) {
+ view = convertView;
+ }
+ }
+ if (view == null) {
+ if (displayEmpty) {
+ view = newEmptyView(parent);
+ } else {
+ view = newChildView(parent);
+ }
+ }
+ if (!displayEmpty) {
+ Cursor cursor = isPosForOngoingConversation(groupPosition)
+ ? getOngoingConversations() : getSubscriptions();
+ cursor.moveToPosition(childPosition);
+ ((ContactView) view).bind(cursor, null, isScrolling());
+ }
+ return view;
+ } else {
+ return mAdapter.getChildView(getChildAdapterPosition(groupPosition), childPosition,
+ isLastChild, convertView, parent);
+ }
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ if (!mDataValid) {
+ return 0;
+ }
+ if (isPosForOngoingConversation(groupPosition)) {
+ // if there are no ongoing conversations, we want to display "empty" textview
+ int count = getOngoingConversationCount();
+ if (count == 0) {
+ count = 1;
+ }
+ return count;
+ } else if (isPosForSubscription(groupPosition)) {
+ return getSubscriptionCount();
+ } else {
+ // XXX getChildrenCount() may be called with an invalid groupPosition that is larger
+ // than the total number of all groups.
+ int position = getChildAdapterPosition(groupPosition);
+ if (position >= mAdapter.getGroupCount()) {
+ Log.w(ImApp.LOG_TAG, "getChildrenCount out of range");
+ return 0;
+ }
+ return mAdapter.getChildrenCount(position);
+ }
+ }
+
+ public Object getGroup(int groupPosition) {
+ if (isPosForOngoingConversation(groupPosition)
+ || isPosForSubscription(groupPosition)) {
+ return null;
+ } else {
+ return mAdapter.getGroup(getChildAdapterPosition(groupPosition));
+ }
+ }
+
+ public int getGroupCount() {
+ if (!mDataValid) {
+ return 0;
+ }
+ int count = mAdapter.getGroupCount();
+
+ // ongoing conversations
+ count++;
+
+ if (getSubscriptionCount() > 0) {
+ count++;
+ }
+
+ return count;
+ }
+
+ public long getGroupId(int groupPosition) {
+ if (isPosForOngoingConversation(groupPosition) || isPosForSubscription(groupPosition)) {
+ return 0;
+ } else {
+ return mAdapter.getGroupId(getChildAdapterPosition(groupPosition));
+ }
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ if (isPosForOngoingConversation(groupPosition) || isPosForSubscription(groupPosition)) {
+ View v;
+ if (convertView != null) {
+ v = convertView;
+ } else {
+ v = newGroupView(parent);
+ }
+
+ TextView text1 = (TextView)v.findViewById(R.id.text1);
+ TextView text2 = (TextView)v.findViewById(R.id.text2);
+
+ Resources r = v.getResources();
+ ImApp app = ImApp.getApplication(mActivity);
+ BrandingResources brandingRes = app.getBrandingResource(mProviderId);
+ String text = isPosForOngoingConversation(groupPosition) ?
+ brandingRes.getString(
+ BrandingResourceIDs.STRING_ONGOING_CONVERSATION,
+ getOngoingConversationCount()) :
+ r.getString(R.string.subscriptions);
+ text1.setText(text);
+ text2.setVisibility(View.GONE);
+ return v;
+ } else {
+ return mAdapter.getGroupView(getChildAdapterPosition(groupPosition), isExpanded,
+ convertView, parent);
+ }
+ }
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ if (isPosForOngoingConversation(groupPosition)) {
+ // "Empty" TextView is not selectable
+ if (getOngoingConversationCount()==0) return false;
+ return true;
+ }
+ if (isPosForSubscription(groupPosition)) return true;
+ return mAdapter.isChildSelectable(getChildAdapterPosition(groupPosition), childPosition);
+ }
+
+ public boolean stableIds() {
+ return true;
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mAdapter.registerDataSetObserver(observer);
+ super.registerDataSetObserver(observer);
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mAdapter.unregisterDataSetObserver(observer);
+ super.unregisterDataSetObserver(observer);
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public void onGroupCollapsed(int groupPosition) {
+ super.onGroupCollapsed(groupPosition);
+ mExpandedGroups.remove(Integer.valueOf(groupPosition));
+ int pos = getChildAdapterPosition(groupPosition);
+ if (pos >= 0) {
+ mAdapter.onGroupCollapsed(pos);
+ }
+ }
+
+ @Override
+ public void onGroupExpanded(int groupPosition) {
+ super.onGroupExpanded(groupPosition);
+ mExpandedGroups.add(groupPosition);
+ int pos = getChildAdapterPosition(groupPosition);
+ if (pos >= 0) {
+ mAdapter.onGroupExpanded(pos);
+ }
+ }
+
+ public int[] getExpandedGroups() {
+ ArrayList<Integer> expandedGroups = mExpandedGroups;
+ int size = expandedGroups.size();
+ int[] res = new int[size];
+ for (int i = 0; i < size; i++) {
+ res[i] = expandedGroups.get(i);
+ }
+ return res;
+ }
+
+ View newChildView(ViewGroup parent) {
+ return mInflate.inflate(R.layout.contact_view, parent, false);
+ }
+
+ View newEmptyView(ViewGroup parent) {
+ return mInflate.inflate(R.layout.empty_conversation_group_view, parent, false);
+ }
+
+ View newGroupView(ViewGroup parent) {
+ return mInflate.inflate(R.layout.group_view, parent, false);
+ }
+
+ private synchronized Cursor getOngoingConversations() {
+ if (mOngoingConversations == null) {
+ startQueryOngoingConversations();
+ }
+ return mOngoingConversations;
+ }
+
+ synchronized void setOngoingConversations(Cursor c) {
+ if (mOngoingConversations != null) {
+ mOngoingConversations.unregisterContentObserver(mContentObserver);
+ mOngoingConversations.unregisterDataSetObserver(mDataSetObserver);
+ mOngoingConversations.close();
+ }
+ c.registerContentObserver(mContentObserver);
+ c.registerDataSetObserver(mDataSetObserver);
+ mOngoingConversations = c;
+ }
+
+ private int getOngoingConversationCount() {
+ Cursor c = getOngoingConversations();
+ return c == null ? 0 : c.getCount();
+ }
+
+ private synchronized Cursor getSubscriptions() {
+ if (mSubscriptions == null) {
+ startQuerySubscriptions();
+ }
+ return mSubscriptions;
+ }
+
+ synchronized void setSubscriptions(Cursor c) {
+ if (mSubscriptions != null) {
+ mSubscriptions.close();
+ }
+ // we don't need to register observers on mSubscriptions because
+ // we already have observers on mOngoingConversations and they
+ // will be notified if there is any changes of subscription
+ // since the two cursors come from the same table.
+ mSubscriptions = c;
+ }
+
+ private int getSubscriptionCount() {
+ Cursor c = getSubscriptions();
+ return c == null ? 0 : c.getCount();
+ }
+
+ public boolean isPosForOngoingConversation(int groupPosition) {
+ return groupPosition == 0;
+ }
+
+ public boolean isPosForSubscription(int groupPosition) {
+ return groupPosition == 1 && getSubscriptionCount() > 0;
+ }
+
+ private int getChildAdapterPosition(int groupPosition) {
+ if (getSubscriptionCount() > 0) {
+ return groupPosition - 2;
+ } else {
+ return groupPosition - 1;
+ }
+ }
+
+ private Cursor moveTo(Cursor cursor, int position) {
+ if (cursor.moveToPosition(position)) {
+ return cursor;
+ }
+ return null;
+ }
+
+ private long getId(Cursor cursor, int position) {
+ if (cursor.moveToPosition(position)) {
+ return cursor.getLong(ContactView.COLUMN_CONTACT_ID);
+ }
+ return 0;
+ }
+
+ class ListTreeAdapter extends CursorTreeAdapter {
+
+ public ListTreeAdapter(Cursor cursor) {
+ super(cursor, mActivity);
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor,
+ boolean isLastChild) {
+ // binding when child is text view for an empty group
+ if (view instanceof TextView) {
+ ((TextView) view).setText(mActivity.getText(R.string.empty_contact_group));
+ } else {
+ ((ContactView) view).bind(cursor, null, isScrolling());
+ }
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor,
+ boolean isExpanded) {
+ TextView text1 = (TextView)view.findViewById(R.id.text1);
+ TextView text2 = (TextView)view.findViewById(R.id.text2);
+ Resources r = view.getResources();
+
+ text1.setText(cursor.getString(COLUMN_CONTACT_LIST_NAME));
+ text2.setVisibility(View.VISIBLE);
+ text2.setText(r.getString(R.string.online_count, getOnlineChildCount(cursor)));
+ }
+
+ View newEmptyView(ViewGroup parent) {
+ return mInflate.inflate(R.layout.empty_contact_group_view, parent, false);
+ }
+
+ // if the group is empty, provide a text view. The infrastructure provides a "convertView"
+ // as a possible suggestion to reuse an existing view's data. It may be null, it may be a
+ // TextView, or it may be a ContactView, so we need to test the possible cases.
+ @Override
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ // Provide a TextView if the group is empty
+ if (super.getChildrenCount(groupPosition)==0) {
+ if (convertView != null) {
+ if (convertView instanceof TextView) {
+ ((TextView) convertView).setText(
+ mActivity.getText(R.string.empty_contact_group));
+ return convertView;
+ }
+ }
+ return newEmptyView(parent);
+ }
+ if ( !(convertView instanceof ContactView) ) {
+ convertView = null;
+ }
+ return super.getChildView(groupPosition, childPosition, isLastChild, convertView,
+ parent);
+ }
+
+ @Override
+ protected Cursor getChildrenCursor(Cursor groupCursor) {
+ long listId = groupCursor.getLong(COLUMN_CONTACT_LIST_ID);
+ startQueryContacts(listId);
+ return null;
+ }
+
+ // return a TextView for empty groups
+ @Override
+ protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
+ ViewGroup parent) {
+ if (cursor.getCount() == 0) {
+ return newEmptyView(parent);
+ } else {
+ return ContactListTreeAdapter.this.newChildView(parent);
+ }
+ }
+
+ @Override
+ protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
+ ViewGroup parent) {
+ return ContactListTreeAdapter.this.newGroupView(parent);
+ }
+
+ private int getOnlineChildCount(Cursor groupCursor) {
+ long listId = groupCursor.getLong(COLUMN_CONTACT_LIST_ID);
+ if (mOnlineContactsCountMap == null) {
+ String where = Im.Contacts.ACCOUNT + "=" + mAccountId;
+ ContentResolver cr = mActivity.getContentResolver();
+
+ Cursor c = cr.query(Im.Contacts.CONTENT_URI_ONLINE_COUNT,
+ CONTACT_COUNT_PROJECTION, where, null, null);
+ mOnlineContactsCountMap = new ContentQueryMap(c,
+ Im.Contacts.CONTACTLIST, true, mHandler);
+ mOnlineContactsCountMap.addObserver(new Observer(){
+ public void update(Observable observable, Object data) {
+ notifyDataSetChanged();
+ }});
+ }
+ ContentValues value = mOnlineContactsCountMap.getValues(String.valueOf(listId));
+ return value == null ? 0 : value.getAsInteger(Im.Contacts._COUNT);
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ int children = super.getChildrenCount(groupPosition);
+ if (children == 0) {
+ // Count the empty group text item as a child
+ return 1;
+ }
+ return children;
+ }
+
+ // Don't allow the empty group text item to be selected
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return (super.getChildrenCount(groupPosition) > 0);
+ }
+ }
+
+ private class MyContentObserver extends ContentObserver {
+
+ public MyContentObserver() {
+ super(mHandler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("MyContentObserver.onChange() autoRequery=" + mAutoRequery);
+ }
+ // Don't requery when fling. We will schedule a requery when the fling is complete.
+ if (isScrolling()) {
+ return;
+ }
+ if (mAutoRequery) {
+ startQueryOngoingConversations();
+ } else {
+ mRequeryPending = true;
+ }
+ }
+ }
+
+ private class MyDataSetObserver extends DataSetObserver {
+ public MyDataSetObserver() {
+ }
+
+ @Override
+ public void onChanged() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("MyDataSetObserver.onChanged()");
+ }
+
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("MyDataSetObserver.onInvalidated()");
+ }
+
+ mDataValid = false;
+ notifyDataSetInvalidated();
+ }
+ }
+
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ // no op
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ int oldState = mScrollState;
+
+ mScrollState = scrollState;
+ // If we just finished a fling then some items may not have an icon
+ // So force a full redraw now that the fling is complete
+ if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
+ notifyDataSetChanged();
+ }
+ }
+
+ public boolean isScrolling() {
+ return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
+ }
+}
diff --git a/src/com/android/im/app/ContactListView.java b/src/com/android/im/app/ContactListView.java
new file mode 100644
index 0000000..91cd738
--- /dev/null
+++ b/src/com/android/im/app/ContactListView.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import com.android.im.IChatSession;
+import com.android.im.IChatSessionManager;
+import com.android.im.IContactListListener;
+import com.android.im.IContactListManager;
+import com.android.im.IImConnection;
+import com.android.im.ISubscriptionListener;
+import com.android.im.R;
+import com.android.im.app.adapter.ContactListListenerAdapter;
+import com.android.im.engine.Contact;
+import com.android.im.engine.ContactListManager;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.service.ImServiceConstants;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ExpandableListView;
+import android.widget.LinearLayout;
+import android.widget.ExpandableListView.OnChildClickListener;
+
+public class ContactListView extends LinearLayout {
+
+ Activity mScreen;
+ IImConnection mConn;
+ SimpleAlertHandler mHandler;
+ private final IContactListListener mContactListListener;
+
+ UserPresenceView mPresenceView;
+ ExpandableListView mContactsList;
+ private ContactListTreeAdapter mAdapter;
+ private boolean mHideOfflineContacts;
+ private SavedState mSavedState;
+
+ public ContactListView(Context screen, AttributeSet attrs) {
+ super(screen, attrs);
+ mScreen = (Activity)screen;
+ mHandler = new SimpleAlertHandler(mScreen);
+ mContactListListener = new MyContactListListener(mHandler);
+ }
+
+ private class MyContactListListener extends ContactListListenerAdapter {
+ public MyContactListListener(SimpleAlertHandler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onAllContactListsLoaded() {
+ if (mAdapter != null) {
+ mAdapter.startAutoRequery();
+ }
+ }
+ }
+
+ private final ISubscriptionListener.Stub mSubscriptionListener = new ISubscriptionListener.Stub() {
+
+ public void onSubScriptionRequest(Contact from) {
+ querySubscription();
+ }
+
+ public void onSubscriptionApproved(String contact) {
+ querySubscription();
+ }
+
+ public void onSubscriptionDeclined(String contact) {
+ querySubscription();
+ }
+
+ private void querySubscription() {
+ if (mAdapter != null) {
+ mAdapter.startQuerySubscriptions();
+ }
+ }
+ };
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mPresenceView = (UserPresenceView)findViewById(R.id.userPresence);
+ mContactsList = (ExpandableListView) findViewById(R.id.contactsList);
+ mContactsList.setOnChildClickListener(mOnChildClickListener);
+ }
+
+ public ExpandableListView getListView() {
+ return mContactsList;
+ }
+
+ public void setConnection(IImConnection conn) {
+ if (mConn != conn) {
+ if (mConn != null) {
+ unregisterListeners();
+ }
+ mConn = conn;
+
+ if (conn != null) {
+ registerListeners();
+ mPresenceView.setConnection(conn);
+
+ if (mAdapter == null) {
+ mAdapter = new ContactListTreeAdapter(conn, mScreen);
+ mAdapter.setHideOfflineContacts(mHideOfflineContacts);
+ mContactsList.setAdapter(mAdapter);
+ mContactsList.setOnScrollListener(mAdapter);
+ if (mSavedState != null) {
+ int[] expandedGroups = mSavedState.mExpandedGroups;
+ if(expandedGroups != null) {
+ for (int group : expandedGroups) {
+ mContactsList.expandGroup(group);
+ }
+ }
+ }
+ } else {
+ mAdapter.changeConnection(conn);
+ }
+ try {
+ IContactListManager listMgr = conn.getContactListManager();
+ if (listMgr.getState() == ContactListManager.LISTS_LOADED) {
+ mAdapter.startAutoRequery();
+ }
+ } catch (RemoteException e) {
+ Log.e(ImApp.LOG_TAG, "Service died!");
+ }
+ }
+ } else {
+ mContactsList.invalidateViews();
+ }
+ }
+
+ public void setHideOfflineContacts(boolean hide) {
+ if (mAdapter != null) {
+ mAdapter.setHideOfflineContacts(hide);
+ } else {
+ mHideOfflineContacts = hide;
+ }
+ }
+
+ public void startChat() {
+ startChat(getSelectedContact());
+ }
+
+ public void startChatAtPosition(long packedPosition) {
+ startChat(getContactAtPosition(packedPosition));
+ }
+
+ void startChat(Cursor c) {
+ if (c != null) {
+ long id = c.getLong(c.getColumnIndexOrThrow(Im.Contacts._ID));
+ String username = c.getString(c.getColumnIndexOrThrow(Im.Contacts.USERNAME));
+ try {
+ IChatSessionManager manager = mConn.getChatSessionManager();
+ IChatSession session = manager.getChatSession(username);
+ if(session == null) {
+ manager.createChatSession(username);
+ }
+
+ Uri data = ContentUris.withAppendedId(Im.Chats.CONTENT_URI, id);
+ Intent i = new Intent(Intent.ACTION_VIEW, data);
+ mScreen.startActivity(i);
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ clearFocusIfEmpty(c);
+ }
+ }
+
+ private void clearFocusIfEmpty(Cursor c) {
+ // clear focus if there's only one item so that it would focus on the
+ // "empty" item after the contact removed.
+ if (c.getCount() == 1) {
+ clearFocus();
+ }
+ }
+
+ public void endChat() {
+ endChat(getSelectedContact());
+ }
+
+ public void endChatAtPosition(long packedPosition) {
+ endChat(getContactAtPosition(packedPosition));
+ }
+
+ void endChat(Cursor c) {
+ if(c != null) {
+ String username = c.getString(c.getColumnIndexOrThrow(Im.Contacts.USERNAME));
+ try {
+ IChatSessionManager manager = mConn.getChatSessionManager();
+ IChatSession session = manager.getChatSession(username);
+ if(session != null) {
+ session.leave();
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ clearFocusIfEmpty(c);
+ }
+ }
+
+ public void viewContactPresence() {
+ viewContactPresence(getSelectedContact());
+ }
+
+ public void viewContactPresenceAtPostion(long packedPosition) {
+ viewContactPresence(getContactAtPosition(packedPosition));
+ }
+
+ public void viewContactPresence(Cursor c) {
+ if (c != null) {
+ long id = c.getLong(c.getColumnIndexOrThrow(Im.Contacts._ID));
+ Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, id);
+ Intent i = new Intent(Intent.ACTION_VIEW, data);
+ mScreen.startActivity(i);
+ }
+ }
+
+ public boolean isContactAtPosition(long packedPosition) {
+ int type = ExpandableListView.getPackedPositionType(packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition);
+ return (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD)
+ && !mAdapter.isPosForSubscription(groupPosition);
+ }
+
+ public boolean isContactSelected() {
+ long pos = mContactsList.getSelectedPosition();
+ return isContactAtPosition(pos);
+ }
+
+ public boolean isConversationAtPosition(long packedPosition) {
+ int type = ExpandableListView.getPackedPositionType(packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition);
+ return (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD)
+ && mAdapter.isPosForOngoingConversation(groupPosition);
+ }
+
+ public boolean isConversationSelected () {
+ long pos = mContactsList.getSelectedPosition();
+ return isConversationAtPosition(pos);
+ }
+
+ public boolean isContactsLoaded() {
+ try {
+ IContactListManager manager = mConn.getContactListManager();
+ return (manager.getState() == ContactListManager.LISTS_LOADED);
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ return false;
+ }
+ }
+
+ public void removeContact() {
+ removeContact(getSelectedContact());
+ }
+
+ public void removeContactAtPosition(long packedPosition) {
+ removeContact(getContactAtPosition(packedPosition));
+ }
+
+ void removeContact(Cursor c) {
+ if (c == null) {
+ mHandler.showAlert(R.string.error, R.string.select_contact);
+ } else {
+ String nickname = c.getString(c.getColumnIndexOrThrow(Im.Contacts.NICKNAME));
+ final String address = c.getString(c.getColumnIndexOrThrow(Im.Contacts.USERNAME));
+ DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
+ public void onClick(DialogInterface dialog, int whichButton) {
+ try {
+ IContactListManager manager = mConn.getContactListManager();
+ int res = manager.removeContact(address);
+ if (res != ImErrorInfo.NO_ERROR) {
+ mHandler.showAlert(R.string.error,
+ ErrorResUtils.getErrorRes(getResources(), res, address));
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ };
+ Resources r = getResources();
+
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.confirm)
+ .setMessage(r.getString(R.string.confirm_delete_contact, nickname))
+ .setPositiveButton(R.string.yes, confirmListener) // default button
+ .setNegativeButton(R.string.no, null)
+ .setCancelable(false)
+ .show();
+
+ clearFocusIfEmpty(c);
+ }
+ }
+
+ public void blockContact() {
+ blockContact(getSelectedContact());
+ }
+
+ public void blockContactAtPosition(long packedPosition) {
+ blockContact(getContactAtPosition(packedPosition));
+ }
+
+ void blockContact(Cursor c) {
+ if (c == null) {
+ mHandler.showAlert(R.string.error, R.string.select_contact);
+ } else {
+ String nickname = c.getString(c.getColumnIndexOrThrow(Im.Contacts.NICKNAME));
+ final String address = c.getString(c.getColumnIndexOrThrow(Im.Contacts.USERNAME));
+ DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
+ public void onClick(DialogInterface dialog, int whichButton) {
+ try {
+ IContactListManager manager = mConn.getContactListManager();
+ int res = manager.blockContact(address);
+ if (res != ImErrorInfo.NO_ERROR) {
+ mHandler.showAlert(R.string.error,
+ ErrorResUtils.getErrorRes(getResources(), res, address));
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ };
+
+ Resources r = getResources();
+
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.confirm)
+ .setMessage(r.getString(R.string.confirm_block_contact, nickname))
+ .setPositiveButton(R.string.yes, confirmListener) // default button
+ .setNegativeButton(R.string.no, null)
+ .setCancelable(false)
+ .show();
+ clearFocusIfEmpty(c);
+ }
+ }
+
+ public Cursor getContactAtPosition(long packedPosition) {
+ int type = ExpandableListView.getPackedPositionType(packedPosition);
+ if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+ int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition);
+ int childPosition = ExpandableListView.getPackedPositionChild(packedPosition);
+ return (Cursor) mAdapter.getChild(groupPosition, childPosition);
+ }
+ return null;
+ }
+
+ public Cursor getSelectedContact() {
+ long pos = mContactsList.getSelectedPosition();
+ if (ExpandableListView.getPackedPositionType(pos)
+ == ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+ return (Cursor)mContactsList.getSelectedItem();
+ }
+ return null;
+ }
+
+ public String getSelectedContactList() {
+ long pos = mContactsList.getSelectedPosition();
+ int groupPos = ExpandableListView.getPackedPositionGroup(pos);
+ if (groupPos == -1) {
+ return null;
+ }
+
+ Cursor cursor = (Cursor)mAdapter.getGroup(groupPos);
+ if (cursor == null) {
+ return null;
+ }
+ return cursor.getString(cursor.getColumnIndexOrThrow(Im.ContactList.NAME));
+ }
+
+ private void registerListeners() {
+ try{
+ IContactListManager listManager = mConn.getContactListManager();
+ listManager.registerContactListListener(mContactListListener);
+ listManager.registerSubscriptionListener(mSubscriptionListener);
+ }catch(RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+
+ private void unregisterListeners() {
+ try{
+ IContactListManager listManager = mConn.getContactListManager();
+ listManager.unregisterContactListListener(mContactListListener);
+ listManager.unregisterSubscriptionListener(mSubscriptionListener);
+ }catch(RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+
+ private final OnChildClickListener mOnChildClickListener = new OnChildClickListener() {
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ Cursor cursor = (Cursor)parent.getExpandableListAdapter().getChild(
+ groupPosition, childPosition);
+ int subscriptionType = cursor.getInt(ContactView.COLUMN_SUBSCRIPTION_TYPE);
+ int subscriptionStatus = cursor.getInt(ContactView.COLUMN_SUBSCRIPTION_STATUS);
+ if ((subscriptionType == Im.Contacts.SUBSCRIPTION_TYPE_FROM)
+ && (subscriptionStatus == Im.Contacts.SUBSCRIPTION_STATUS_SUBSCRIBE_PENDING)){
+ long providerId = cursor.getLong(ContactView.COLUMN_CONTACT_PROVIDER);
+ String username = cursor.getString(ContactView.COLUMN_CONTACT_USERNAME);
+ Intent intent = new Intent(ImServiceConstants.ACTION_MANAGE_SUBSCRIPTION,
+ ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, id));
+ intent.putExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, providerId);
+ intent.putExtra(ImServiceConstants.EXTRA_INTENT_FROM_ADDRESS, username);
+ mScreen.startActivity(intent);
+ } else {
+ startChat(cursor);
+ }
+ return true;
+ }
+ };
+
+ static class SavedState extends BaseSavedState {
+ int[] mExpandedGroups;
+
+ SavedState(Parcelable superState, int[] expandedGroups) {
+ super(superState);
+ mExpandedGroups = expandedGroups;
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ mExpandedGroups = in.createIntArray();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeIntArray(mExpandedGroups);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ int[] expandedGroups = mAdapter == null ? null
+ : mAdapter.getExpandedGroups();
+ return new SavedState(superState, expandedGroups);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ mSavedState = ss;
+ }
+}
diff --git a/src/com/android/im/app/ContactPresenceActivity.java b/src/com/android/im/app/ContactPresenceActivity.java
new file mode 100644
index 0000000..36e09a9
--- /dev/null
+++ b/src/com/android/im/app/ContactPresenceActivity.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Im;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ImageSpan;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
+
+public class ContactPresenceActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setTheme(android.R.style.Theme_Dialog);
+ setContentView(R.layout.contact_presence_activity);
+
+ ImageView imgAvatar = (ImageView) findViewById(R.id.imgAvatar);
+ TextView labelName = (TextView) findViewById(R.id.labelName);
+ TextView txtName = (TextView) findViewById(R.id.txtName);
+ TextView txtStatus = (TextView) findViewById(R.id.txtStatus);
+ TextView txtClientType = (TextView) findViewById(R.id.txtClientType);
+ TextView txtCustomStatus = (TextView) findViewById(R.id.txtStatusText);
+
+ Intent i = getIntent();
+ Uri uri = i.getData();
+ if(uri == null) {
+ warning("No data to show");
+ finish();
+ return;
+ }
+
+ ContentResolver cr = getContentResolver();
+ Cursor c = cr.query(uri, null, null, null, null);
+ if(c == null) {
+ warning("Database error when query " + uri);
+ finish();
+ return;
+ }
+
+ if(c.moveToFirst()) {
+ long providerId = c.getLong(c.getColumnIndexOrThrow(Im.Contacts.PROVIDER));
+ String username = c.getString(c.getColumnIndexOrThrow(Im.Contacts.USERNAME));
+ String nickname = c.getString(c.getColumnIndexOrThrow(Im.Contacts.NICKNAME));
+ int status = c.getInt(c.getColumnIndexOrThrow(Im.Contacts.PRESENCE_STATUS));
+ int clientType = c.getInt(c.getColumnIndexOrThrow(Im.Contacts.CLIENT_TYPE));
+ String customStatus = c.getString(c.getColumnIndexOrThrow(Im.Contacts.PRESENCE_CUSTOM_STATUS));
+
+ ImApp app = ImApp.getApplication(this);
+ BrandingResources brandingRes = app.getBrandingResource(providerId);
+ setTitle(brandingRes.getString(BrandingResourceIDs.STRING_CONTACT_INFO_TITLE));
+
+ Drawable avatar = DatabaseUtils.getAvatarFromCursor(c,
+ c.getColumnIndexOrThrow(Im.Contacts.AVATAR_DATA));
+ if (avatar != null) {
+ imgAvatar.setImageDrawable(avatar);
+ } else {
+ imgAvatar.setImageResource(R.drawable.avatar_unknown);
+ }
+
+ labelName.setText(brandingRes.getString(
+ BrandingResourceIDs.STRING_LABEL_USERNAME));
+ txtName.setText(ImpsAddressUtils.getDisplayableAddress(username));
+
+ String statusString = brandingRes.getString(
+ PresenceUtils.getStatusStringRes(status));
+ SpannableString s = new SpannableString("+ " + statusString);
+ ImageSpan imageSpan = new ImageSpan(
+ brandingRes.getDrawable(PresenceUtils.getStatusIconId(status)));
+ s.setSpan(imageSpan, 0, 1, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ txtStatus.setText(s);
+
+ txtClientType.setText(getClientTypeString(clientType));
+
+ if (!TextUtils.isEmpty(customStatus)) {
+ txtCustomStatus.setVisibility(View.VISIBLE);
+ txtCustomStatus.setText("\"" + customStatus + "\"");
+ } else {
+ txtCustomStatus.setVisibility(View.GONE);
+ }
+ }
+ c.close();
+ }
+
+ private String getClientTypeString(int clientType) {
+ Resources res = getResources();
+ switch (clientType) {
+ case Im.Contacts.CLIENT_TYPE_MOBILE:
+ return res.getString(R.string.client_type_mobile);
+
+ default:
+ return res.getString(R.string.client_type_computer);
+ }
+ }
+
+ private static void warning(String msg) {
+ Log.w(ImApp.LOG_TAG, "<ContactPresenceActivity> " + msg);
+ }
+}
diff --git a/src/com/android/im/app/ContactView.java b/src/com/android/im/app/ContactView.java
new file mode 100644
index 0000000..a4a164a
--- /dev/null
+++ b/src/com/android/im/app/ContactView.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Im;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.UnderlineSpan;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.im.R;
+import com.android.im.plugin.BrandingResourceIDs;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+
+public class ContactView extends LinearLayout {
+ static final String[] CONTACT_PROJECTION = {
+ Im.Contacts._ID,
+ Im.Contacts.PROVIDER,
+ Im.Contacts.ACCOUNT,
+ Im.Contacts.USERNAME,
+ Im.Contacts.NICKNAME,
+ Im.Contacts.TYPE,
+ Im.Contacts.SUBSCRIPTION_TYPE,
+ Im.Contacts.SUBSCRIPTION_STATUS,
+ Im.Presence.PRESENCE_STATUS,
+ Im.Presence.PRESENCE_CUSTOM_STATUS,
+ Im.Chats.LAST_MESSAGE_DATE,
+ Im.Chats.LAST_UNREAD_MESSAGE,
+ };
+
+ static final int COLUMN_CONTACT_ID = 0;
+ static final int COLUMN_CONTACT_PROVIDER = 1;
+ static final int COLUMN_CONTACT_ACCOUNT = 2;
+ static final int COLUMN_CONTACT_USERNAME = 3;
+ static final int COLUMN_CONTACT_NICKNAME = 4;
+ static final int COLUMN_CONTACT_TYPE = 5;
+ static final int COLUMN_SUBSCRIPTION_TYPE = 6;
+ static final int COLUMN_SUBSCRIPTION_STATUS = 7;
+ static final int COLUMN_CONTACT_PRESENCE_STATUS = 8;
+ static final int COLUMN_CONTACT_CUSTOM_STATUS = 9;
+ static final int COLUMN_LAST_MESSAGE_DATE = 10;
+ static final int COLUMN_LAST_MESSAGE = 11;
+
+ private ImageView mPresence;
+ private TextView mLine1;
+ private TextView mLine2;
+ private TextView mTimeStamp;
+
+ private Handler mHandler;
+ private boolean mLayoutDirty;
+
+ public ContactView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mLayoutDirty = true;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mPresence = (ImageView) findViewById(R.id.presence);
+ mLine1 = (TextView) findViewById(R.id.line1);
+ mLine2 = (TextView) findViewById(R.id.line2);
+ mTimeStamp = (TextView)findViewById(R.id.timestamp);
+
+ mHandler = new Handler();
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ super.setSelected(selected);
+ if(selected) {
+ // While layout, the width of children is unknown, we have to start
+ // animation when layout is done.
+ if (mLayoutDirty) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ startAnimationNow();
+ }
+ });
+ } else {
+ startAnimationNow();
+ }
+ } else {
+ mLine2.clearAnimation();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ mLayoutDirty = false;
+ super.onLayout(changed, l, t, r, b);
+ }
+
+ @Override
+ public void requestLayout() {
+ super.requestLayout();
+ mLayoutDirty = true;
+ }
+
+ /*package*/ void startAnimationNow() {
+ View parent = (View)mLine2.getParent();
+ int width = mLine2.getWidth();
+
+ int parentWidth = parent.getWidth() - parent.getPaddingLeft()
+ - parent.getPaddingRight();
+ if(width > parentWidth) {
+ int fromXDelta = parentWidth;
+ int toXDelta = - width;
+ int duration = (fromXDelta - toXDelta) * 32;
+ Animation animation = new TranslateAnimation(fromXDelta, toXDelta, 0, 0);
+ animation.setDuration(duration);
+ animation.setRepeatMode(Animation.RESTART);
+ animation.setRepeatCount(Animation.INFINITE);
+ mLine2.startAnimation(animation);
+ }
+ }
+
+ public void bind(Cursor cursor, String underLineText, boolean scrolling) {
+ bind(cursor, underLineText, true, scrolling);
+ }
+
+ public void bind(Cursor cursor, String underLineText, boolean showChatMsg, boolean scrolling) {
+ Resources r = getResources();
+ long providerId = cursor.getLong(COLUMN_CONTACT_PROVIDER);
+ String username = cursor.getString(COLUMN_CONTACT_USERNAME);
+ String nickname = cursor.getString(COLUMN_CONTACT_NICKNAME);
+ int type = cursor.getInt(COLUMN_CONTACT_TYPE);
+ String statusText = cursor.getString(COLUMN_CONTACT_CUSTOM_STATUS);
+ String lastMsg = cursor.getString(COLUMN_LAST_MESSAGE);
+
+ boolean hasChat = !cursor.isNull(COLUMN_LAST_MESSAGE_DATE);
+
+ ImApp app = ImApp.getApplication((Activity)mContext);
+ BrandingResources brandingRes = app.getBrandingResource(providerId);
+
+ int presence = cursor.getInt(COLUMN_CONTACT_PRESENCE_STATUS);
+ // status icon
+
+ if (Im.Contacts.TYPE_GROUP == type) {
+ int iconId = lastMsg == null ? R.drawable.group_chat
+ : R.drawable.group_chat_new;
+ mPresence.setImageResource(iconId);
+ } else if (hasChat) {
+ int iconId = lastMsg == null ? BrandingResourceIDs.DRAWABLE_READ_CHAT
+ : BrandingResourceIDs.DRAWABLE_UNREAD_CHAT;
+ mPresence.setImageDrawable(brandingRes.getDrawable(iconId));
+ } else {
+ int iconId = PresenceUtils.getStatusIconId(presence);
+ mPresence.setImageDrawable(brandingRes.getDrawable(iconId));
+ }
+
+ // line1
+ CharSequence line1;
+ if (Im.Contacts.TYPE_GROUP == type) {
+ ContentResolver resolver = getContext().getContentResolver();
+ long id = cursor.getLong(ContactView.COLUMN_CONTACT_ID);
+ line1 = queryGroupMembers(resolver, id);
+ } else {
+ line1 = TextUtils.isEmpty(nickname) ?
+ ImpsAddressUtils.getDisplayableAddress(username) : nickname;
+
+ if (!TextUtils.isEmpty(underLineText)) {
+ // highlight/underline the word being searched
+ String lowercase = line1.toString().toLowerCase();
+ int start = lowercase.indexOf(underLineText.toLowerCase());
+ if (start >= 0) {
+ int end = start + underLineText.length();
+ SpannableString str = new SpannableString(line1);
+ str.setSpan(new UnderlineSpan(), start, end,
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ line1 = str;
+ }
+ }
+
+ if (Im.Contacts.TYPE_TEMPORARY == type) {
+ // Add a mark at the front of name if it's only a temporary
+ // contact.
+ SpannableStringBuilder str = new SpannableStringBuilder(
+ r.getText(R.string.unknown_contact));
+ str.setSpan(new RelativeSizeSpan(0.8f), 0, str.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ str.append(line1);
+ line1 = str;
+ }
+ }
+ mLine1.setText(line1);
+
+ // time stamp
+ if (showChatMsg && hasChat) {
+ mTimeStamp.setVisibility(VISIBLE);
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(cursor.getLong(COLUMN_LAST_MESSAGE_DATE));
+ DateFormat formatter = DateFormat.getTimeInstance(DateFormat.SHORT);
+ mTimeStamp.setText(formatter.format(cal.getTime()));
+ } else {
+ mTimeStamp.setVisibility(GONE);
+ }
+
+ // line2
+ CharSequence line2 = null;
+ if (showChatMsg) {
+ line2 = lastMsg;
+ }
+
+ if (TextUtils.isEmpty(line2)){
+ if (Im.Contacts.TYPE_GROUP == type) {
+ // Show nothing in line2 if it's a group and don't
+ // have any unread message.
+ line2 = null;
+ } else {
+ // Show the custom status text if there's no new message.
+ line2 = statusText;
+ }
+ }
+
+ if (TextUtils.isEmpty(line2)) {
+ // Show a string of presence if there is neither new message nor
+ // custom status text.
+ line2 = brandingRes.getString(PresenceUtils.getStatusStringRes(presence));
+ }
+
+ mLine2.setText(line2);
+
+ View contactInfoPanel = findViewById(R.id.contactInfo);
+ if (hasChat && showChatMsg) {
+ contactInfoPanel.setBackgroundResource(R.drawable.list_item_im_bubble);
+ mLine1.setTextColor(r.getColor(R.color.chat_contact));
+ } else {
+ contactInfoPanel.setBackgroundDrawable(null);
+ contactInfoPanel.setPadding(4, 0, 0, 0);
+ mLine1.setTextColor(r.getColor(R.color.nonchat_contact));
+ }
+ }
+
+ private String queryGroupMembers(ContentResolver resolver, long groupId) {
+ String[] projection = { Im.GroupMembers.NICKNAME };
+ Uri uri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, groupId);
+ Cursor c = resolver.query(uri, projection, null, null, null);
+ StringBuilder buf = new StringBuilder();
+ if(c != null) {
+ while(c.moveToNext()) {
+ buf.append(c.getString(0));
+ if(!c.isLast()) {
+ buf.append(',');
+ }
+ }
+ c.close();
+ }
+ return buf.toString();
+ }
+}
diff --git a/src/com/android/im/app/ContactsPickerActivity.java b/src/com/android/im/app/ContactsPickerActivity.java
new file mode 100644
index 0000000..eb52aa3
--- /dev/null
+++ b/src/com/android/im/app/ContactsPickerActivity.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import com.android.im.R;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Im;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Filter;
+import android.widget.ListView;
+import android.widget.ResourceCursorAdapter;
+
+/**
+ * Activity used to pick a contact.
+ */
+public class ContactsPickerActivity extends ListActivity {
+ public final static String EXTRA_EXCLUDED_CONTACTS = "excludes";
+
+ public final static String EXTRA_RESULT_USERNAME = "result";
+
+ private ContactsAdapter mAdapter;
+ private String mExcludeClause;
+ Uri mData;
+ Filter mFilter;
+
+ private static final void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "<ContactsPickerActivity> " + msg);
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.contacts_picker_activity);
+ if(!resolveIntent()){
+ if(Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("no data, finish");
+ }
+ finish();
+ return;
+ }
+
+ EditText filter = (EditText)findViewById(R.id.filter);
+ filter.addTextChangedListener(new TextWatcher() {
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ mFilter.filter(s);
+ }
+
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ private boolean resolveIntent() {
+ Intent i = getIntent();
+ mData = i.getData();
+
+ if(mData == null) {
+ return false;
+ }
+ mExcludeClause = buildExcludeClause(i.getStringArrayExtra(EXTRA_EXCLUDED_CONTACTS));
+ Cursor cursor = managedQuery(mData, ContactView.CONTACT_PROJECTION,
+ mExcludeClause, Im.Contacts.DEFAULT_SORT_ORDER);
+ if (cursor == null) {
+ return false;
+ }
+
+ mAdapter = new ContactsAdapter(this, cursor);
+ mFilter = mAdapter.getFilter();
+ setListAdapter(mAdapter);
+ return true;
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ Cursor cursor = (Cursor)mAdapter.getItem(position);
+ Intent data = new Intent();
+ data.putExtra(EXTRA_RESULT_USERNAME,
+ cursor.getString(ContactView.COLUMN_CONTACT_USERNAME));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ private static String buildExcludeClause(String[] excluded) {
+ if (excluded == null || excluded.length == 0) {
+ return null;
+ }
+
+ StringBuilder clause = new StringBuilder();
+ clause.append(Im.Contacts.USERNAME);
+ clause.append(" NOT IN (");
+ int len = excluded.length;
+ for (int i = 0; i < len - 1; i++) {
+ DatabaseUtils.appendValueToSql(clause, excluded[i]);
+ clause.append(',');
+ }
+ DatabaseUtils.appendValueToSql(clause, excluded[len - 1]);
+ clause.append(')');
+ return clause.toString();
+ }
+
+ Cursor runQuery(CharSequence constraint) {
+ String where;
+ if (constraint == null) {
+ where = mExcludeClause;
+ } else {
+ StringBuilder buf = new StringBuilder();
+ if (mExcludeClause != null) {
+ buf.append(mExcludeClause).append(" AND ");
+ }
+
+ buf.append(Im.Contacts.NICKNAME);
+ buf.append(" LIKE ");
+ DatabaseUtils.appendValueToSql(buf, "%" + constraint + "%");
+
+ where = buf.toString();
+ }
+ return managedQuery(mData, ContactView.CONTACT_PROJECTION, where,
+ Im.Contacts.DEFAULT_SORT_ORDER);
+ }
+
+ private class ContactsAdapter extends ResourceCursorAdapter {
+ private String mConstraints;
+
+ public ContactsAdapter(Context context, Cursor c) {
+ super(context, R.layout.contact_view, c);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ContactView v = (ContactView)view;
+ v.setPadding(0, 0, 0, 0);
+ v.bind(cursor, mConstraints, false);
+ }
+
+ @Override
+ public void changeCursor(Cursor cursor) {
+ if(mCursor != null && mCursor != cursor) {
+ mCursor.deactivate();
+ }
+ super.changeCursor(cursor);
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ mConstraints = constraint.toString();
+
+ return ContactsPickerActivity.this.runQuery(constraint);
+ }
+ }
+
+}
diff --git a/src/com/android/im/app/Dashboard.java b/src/com/android/im/app/Dashboard.java
new file mode 100644
index 0000000..e9ca8ae
--- /dev/null
+++ b/src/com/android/im/app/Dashboard.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.R;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.Im;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+import android.widget.AdapterView;
+import android.widget.Gallery;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+public class Dashboard extends LinearLayout implements Gallery.OnItemClickListener {
+ private Gallery mGallery;
+
+ private Cursor mChats;
+
+ private long mAccountId;
+ private String mUserName;
+ Activity mActivity;
+
+ public Dashboard(Context screen, AttributeSet attrs) {
+ super(screen, attrs);
+ }
+
+ public static final void openDashboard(Activity parent, long accountId, String username) {
+ LayoutInflater inflate = LayoutInflater.from(parent);
+ View v = inflate.inflate(R.layout.dashboard, null);
+
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND,
+ PixelFormat.TRANSLUCENT);
+
+ lp.width = WindowManager.LayoutParams.FILL_PARENT;
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ lp.token = parent.getWindow().peekDecorView().getWindowToken();
+
+ WindowManagerImpl.getDefault().addView(v, lp);
+
+ ((Dashboard) v).init(parent, accountId, username);
+ }
+
+ public final void init(Activity activity, long accountId, String username) {
+ mActivity = activity;
+ mAccountId = accountId;
+ mUserName = username;
+ mGallery = (Gallery) findViewById(R.id.chats_gallery);
+
+ mGallery.setEmptyView(findViewById(R.id.empty));
+
+ ContentResolver cr = mContext.getContentResolver();
+
+ mChats = cr.query(Im.Contacts.CONTENT_URI_CHAT_CONTACTS, null, null, null, null);
+ mGallery.setAdapter(new DashboardAdapter(mContext, mChats));
+ mGallery.setSelection(getInitialPosition());
+ mGallery.setOnItemClickListener(this);
+ mGallery.requestFocus();
+ }
+
+ public final void init(Activity activity) {
+ init(activity, -1L, null);
+ }
+
+ @Override
+ public final boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ closeDashboard();
+ return true;
+ }
+
+ return super.dispatchKeyEvent(event);
+ }
+
+ void closeDashboard() {
+ WindowManagerImpl.getDefault().removeView(this);
+ if (mChats != null) {
+ mChats.deactivate();
+ }
+ }
+
+ private int getInitialPosition() {
+ if ((mAccountId == -1) || (mUserName == null)) {
+ return -1;
+ }
+
+ int usernameColumn = mChats.getColumnIndexOrThrow(Im.Contacts.USERNAME);
+ int accountColumn = mChats.getColumnIndexOrThrow(Im.Contacts.ACCOUNT);
+
+ mChats.moveToPosition(-1);
+ while (mChats.moveToNext()) {
+ if ((mAccountId == mChats.getLong(accountColumn))
+ && mUserName.equals(mChats.getString(usernameColumn))) {
+ return mChats.getPosition();
+ }
+ }
+ return -1;
+ }
+
+ private class DashboardAdapter extends ResourceCursorAdapter {
+ public DashboardAdapter(Context context, Cursor c) {
+ super(context, R.layout.dashboard_item, c);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor c) {
+ long providerId = c.getLong(c.getColumnIndexOrThrow(Im.Contacts.PROVIDER));
+ String nickname = c.getString(c.getColumnIndexOrThrow(Im.Contacts.NICKNAME));
+ TextView t = (TextView) view.findViewById(R.id.name);
+
+ t.setText(nickname);
+
+ ImageView i = (ImageView) view.findViewById(R.id.presence);
+ int presenceMode = c.getInt(c.getColumnIndexOrThrow(Im.Contacts.PRESENCE_STATUS));
+ ImApp app = ImApp.getApplication(mActivity);
+ BrandingResources brandingRes = app.getBrandingResource(providerId);
+ Drawable presenceIcon = brandingRes.getDrawable(
+ PresenceUtils.getStatusIconId(presenceMode));
+ i.setImageDrawable(presenceIcon);
+
+ setAvatar(c, view);
+ }
+
+ private void setAvatar(Cursor c, View v) {
+ ImageView i = (ImageView) v.findViewById(R.id.avatar);
+
+ int avatarDataColumn = c.getColumnIndexOrThrow(Im.Contacts.AVATAR_DATA);
+ Drawable avatar = DatabaseUtils.getAvatarFromCursor(c, avatarDataColumn);
+
+ if (avatar == null) {
+ setBitmapResource(i, R.drawable.avatar_unknown);
+ } else {
+ i.setImageDrawable(avatar);
+ }
+ }
+
+ private final void setBitmapResource(ImageView i, int r) {
+ Resources res = mContext.getResources();
+
+ i.setImageDrawable(res.getDrawable(r));
+ }
+ }
+
+ public final void onItemClick(AdapterView parent, View view, int position, long id) {
+ Cursor c = (Cursor) mGallery.getItemAtPosition(position);
+ String contact = c.getString(c.getColumnIndexOrThrow(Im.Contacts.USERNAME));
+ long account = c.getLong(c.getColumnIndexOrThrow(Im.Contacts.ACCOUNT));
+
+ if ((account == mAccountId) && contact.equals(mUserName)) {
+ closeDashboard();
+ return;
+ }
+
+ Intent intent;
+ Uri uri = ContentUris.withAppendedId(Im.Chats.CONTENT_URI, id);
+ intent = new Intent(Intent.ACTION_VIEW, uri);
+
+ closeDashboard();
+ mActivity.finish();
+ mActivity.startActivity(intent);
+ }
+}
diff --git a/src/com/android/im/app/DatabaseUtils.java b/src/com/android/im/app/DatabaseUtils.java
new file mode 100644
index 0000000..90d0433
--- /dev/null
+++ b/src/com/android/im/app/DatabaseUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.Im;
+
+public class DatabaseUtils {
+ private DatabaseUtils() {
+ }
+
+ public static Cursor queryAccountsForProvider(ContentResolver cr,
+ String[] projection, long providerId) {
+ StringBuilder where = new StringBuilder(Im.Account.ACTIVE);
+ where.append("=1 AND ").append(Im.Account.PROVIDER).append('=').append(providerId);
+ Cursor c = cr.query(Im.Account.CONTENT_URI, projection, where.toString(), null, null);
+ if (c != null && !c.moveToFirst()) {
+ c.close();
+ return null;
+ }
+ return c;
+ }
+
+ public static Drawable getAvatarFromCursor(Cursor cursor, int dataColumn) {
+ byte[] rawData = cursor.getBlob(dataColumn);
+ if (rawData == null) {
+ return null;
+ }
+ return decodeAvatar(rawData);
+ }
+
+ public static Uri getAvatarUri(Uri baseUri, long providerId, long accountId) {
+ Uri.Builder builder = baseUri.buildUpon();
+ ContentUris.appendId(builder, providerId);
+ ContentUris.appendId(builder, accountId);
+ return builder.build();
+ }
+
+ public static Drawable getAvatarFromCursor(Cursor cursor, int dataColumn,
+ int encodedDataColumn, String username, boolean updateBlobUseCursor,
+ ContentResolver resolver, Uri updateBlobUri) {
+ /**
+ * Optimization: the avatar table in IM content provider have two
+ * columns, one for the raw blob data, another for the base64 encoded
+ * data. The reason for this is when the avatars are initially
+ * downloaded, they are in the base64 encoded form, and instead of
+ * base64 decode the avatars for all the buddies up front, we can just
+ * simply store the encoded data in the table, and decode them on demand
+ * when displaying them. Once we decode the avatar, we store the decoded
+ * data as a blob, and null out the encoded column in the avatars table.
+ * query the raw blob data first, if present, great; if not, query the
+ * encoded data, decode it and store as the blob, and null out the
+ * encoded column.
+ */
+ byte[] rawData = cursor.getBlob(dataColumn);
+
+ if (rawData == null) {
+ String encodedData = cursor.getString(encodedDataColumn);
+ if (encodedData == null) {
+ // Log.e(LogTag.LOG_TAG, "getAvatarFromCursor for " + username +
+ // ", no raw or encoded data!");
+ return null;
+ }
+
+ rawData = android.os.Base64Utils.decodeBase64(encodedData);
+
+ // if (DBG) {
+ // log("getAvatarFromCursor for " + username + ": found encoded
+ // data,"
+ // + " update blob with data, len=" + rawData.length);
+ // }
+
+ if (updateBlobUseCursor) {
+ cursor.updateBlob(dataColumn, rawData);
+ cursor.updateString(encodedDataColumn, null);
+ cursor.commitUpdates();
+ } else {
+ updateAvatarBlob(resolver, updateBlobUri, rawData, username);
+ }
+ }
+
+ return decodeAvatar(rawData);
+ }
+
+ private static void updateAvatarBlob(ContentResolver resolver, Uri updateUri, byte[] data,
+ String username) {
+ ContentValues values = new ContentValues(3);
+ values.put(Im.Avatars.DATA, data);
+
+ StringBuilder buf = new StringBuilder(Im.Avatars.CONTACT);
+ buf.append("=?");
+
+ String[] selectionArgs = new String[] {
+ username
+ };
+
+ resolver.update(updateUri, values, buf.toString(), selectionArgs);
+ }
+
+ private static Drawable decodeAvatar(byte[] data) {
+ Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length);
+ Drawable avatar = new BitmapDrawable(b);
+ return avatar;
+ }
+}
diff --git a/src/com/android/im/app/ErrorResUtils.java b/src/com/android/im/app/ErrorResUtils.java
new file mode 100644
index 0000000..3f6eee0
--- /dev/null
+++ b/src/com/android/im/app/ErrorResUtils.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.R;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.imps.ImpsErrorInfo;
+
+import android.content.res.Resources;
+
+public class ErrorResUtils {
+
+ public static String getErrorRes(Resources res, int code, Object... args) {
+ int resId = getErrorResId(code);
+ if (resId == 0) {
+ return res.getString(R.string.general_error, code);
+ } else {
+ return res.getString(resId, args);
+ }
+ }
+
+ private static int getErrorResId(int code) {
+ switch (code) {
+ case ImErrorInfo.ILLEGAL_CONTACT_LIST_MANAGER_STATE:
+ return R.string.contact_not_loaded;
+
+ case ImErrorInfo.CONTACT_EXISTS_IN_LIST:
+ return R.string.contact_already_exist;
+
+ case ImErrorInfo.CANT_ADD_BLOCKED_CONTACT:
+ return R.string.contact_blocked;
+
+ case ImErrorInfo.CANT_CONNECT_TO_SERVER:
+ return R.string.cant_connect_to_server;
+
+ case ImErrorInfo.NETWORK_ERROR:
+ return R.string.network_error;
+
+ case ImpsErrorInfo.SERVICE_NOT_SUPPORTED:
+ return R.string.service_not_support;
+
+ case ImpsErrorInfo.INVALID_PASSWORD:
+ return R.string.invalid_password;
+
+ case ImpsErrorInfo.INTERNAL_SERVER_OR_NETWORK_ERROR:
+ return R.string.internal_server_error;
+
+ case ImpsErrorInfo.NOT_IMPLMENTED:
+ return R.string.not_implemented;
+
+ case ImpsErrorInfo.SERVER_UNAVAILABLE:
+ return R.string.service_unavaiable;
+
+ case ImpsErrorInfo.TIMEOUT:
+ return R.string.timeout;
+
+ case ImpsErrorInfo.VERSION_NOT_SUPPORTED:
+ return R.string.version_not_supported;
+
+ case ImpsErrorInfo.MESSAGE_QUEUE_FULL:
+ return R.string.message_queue_full;
+
+ case ImpsErrorInfo.DOMAIN_NOT_SUPPORTED:
+ return R.string.domain_not_supported;
+
+ case ImpsErrorInfo.UNKNOWN_USER:
+ return R.string.unknown_user;
+
+ case ImpsErrorInfo.RECIPIENT_BLOCKED_SENDER:
+ return R.string.recipient_blocked_the_user;
+
+ case ImpsErrorInfo.SESSION_EXPIRED:
+ return R.string.session_expired;
+
+ case ImpsErrorInfo.FORCED_LOGOUT:
+ return R.string.forced_logout;
+
+ case ImpsErrorInfo.ALREADY_LOGGED:
+ return R.string.already_logged_in;
+
+ case ImErrorInfo.NOT_LOGGED_IN:
+ return R.string.not_signed_in;
+
+ case ImpsErrorInfo.MSISDN_ERROR:
+ return R.string.msisdn_error;
+
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/src/com/android/im/app/ImApp.java b/src/com/android/im/app/ImApp.java
new file mode 100644
index 0000000..2ff011e
--- /dev/null
+++ b/src/com/android/im/app/ImApp.java
@@ -0,0 +1,653 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Broadcaster;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.util.Log;
+
+import com.android.im.IConnectionCreationListener;
+import com.android.im.IImConnection;
+import com.android.im.IRemoteImService;
+import com.android.im.R;
+import com.android.im.app.adapter.ConnectionListenerAdapter;
+import com.android.im.engine.ImConnection;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.plugin.BrandingResourceIDs;
+import com.android.im.plugin.ImPluginInfo;
+import com.android.im.service.ImServiceConstants;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+public class ImApp extends Application {
+ public static final String LOG_TAG = "ImApp";
+
+ public static final String EXTRA_INTENT_SEND_TO_USER = "Send2_U";
+ public static final String EXTRA_INTENT_PASSWORD = "password";
+
+ private static ImApp sImApp;
+
+ IRemoteImService mImService;
+
+ HashMap<Long, IImConnection> mConnections;
+ MyConnListener mConnectionListener;
+ HashMap<Long, ProviderDef> mProviders;
+
+ Broadcaster mBroadcaster;
+
+ /** A queue of messages that are waiting to be sent when service is connected.*/
+ ArrayList<Message> mQueue = new ArrayList<Message>();
+
+ /** A flag indicates that we have called to start the service.*/
+ private boolean mServiceStarted;
+ private Context mApplicationContext;
+ private Resources mPrivateResources;
+
+ private HashMap<Long, BrandingResources> mBrandingResources;
+ private BrandingResources mDefaultBrandingResources;
+
+ public static final int EVENT_SERVICE_CONNECTED = 100;
+ public static final int EVENT_CONNECTION_CREATED = 150;
+ public static final int EVENT_CONNECTION_LOGGING_IN = 200;
+ public static final int EVENT_CONNECTION_LOGGED_IN = 201;
+ public static final int EVENT_CONNECTION_LOGGING_OUT = 202;
+ public static final int EVENT_CONNECTION_DISCONNECTED = 203;
+ public static final int EVENT_CONNECTION_SUSPENDED = 204;
+ public static final int EVENT_USER_PRESENCE_UPDATED = 300;
+ public static final int EVENT_UPDATE_USER_PRESENCE_ERROR = 301;
+
+ private static final String[] PROVIDER_PROJECTION = {
+ Im.Provider._ID,
+ Im.Provider.NAME,
+ Im.Provider.FULLNAME,
+ Im.Provider.SIGNUP_URL,
+ };
+
+ private static final String[] ACCOUNT_PROJECTION = {
+ Im.Account._ID,
+ Im.Account.PROVIDER,
+ Im.Account.NAME,
+ Im.Account.USERNAME,
+ Im.Account.PASSWORD,
+ };
+
+ static final void log(String log) {
+ Log.d(LOG_TAG, log);
+ }
+
+ public static ImApp getApplication(Activity activity) {
+ // TODO should this be synchronized?
+ if (sImApp == null) {
+ initialize(activity);
+ }
+
+ return sImApp;
+ }
+
+ /**
+ * Initialize performs the manual ImApp instantiation and initialization. When the
+ * ImApp is started first in the process, the ImApp public constructor should be called,
+ * and sImApp initialized. So calling initialize() later should have no effect. However,
+ * if another application runs in the same process and is started first, the ImApp
+ * application object won't be instantiated, and we need to call initialize() manually to
+ * instantiate and initialize it.
+ */
+ private static void initialize(Activity activity) {
+ // construct the TalkApp manually and call onCreate().
+ sImApp = new ImApp();
+ sImApp.mApplicationContext = activity.getApplication();
+ sImApp.mPrivateResources = activity.getResources();
+ sImApp.onCreate();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (mApplicationContext == this) {
+ return super.getResources();
+ }
+
+ return mPrivateResources;
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ if (mApplicationContext == this) {
+ return super.getContentResolver();
+ }
+
+ return mApplicationContext.getContentResolver();
+ }
+
+ public ImApp() {
+ super();
+ mConnections = new HashMap<Long, IImConnection>();
+ mApplicationContext = this;
+ sImApp = this;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBroadcaster = new Broadcaster();
+ loadDefaultBrandingRes();
+ mBrandingResources = new HashMap<Long, BrandingResources>();
+ }
+
+ @Override
+ public void onTerminate() {
+ stopImServiceIfInactive();
+ if (mImService != null) {
+ try {
+ mImService.removeConnectionCreatedListener(mConnCreationListener);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, "failed to remove ConnectionCreatedListener");
+ }
+ }
+
+ super.onTerminate();
+ }
+
+ public synchronized void startImServiceIfNeed() {
+ if(!mServiceStarted) {
+ if(Log.isLoggable(LOG_TAG, Log.DEBUG)) log("start ImService");
+
+ Intent serviceIntent = new Intent();
+ serviceIntent.setComponent(ImServiceConstants.IM_SERVICE_COMPONENT);
+ mApplicationContext.startService(serviceIntent);
+ mApplicationContext.bindService(serviceIntent, mImServiceConn, Context.BIND_AUTO_CREATE);
+ mServiceStarted = true;
+
+ mConnectionListener = new MyConnListener(new Handler());
+ }
+ }
+
+ public synchronized void stopImServiceIfInactive() {
+ boolean hasActiveConnection = true;
+ synchronized (mConnections) {
+ hasActiveConnection = !mConnections.isEmpty();
+ }
+
+ if (!hasActiveConnection && mServiceStarted) {
+ if (Log.isLoggable(LOG_TAG, Log.DEBUG))
+ log("stop ImService because there's no active connections");
+
+ if(mImService != null) {
+ mApplicationContext.unbindService(mImServiceConn);
+ mImService = null;
+ }
+ Intent intent = new Intent();
+ intent.setComponent(ImServiceConstants.IM_SERVICE_COMPONENT);
+ mApplicationContext.stopService(intent);
+ mServiceStarted = false;
+ }
+ }
+
+ private ServiceConnection mImServiceConn = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if(Log.isLoggable(LOG_TAG, Log.DEBUG))
+ log("service connected");
+
+ mImService = IRemoteImService.Stub.asInterface(service);
+ fetchActiveConnections();
+ loadBrandingResources();
+
+ synchronized (mQueue) {
+ for (Message msg : mQueue) {
+ msg.sendToTarget();
+ }
+ mQueue.clear();
+ }
+ Message msg = Message.obtain(null, EVENT_SERVICE_CONNECTED);
+ mBroadcaster.broadcast(msg);
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ if(Log.isLoggable(LOG_TAG, Log.DEBUG))
+ log("service disconnected");
+
+ mConnections.clear();
+ mImService = null;
+ }
+ };
+
+ public boolean serviceConnected() {
+ return mImService != null;
+ }
+
+ public static long insertOrUpdateAccount(ContentResolver cr,
+ long providerId, String userName, String pw) {
+ String selection = Im.Account.PROVIDER + "=? AND " + Im.Account.USERNAME + "=?";
+ String[] selectionArgs = {Long.toString(providerId), userName };
+
+ Cursor c = cr.query(Im.Account.CONTENT_URI, ACCOUNT_PROJECTION,
+ selection, selectionArgs, null);
+ if (c != null && c.moveToFirst()) {
+ // Update the password
+ c.updateString(c.getColumnIndexOrThrow(Im.Account.PASSWORD), pw);
+ c.commitUpdates();
+
+ long id = c.getLong(c.getColumnIndexOrThrow(Im.Account._ID));
+ c.close();
+ return id;
+ } else {
+ ContentValues values = new ContentValues(4);
+ values.put(Im.Account.PROVIDER, providerId);
+ values.put(Im.Account.NAME, userName);
+ values.put(Im.Account.USERNAME, userName);
+ values.put(Im.Account.PASSWORD, pw);
+
+ Uri result = cr.insert(Im.Account.CONTENT_URI, values);
+ return ContentUris.parseId(result);
+ }
+ }
+
+ private void loadImProviderSettings() {
+ if (mProviders != null) {
+ return;
+ }
+ mProviders = new HashMap<Long, ProviderDef>();
+ ContentResolver cr = getContentResolver();
+ Cursor c = cr.query(Im.Provider.CONTENT_URI, PROVIDER_PROJECTION,
+ null, null, null);
+ if (c == null) {
+ return;
+ }
+
+ while (c.moveToNext()) {
+ long id = c.getLong(0);
+ String providerName = c.getString(1);
+ String fullName = c.getString(2);
+ String signUpUrl = c.getString(3);
+
+ mProviders.put(id, new ProviderDef(id, providerName, fullName, signUpUrl));
+ }
+
+ c.close();
+ }
+
+ private void loadDefaultBrandingRes() {
+ HashMap<Integer, Integer> resMapping = new HashMap<Integer, Integer>();
+
+ resMapping.put(BrandingResourceIDs.DRAWABLE_LOGO, R.drawable.imlogo_s);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_ONLINE,
+ android.R.drawable.presence_online);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_AWAY,
+ android.R.drawable.presence_away);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_BUSY,
+ android.R.drawable.presence_busy);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_INVISIBLE,
+ android.R.drawable.presence_invisible);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_OFFLINE,
+ android.R.drawable.presence_offline);
+ resMapping.put(BrandingResourceIDs.DRAWABLE_BLOCK, R.drawable.ic_im_block);
+
+ resMapping.put(BrandingResourceIDs.STRING_ARRAY_SMILEY_NAMES, R.array.default_smiley_names);
+ resMapping.put(BrandingResourceIDs.STRING_ARRAY_SMILEY_TEXTS, R.array.default_smiley_texts);
+
+ resMapping.put(BrandingResourceIDs.STRING_PRESENCE_AVAILABLE, R.string.presence_available);
+ resMapping.put(BrandingResourceIDs.STRING_PRESENCE_BUSY, R.string.presence_busy);
+ resMapping.put(BrandingResourceIDs.STRING_PRESENCE_AWAY, R.string.presence_away);
+ resMapping.put(BrandingResourceIDs.STRING_PRESENCE_IDLE, R.string.presence_idle);
+ resMapping.put(BrandingResourceIDs.STRING_PRESENCE_OFFLINE, R.string.presence_offline);
+ resMapping.put(BrandingResourceIDs.STRING_PRESENCE_INVISIBLE, R.string.presence_invisible);
+ resMapping.put(BrandingResourceIDs.STRING_LABEL_USERNAME, R.string.label_username);
+ resMapping.put(BrandingResourceIDs.STRING_ONGOING_CONVERSATION,
+ R.string.ongoing_conversation);
+ resMapping.put(BrandingResourceIDs.STRING_ADD_CONTACT_TITLE, R.string.add_contact_title);
+ resMapping.put(BrandingResourceIDs.STRING_LABEL_INPUT_CONTACT,
+ R.string.input_contact_label);
+ resMapping.put(BrandingResourceIDs.STRING_BUTTON_ADD_CONTACT, R.string.invite_label);
+ resMapping.put(BrandingResourceIDs.STRING_CONTACT_INFO_TITLE,
+ R.string.contact_profile_title);
+
+ resMapping.put(BrandingResourceIDs.STRING_MENU_ADD_CONTACT, R.string.menu_add_contact);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_BLOCK_CONTACT, R.string.menu_block_contact);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_CONTACT_LIST,
+ R.string.menu_view_contact_list);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_DELETE_CONTACT,
+ R.string.menu_remove_contact);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_END_CHAT, R.string.menu_end_conversation);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_INSERT_SMILEY, R.string.menu_insert_smiley);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_START_CHAT, R.string.menu_start_chat);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_VIEW_PROFILE, R.string.menu_view_profile);
+ resMapping.put(BrandingResourceIDs.STRING_MENU_SWITCH_CHATS, R.string.menu_switch_chats);
+
+ resMapping.put(BrandingResourceIDs.STRING_TOAST_CHECK_AUTO_SIGN_IN,
+ R.string.check_auto_sign_in);
+ resMapping.put(BrandingResourceIDs.STRING_LABEL_SIGN_UP, R.string.sign_up);
+
+ mDefaultBrandingResources = new BrandingResources(this, resMapping, null /* default res */);
+ }
+
+ void loadBrandingResources() {
+ try {
+ List<ImPluginInfo> plugins = mImService.getAllPlugins();
+ for (ImPluginInfo plugin : plugins) {
+ long providerId = getProviderId(plugin.mProviderName);
+ if (!mBrandingResources.containsKey(providerId)) {
+ BrandingResources res = new BrandingResources(this,
+ plugin, mDefaultBrandingResources);
+ mBrandingResources.put(providerId, res);
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Service died while loading branding resource");
+ }
+ }
+
+ public long getProviderId(String name) {
+ loadImProviderSettings();
+ for (ProviderDef provider: mProviders.values()) {
+ if(provider.mName.equals(name)) {
+ return provider.mId;
+ }
+ }
+ return -1;
+ }
+
+ public ProviderDef getProvider(long id) {
+ loadImProviderSettings();
+ return mProviders.get(id);
+ }
+
+ public List<ProviderDef> getProviders() {
+ loadImProviderSettings();
+ ArrayList<ProviderDef> result = new ArrayList<ProviderDef>();
+ result.addAll(mProviders.values());
+ return result;
+ }
+
+ public BrandingResources getBrandingResource(long providerId) {
+ BrandingResources res = mBrandingResources.get(providerId);
+ return res == null ? mDefaultBrandingResources : res;
+ }
+
+ public IImConnection createConnection(long providerId) throws RemoteException {
+ IImConnection conn = getConnection(providerId);
+ if (conn == null) {
+ conn = mImService.createConnection(providerId);
+ }
+ return conn;
+ }
+
+ IImConnection getConnection(long providerId) {
+ synchronized (mConnections) {
+ return mConnections.get(providerId);
+ }
+ }
+
+ public IImConnection getConnectionByAccount(long accountId) {
+ synchronized (mConnections) {
+ for (IImConnection conn : mConnections.values()) {
+ try {
+ if (conn.getAccountId() == accountId) {
+ return conn;
+ }
+ } catch (RemoteException e) {
+ // No server!
+ }
+ }
+ return null;
+ }
+ }
+
+ public List<IImConnection> getActiveConnections() {
+ synchronized (mConnections) {
+ ArrayList<IImConnection> result = new ArrayList<IImConnection>();
+ result.addAll(mConnections.values());
+ return result;
+ }
+ }
+
+ public void callWhenServiceConnected(Handler target, Runnable callback) {
+ Message msg = Message.obtain(target, callback);
+ if (serviceConnected()) {
+ msg.sendToTarget();
+ } else {
+ startImServiceIfNeed();
+ synchronized (mQueue) {
+ mQueue.add(msg);
+ }
+ }
+ }
+
+ public void removePendingCall(Handler target) {
+ synchronized (mQueue) {
+ Iterator<Message> iter = mQueue.iterator();
+ while (iter.hasNext()) {
+ Message msg = iter.next();
+ if (msg.getTarget() == target) {
+ iter.remove();
+ }
+ }
+ }
+ }
+
+ public void registerForBroadcastEvent(int what, Handler target) {
+ mBroadcaster.request(what, target, what);
+ }
+
+ public void unregisterForBroadcastEvent(int what, Handler target) {
+ mBroadcaster.cancelRequest(what, target, what);
+ }
+
+ public void registerForConnEvents(Handler handler) {
+ mBroadcaster.request(EVENT_CONNECTION_CREATED, handler,
+ EVENT_CONNECTION_CREATED);
+ mBroadcaster.request(EVENT_CONNECTION_LOGGING_IN, handler,
+ EVENT_CONNECTION_LOGGING_IN);
+ mBroadcaster.request(EVENT_CONNECTION_LOGGED_IN, handler,
+ EVENT_CONNECTION_LOGGED_IN);
+ mBroadcaster.request(EVENT_CONNECTION_LOGGING_OUT, handler,
+ EVENT_CONNECTION_LOGGING_OUT);
+ mBroadcaster.request(EVENT_CONNECTION_SUSPENDED, handler,
+ EVENT_CONNECTION_SUSPENDED);
+ mBroadcaster.request(EVENT_CONNECTION_DISCONNECTED, handler,
+ EVENT_CONNECTION_DISCONNECTED);
+ mBroadcaster.request(EVENT_USER_PRESENCE_UPDATED, handler,
+ EVENT_USER_PRESENCE_UPDATED);
+ mBroadcaster.request(EVENT_UPDATE_USER_PRESENCE_ERROR, handler,
+ EVENT_UPDATE_USER_PRESENCE_ERROR);
+ }
+
+ public void unregisterForConnEvents(Handler handler) {
+ mBroadcaster.cancelRequest(EVENT_CONNECTION_CREATED, handler,
+ EVENT_CONNECTION_CREATED);
+ mBroadcaster.cancelRequest(EVENT_CONNECTION_LOGGING_IN, handler,
+ EVENT_CONNECTION_LOGGING_IN);
+ mBroadcaster.cancelRequest(EVENT_CONNECTION_LOGGED_IN, handler,
+ EVENT_CONNECTION_LOGGED_IN);
+ mBroadcaster.cancelRequest(EVENT_CONNECTION_LOGGING_OUT, handler,
+ EVENT_CONNECTION_LOGGING_OUT);
+ mBroadcaster.cancelRequest(EVENT_CONNECTION_SUSPENDED, handler,
+ EVENT_CONNECTION_SUSPENDED);
+ mBroadcaster.cancelRequest(EVENT_CONNECTION_DISCONNECTED, handler,
+ EVENT_CONNECTION_DISCONNECTED);
+ mBroadcaster.cancelRequest(EVENT_USER_PRESENCE_UPDATED, handler,
+ EVENT_USER_PRESENCE_UPDATED);
+ mBroadcaster.cancelRequest(EVENT_UPDATE_USER_PRESENCE_ERROR, handler,
+ EVENT_UPDATE_USER_PRESENCE_ERROR);
+ }
+
+ void broadcastConnEvent(int what, long providerId, ImErrorInfo error) {
+ if(Log.isLoggable(LOG_TAG, Log.DEBUG)){
+ log("broadcasting connection event " + what + ", provider id " + providerId);
+ }
+ android.os.Message msg = android.os.Message.obtain(
+ null,
+ what,
+ (int)(providerId >> 32), (int)providerId,
+ error);
+ mBroadcaster.broadcast(msg);
+ }
+
+ public void dismissNotifications(long providerId) {
+ if (mImService != null) {
+ try {
+ mImService.dismissNotifications(providerId);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ private void fetchActiveConnections() {
+ try {
+ // register the listener before fetch so that we won't miss any connection.
+ mImService.addConnectionCreatedListener(mConnCreationListener);
+ synchronized (mConnections) {
+ for(IBinder binder: (List<IBinder>) mImService.getActiveConnections()) {
+ IImConnection conn = IImConnection.Stub.asInterface(binder);
+ long providerId = conn.getProviderId();
+ if (!mConnections.containsKey(providerId)) {
+ mConnections.put(providerId, conn);
+ conn.registerConnectionListener(mConnectionListener);
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "fetching active connections", e);
+ }
+ }
+
+ private final IConnectionCreationListener mConnCreationListener
+ = new IConnectionCreationListener.Stub() {
+ public void onConnectionCreated(IImConnection conn)
+ throws RemoteException {
+ long providerId = conn.getProviderId();
+ synchronized (mConnections) {
+ if (!mConnections.containsKey(providerId)) {
+ mConnections.put(providerId, conn);
+ conn.registerConnectionListener(mConnectionListener);
+ }
+ }
+ broadcastConnEvent(EVENT_CONNECTION_CREATED, providerId, null);
+ }
+ };
+
+ private final class MyConnListener extends ConnectionListenerAdapter {
+ public MyConnListener(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onConnectionStateChange(IImConnection conn, int state,
+ ImErrorInfo error) {
+ if(Log.isLoggable(LOG_TAG, Log.DEBUG)){
+ log("onConnectionStateChange(" + state + ", " + error + ")");
+ }
+
+ try {
+ int what = -1;
+ long providerId = conn.getProviderId();
+ switch (state) {
+ case ImConnection.LOGGED_IN:
+ what = EVENT_CONNECTION_LOGGED_IN;
+
+ // Update the active value. We restrict to only one active
+ // account per provider right now, so update all accounts of
+ // this provider to inactive first and then update this
+ // account to active.
+ ContentValues values = new ContentValues(1);
+ values.put(Im.Account.ACTIVE, 0);
+ ContentResolver cr = getContentResolver();
+ cr.update(Im.Account.CONTENT_URI, values,
+ Im.Account.PROVIDER + "=" + providerId, null);
+
+ values.put(Im.Account.ACTIVE, 1);
+ cr.update(ContentUris.withAppendedId(Im.Account.CONTENT_URI, conn.getAccountId()),
+ values, null, null);
+
+ break;
+
+ case ImConnection.LOGGING_IN:
+ what = EVENT_CONNECTION_LOGGING_IN;
+ break;
+
+ case ImConnection.LOGGING_OUT:
+ what = EVENT_CONNECTION_LOGGING_OUT;
+ break;
+
+ case ImConnection.DISCONNECTED:
+ what = EVENT_CONNECTION_DISCONNECTED;
+ synchronized (mConnections) {
+ mConnections.remove(providerId);
+ }
+ // stop the service if there isn't an active connection anymore.
+ stopImServiceIfInactive();
+ break;
+
+ case ImConnection.SUSPENDED:
+ what = EVENT_CONNECTION_SUSPENDED;
+ break;
+ }
+ if (what != -1) {
+ broadcastConnEvent(what, providerId, error);
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "onConnectionStateChange", e);
+ }
+ }
+
+ @Override
+ public void onUpdateSelfPresenceError(IImConnection connection,
+ ImErrorInfo error) {
+ if(Log.isLoggable(LOG_TAG, Log.DEBUG)){
+ log("onUpdateUserPresenceError(" + error + ")");
+ }
+ try {
+ long providerId = connection.getProviderId();
+ broadcastConnEvent(EVENT_UPDATE_USER_PRESENCE_ERROR, providerId,
+ error);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "onUpdateUserPresenceError", e);
+ }
+ }
+
+ @Override
+ public void onSelfPresenceUpdated(IImConnection connection) {
+ if(Log.isLoggable(LOG_TAG, Log.DEBUG)) log("onUserPresenceUpdated");
+
+ try {
+ long providerId = connection.getProviderId();
+ broadcastConnEvent(EVENT_USER_PRESENCE_UPDATED, providerId,
+ null);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "onUserPresenceUpdated", e);
+ }
+ }
+ }
+}
diff --git a/src/com/android/im/app/ImRingtonePreference.java b/src/com/android/im/app/ImRingtonePreference.java
new file mode 100644
index 0000000..9f89c61
--- /dev/null
+++ b/src/com/android/im/app/ImRingtonePreference.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.service.ImServiceConstants;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.preference.RingtonePreference;
+import android.provider.Im;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * RingtonePreference subclass to save/restore ringtone value from ImProvider.
+ */
+public class ImRingtonePreference extends RingtonePreference {
+ private long mProviderId;
+
+ public ImRingtonePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ Intent intent = ((Activity)context).getIntent();
+ mProviderId = intent.getLongExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, -1);
+ if (mProviderId < 0) {
+ Log.e(ImApp.LOG_TAG,"ImRingtonePreference intent requires provider id extra");
+ throw new RuntimeException("ImRingtonePreference must be created with an provider id");
+ }
+ }
+
+ @Override
+ protected Uri onRestoreRingtone() {
+ final Im.ProviderSettings.QueryMap settings = new Im.ProviderSettings.QueryMap(
+ getContext().getContentResolver(), mProviderId,
+ false /* keep updated */, null /* no handler */);
+
+ String uri = settings.getRingtoneURI();
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.VERBOSE)) {
+ Log.v(ImApp.LOG_TAG, "onRestoreRingtone() finds uri=" + uri + " key=" + getKey());
+ }
+
+
+ if (TextUtils.isEmpty(uri)) {
+ return null;
+ }
+
+ Uri result = Uri.parse(uri);
+
+ settings.close();
+
+ return result;
+ }
+
+ @Override
+ protected void onSaveRingtone(Uri ringtoneUri) {
+ final Im.ProviderSettings.QueryMap settings = new Im.ProviderSettings.QueryMap(
+ getContext().getContentResolver(), mProviderId,
+ false /* keep updated */, null /* no handler */);
+
+ // When ringtoneUri is null, that means 'Silent' was chosen
+ settings.setRingtoneURI(ringtoneUri == null ? "" : ringtoneUri.toString());
+ settings.close();
+ }
+}
+
diff --git a/src/com/android/im/app/ImUrlActivity.java b/src/com/android/im/app/ImUrlActivity.java
new file mode 100644
index 0000000..2b87c8d
--- /dev/null
+++ b/src/com/android/im/app/ImUrlActivity.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.IChatSession;
+import com.android.im.IChatSessionManager;
+import com.android.im.IImConnection;
+import com.android.im.engine.ImConnection;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Iterator;
+import java.util.Set;
+
+public class ImUrlActivity extends Activity {
+ private static final String[] ACCOUNT_PROJECTION = {
+ Im.Account._ID,
+ Im.Account.PASSWORD,
+ };
+ private static final int ACCOUNT_ID_COLUMN = 0;
+ private static final int ACCOUNT_PW_COLUMN = 1;
+
+ private String mProviderCategory;
+ private String mToAddress;
+
+ private ImApp mApp;
+ private IImConnection mConn;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ if (Intent.ACTION_SENDTO.equals(intent.getAction())) {
+ resolveIntent(intent);
+
+ if (!isProviderSupported()) {
+ Log.w(ImApp.LOG_TAG, "<ImUrlActivity>Unsuppported provider:" + mProviderCategory);
+ finish();
+ return;
+ }
+
+ if (TextUtils.isEmpty(mToAddress)) {
+ Log.w(ImApp.LOG_TAG, "<ImUrlActivity>Invalid to address:" + mToAddress);
+ finish();
+ return;
+ }
+ mApp = ImApp.getApplication(this);
+ mApp.callWhenServiceConnected(new Handler(), new Runnable(){
+ public void run() {
+ handleIntent();
+ }});
+
+ } else {
+ finish();
+ }
+ }
+
+ void handleIntent() {
+ ContentResolver cr = getContentResolver();
+ String providername = Im.Provider.getProviderNameForCategory(mProviderCategory);
+ long providerId = Im.Provider.getProviderIdForName(cr, providername);
+ mConn= mApp.getConnection(providerId);
+ if (mConn == null) {
+ Cursor c = DatabaseUtils.queryAccountsForProvider(cr, ACCOUNT_PROJECTION, providerId);
+ if (c == null) {
+ addAccount(providerId);
+ } else {
+ long accountId = c.getLong(ACCOUNT_ID_COLUMN);
+ if (c.isNull(ACCOUNT_PW_COLUMN)) {
+ editAccount(accountId);
+ } else {
+ signInAccount(accountId);
+ }
+ }
+ } else {
+ try {
+ int state = mConn.getState();
+ if (state < ImConnection.LOGGED_IN) {
+ signInAccount(mConn.getAccountId());
+ } else if (state == ImConnection.LOGGED_IN
+ || state == ImConnection.SUSPENDED) {
+ openChat();
+ }
+ } catch (RemoteException e) {
+ // Ouch! Service died! We'll just disappear.
+ Log.w("ImUrlActivity", "Connection disappeared!");
+ }
+ }
+ finish();
+ }
+
+ private void addAccount(long providerId) {
+ Intent intent = new Intent(this, AccountActivity.class);
+ intent.setAction(Intent.ACTION_INSERT);
+ intent.setData(ContentUris.withAppendedId(Im.Provider.CONTENT_URI, providerId));
+ intent.putExtra(ImApp.EXTRA_INTENT_SEND_TO_USER, mToAddress);
+ startActivity(intent);
+ }
+
+ private void editAccount(long accountId) {
+ Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
+ Intent intent = new Intent(this, AccountActivity.class);
+ intent.setAction(Intent.ACTION_EDIT);
+ intent.setData(accountUri);
+ intent.putExtra(ImApp.EXTRA_INTENT_SEND_TO_USER, mToAddress);
+ startActivity(intent);
+ }
+
+ private void signInAccount(long accountId) {
+ Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
+ Intent intent = new Intent(this, SigningInActivity.class);
+ intent.setData(accountUri);
+ intent.putExtra(ImApp.EXTRA_INTENT_SEND_TO_USER, mToAddress);
+ startActivity(intent);
+ }
+
+ private void openChat() {
+ try {
+ IChatSessionManager manager = mConn.getChatSessionManager();
+ IChatSession session = manager.getChatSession(mToAddress);
+ if(session == null) {
+ session = manager.createChatSession(mToAddress);
+ }
+
+ Uri data = ContentUris.withAppendedId(Im.Chats.CONTENT_URI,
+ session.getId());
+ Intent i = new Intent(Intent.ACTION_VIEW, data);
+ startActivity(i);
+ } catch (RemoteException e) {
+ // Ouch! Service died! We'll just disappear.
+ Log.w("ImUrlActivity", "Connection disappeared!");
+ }
+ }
+
+ private void resolveIntent(Intent intent) {
+ Set<String> categories = intent.getCategories();
+ if (categories != null) {
+ Iterator<String> iter = categories.iterator();
+ if (iter.hasNext()) {
+ mProviderCategory = iter.next();
+ }
+ }
+ Uri data = intent.getData();
+ mToAddress = data.getSchemeSpecificPart();
+
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log ("mProviderCategory=" + mProviderCategory + ", mToAddress=" + mToAddress);
+ }
+ }
+
+ private boolean isProviderSupported() {
+ return Im.ProviderCategories.AIM.equals(mProviderCategory)
+ || Im.ProviderCategories.MSN.equals(mProviderCategory)
+ || Im.ProviderCategories.YAHOO.equals(mProviderCategory);
+ }
+
+ private static void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "<ImUrlActivity> " + msg);
+ }
+}
diff --git a/src/com/android/im/app/ImageListAdapter.java b/src/com/android/im/app/ImageListAdapter.java
new file mode 100644
index 0000000..5ac77ee
--- /dev/null
+++ b/src/com/android/im/app/ImageListAdapter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import com.android.im.R;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * A general image list adapter.
+ */
+public class ImageListAdapter extends BaseAdapter implements ListAdapter {
+
+ public static interface ImageListItem {
+ public Drawable getDrawable();
+
+ public CharSequence getText();
+ }
+
+ private final LayoutInflater mInflater;
+ private final List<? extends ImageListItem> mData;
+ private final int mItemViewId;
+ private final int mImageId;
+ private final int mTextId;
+ private final boolean mAreAllItemsSelectable;
+ private final int mSeparatorId;
+
+ public ImageListAdapter(Context context, List<? extends ImageListItem> data) {
+ this(context, data, R.layout.imglist_item, R.id.image, R.id.text, R.id.separator);
+ }
+
+ public ImageListAdapter(Context context, List<? extends ImageListItem> data,
+ int itemViewId, int imgId, int textId, int separatorId) {
+ mData = data;
+ mItemViewId = itemViewId;
+ mImageId = imgId;
+ mTextId = textId;
+ mAreAllItemsSelectable = !data.contains(null);
+ mSeparatorId = separatorId;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ public int getCount() {
+ return mData.size();
+ }
+
+ public Object getItem(int position) {
+ return mData.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v;
+ if (convertView == null) {
+ v = mInflater.inflate(mItemViewId, parent, false);
+ } else {
+ v = convertView;
+ }
+ setupView(position, v);
+ return v;
+ }
+
+ private void setupView(int position, View view) {
+ ImageView iv = (ImageView) view.findViewById(mImageId);
+ TextView tv = (TextView)view.findViewById(mTextId);
+ View separator = view.findViewById(mSeparatorId);
+
+ if (!isEnabled(position)) {
+ if (iv != null) {
+ iv.setVisibility(View.GONE);
+ }
+ if (tv != null) {
+ tv.setVisibility(View.GONE);
+ }
+ if (separator != null) {
+ separator.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (separator != null) {
+ separator.setVisibility(View.GONE);
+ }
+ final ImageListItem item = mData.get(position);
+ if (iv != null) {
+ iv.setVisibility(View.VISIBLE);
+ iv.setImageDrawable(item.getDrawable());
+ }
+ if (tv != null) {
+ tv.setVisibility(View.VISIBLE);
+ tv.setText(item.getText());
+ }
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return mAreAllItemsSelectable;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return mData.get(position) != null;
+ }
+
+}
diff --git a/src/com/android/im/app/ImpsAddressUtils.java b/src/com/android/im/app/ImpsAddressUtils.java
new file mode 100644
index 0000000..c47b1ff
--- /dev/null
+++ b/src/com/android/im/app/ImpsAddressUtils.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+public class ImpsAddressUtils {
+
+ public static String getDisplayableAddress(String impsAddress) {
+ if (impsAddress.startsWith("wv:")) {
+ return impsAddress.substring(3);
+ }
+ return impsAddress;
+ }
+}
diff --git a/src/com/android/im/app/IntTrie.java b/src/com/android/im/app/IntTrie.java
new file mode 100644
index 0000000..cec56ba
--- /dev/null
+++ b/src/com/android/im/app/IntTrie.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+public class IntTrie {
+ private Node mHead;
+
+ class Node {
+ private Node mFirstChild;
+ private Node mNextSibling;
+ private char mKey;
+ int mValue;
+
+ public final void add(String key, int value) {
+ final int len = key.length();
+ Node n = this;
+ int index = 0;
+
+ while (index < len) {
+ n = n.getOrCreateNode(key.charAt(index++));
+ }
+
+ n.mValue = value;
+ }
+
+ private Node getOrCreateNode(char key) {
+ for (Node n = mFirstChild; n != null; n = n.mNextSibling) {
+ if (n.mKey == key) {
+ return n;
+ }
+ }
+
+ Node n = new Node();
+
+ n.mKey = key;
+ n.mNextSibling = mFirstChild;
+ mFirstChild = n;
+
+ return n;
+ }
+
+ Node getNode(char key) {
+ for (Node n = mFirstChild; n != null; n = n.mNextSibling) {
+ if (n.mKey == key) {
+ return n;
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public IntTrie(String[] dictionary, int[] values) {
+ final int len = dictionary.length;
+
+ if (len != values.length) {
+ throw new IllegalArgumentException("dictionary[] and values[] must be the same length");
+ }
+
+ mHead = new Node();
+
+ for (int i = 0; i < len; i++) {
+ mHead.add(dictionary[i], values[i]);
+ }
+ }
+
+ public Node getNode(char key) {
+ return mHead.getNode(key);
+ }
+}
diff --git a/src/com/android/im/app/Markup.java b/src/com/android/im/app/Markup.java
new file mode 100644
index 0000000..b4f07b0
--- /dev/null
+++ b/src/com/android/im/app/Markup.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.plugin.BrandingResourceIDs;
+
+import android.graphics.drawable.Drawable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ImageSpan;
+import android.text.util.Linkify;
+
+public class Markup {
+ private BrandingResources mRes;
+ private IntTrie mSmileys;
+
+ public Markup(BrandingResources res) {
+ mRes = res;
+ mSmileys = new IntTrie(
+ res.getStringArray(BrandingResourceIDs.STRING_ARRAY_SMILEY_TEXTS), res.getSmileyIcons());
+ }
+
+ public final CharSequence markup(CharSequence text) {
+ SpannableString result;
+
+ if (text instanceof SpannableString) {
+ result = (SpannableString) text;
+ } else {
+ result = new SpannableString(text);
+ }
+
+ Linkify.addLinks(result, Linkify.ALL);
+ applyEmoticons(result);
+
+ return result;
+ }
+
+ public final CharSequence applyEmoticons(CharSequence text) {
+ int offset = 0;
+ final int len = text.length();
+ SpannableString result = null;
+
+ while (offset < len) {
+ int index = offset;
+ IntTrie.Node n = mSmileys.getNode(text.charAt(index++));
+ int candidate = 0;
+ int lastMatchEnd = -1;
+
+ // Search the trie until we stop matching
+ while (n != null) {
+ // Record the value and position of the longest match
+ if (n.mValue != 0) {
+ candidate = n.mValue;
+ lastMatchEnd = index;
+ }
+
+ // Let's not run off the end of the input
+ if (index >= len) {
+ break;
+ }
+
+ n = n.getNode(text.charAt(index++));
+ }
+
+ // If we matched a smiley, apply its image over the text
+ if (candidate != 0) {
+ // Lazy-convert the result text to a SpannableString if we have to
+ if (result == null) {
+ if (text instanceof SpannableString) {
+ result = (SpannableString) text;
+ } else {
+ result = new SpannableString(text);
+ text = result;
+ }
+ }
+ Drawable smiley = mRes.getSmileyIcon(candidate);
+ smiley.setBounds(0, 0, smiley.getIntrinsicWidth(), smiley.getIntrinsicHeight());
+ result.setSpan(new ImageSpan(smiley),
+ offset, lastMatchEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ candidate = 0;
+ }
+
+ // if there was a match, start searching for the next one after it
+ // if no match, start at the next character
+ if (lastMatchEnd != -1) {
+ offset = lastMatchEnd;
+ lastMatchEnd = -1;
+ } else {
+ offset++;
+ }
+ }
+
+ // If there were no modifications, return the original string
+ if (result == null) {
+ return text;
+ }
+
+ return result;
+ }
+}
diff --git a/src/com/android/im/app/MessageView.java b/src/com/android/im/app/MessageView.java
new file mode 100644
index 0000000..9a42cbb
--- /dev/null
+++ b/src/com/android/im/app/MessageView.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.provider.Im;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+import android.text.style.URLSpan;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.im.R;
+
+public class MessageView extends LinearLayout {
+
+ private TextView mMessage;
+
+ private Resources mResources;
+
+ public MessageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mMessage = (TextView) findViewById(R.id.message);
+
+ mResources = getResources();
+ }
+
+ public URLSpan[] getMessageLinks() {
+ return mMessage.getUrls();
+ }
+
+ public void bindIncomingMessage(String contact, String body, Date date,
+ Markup smileyRes, boolean scrolling) {
+ CharSequence message = formatMessage(contact, body, date, smileyRes, scrolling);
+ mMessage.setText(message);
+ mMessage.setTextColor(mResources.getColor(R.color.chat_msg));
+ }
+
+ public void bindOutgoingMessage(String body, Date date, Markup smileyRes, boolean scrolling) {
+ String contact = mResources.getString(R.string.me);
+ CharSequence message = formatMessage(contact, body, date, smileyRes, scrolling);
+ mMessage.setText(message);
+ mMessage.setTextColor(mResources.getColor(R.color.chat_msg));
+ }
+
+ public void bindPresenceMessage(String contact, int type, boolean isGroupChat,
+ boolean scrolling) {
+ CharSequence message = formatPresenceUpdates(contact, type, isGroupChat, scrolling);
+ mMessage.setText(message);
+ mMessage.setTextColor(mResources.getColor(R.color.chat_msg_presence));
+ }
+
+ public void bindErrorMessage(int errCode) {
+ mMessage.setText(R.string.msg_sent_failed);
+ mMessage.setTextColor(mResources.getColor(R.color.error));
+ }
+
+ private CharSequence formatMessage(String contact, String body,
+ Date date, Markup smileyRes, boolean scrolling) {
+ if (body.indexOf('\r') != -1) {
+ // first convert \r\n pair to \n, then single \r to \n.
+ // here we can't use HideReturnsTransformationMethod because
+ // it does only 1 to 1 transformation and is unable to handle
+ // the "\r\n" case.
+ body = body.replace("\r\n", "\n").replace('\r', '\n');
+ }
+
+ SpannableStringBuilder buf = new SpannableStringBuilder(contact);
+ buf.append(": ");
+ if (scrolling) {
+ buf.append(body);
+ } else {
+ buf.setSpan(ChatView.STYLE_BOLD, 0, buf.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ buf.append(smileyRes.markup(body));
+
+ if (date != null) {
+ appendTimeStamp(buf, date);
+ }
+ }
+ return buf;
+ }
+
+ private void appendTimeStamp(SpannableStringBuilder buf, Date date) {
+ DateFormat format = new SimpleDateFormat(mResources.getString(R.string.time_stamp));
+ String dateStr = format.format(date);
+ SpannableString spanText = new SpannableString(dateStr);
+ int len = spanText.length();
+ spanText.setSpan(new StyleSpan(Typeface.ITALIC),
+ 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spanText.setSpan(new RelativeSizeSpan(0.8f),
+ 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spanText.setSpan(new ForegroundColorSpan(
+ mResources.getColor(android.R.color.darker_gray)),
+ 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ buf.append('\n');
+ buf.append(spanText);
+ }
+
+ private CharSequence formatPresenceUpdates(String contact, int type,
+ boolean isGroupChat, boolean scrolling) {
+ String body;
+ switch (type) {
+ case Im.MessageType.PRESENCE_AVAILABLE:
+ body = mResources.getString(isGroupChat ? R.string.contact_joined
+ : R.string.contact_online, contact);
+ break;
+
+ case Im.MessageType.PRESENCE_AWAY:
+ body = mResources.getString(R.string.contact_away, contact);
+ break;
+
+ case Im.MessageType.PRESENCE_DND:
+ body = mResources.getString(R.string.contact_busy, contact);
+ break;
+
+ case Im.MessageType.PRESENCE_UNAVAILABLE:
+ body = mResources.getString(isGroupChat ? R.string.contact_left
+ : R.string.contact_offline, contact);
+ break;
+
+ default:
+ return null;
+ }
+
+ if (scrolling) {
+ return body;
+ } else {
+ SpannableString spanText = new SpannableString(body);
+ int len = spanText.length();
+ spanText.setSpan(new StyleSpan(Typeface.ITALIC),
+ 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spanText.setSpan(new RelativeSizeSpan((float)0.8),
+ 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return spanText;
+ }
+ }
+}
diff --git a/src/com/android/im/app/NewChatActivity.java b/src/com/android/im/app/NewChatActivity.java
new file mode 100644
index 0000000..a9c95e7
--- /dev/null
+++ b/src/com/android/im/app/NewChatActivity.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.IChatSession;
+import com.android.im.R;
+import com.android.im.app.adapter.ChatListenerAdapter;
+import com.android.im.plugin.BrandingResourceIDs;
+import com.android.im.service.ImServiceConstants;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.ImageView;
+import android.widget.SimpleAdapter;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class NewChatActivity extends Activity {
+ private static final String[] CHAT_SWITCHER_PROJECTION = {
+ Im.Contacts._ID,
+ Im.Contacts.PROVIDER,
+ Im.Contacts.ACCOUNT,
+ Im.Contacts.USERNAME,
+ Im.Chats.GROUP_CHAT,
+ };
+
+ private static final int CHAT_SWITCHER_ID_COLUMN = 0;
+
+ private static final int REQUEST_PICK_CONTACTS = RESULT_FIRST_USER + 1;
+
+ ImApp mApp;
+
+ ChatView mChatView;
+ SimpleAlertHandler mHandler;
+
+ private AlertDialog mSmileyDialog;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ setContentView(R.layout.chat_view);
+
+ mChatView = (ChatView) findViewById(R.id.chatView);
+ mHandler = mChatView.mHandler;
+
+ final Handler handler = new Handler();
+ mApp= ImApp.getApplication(this);
+ mApp.callWhenServiceConnected(handler, new Runnable() {
+ public void run() {
+ resolveIntent(getIntent());
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mChatView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ mChatView.onPause();
+ super.onPause();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ resolveIntent(intent);
+ }
+
+ void resolveIntent(Intent intent) {
+ if (ImServiceConstants.ACTION_MANAGE_SUBSCRIPTION.equals(intent.getAction())) {
+ long providerId = intent.getLongExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, -1);
+ String from = intent.getStringExtra(ImServiceConstants.EXTRA_INTENT_FROM_ADDRESS);
+ if ((providerId == -1) || (from == null)) {
+ finish();
+ } else {
+ mChatView.bindSubscription(providerId, from);
+ }
+ } else {
+ Uri data = intent.getData();
+ String type = getContentResolver().getType(data);
+ if (Im.Chats.CONTENT_ITEM_TYPE.equals(type)) {
+ mChatView.bindChat(ContentUris.parseId(data));
+ } else if (Im.Invitation.CONTENT_ITEM_TYPE.equals(type)) {
+ mChatView.bindInvitation(ContentUris.parseId(data));
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.chat_screen_menu, menu);
+
+ long providerId = mChatView.getProviderId();
+ BrandingResources brandingRes = mApp.getBrandingResource(providerId);
+ menu.findItem(R.id.menu_view_friend_list).setTitle(
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_CONTACT_LIST));
+ menu.findItem(R.id.menu_switch_chats).setTitle(
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_SWITCH_CHATS));
+ menu.findItem(R.id.menu_insert_smiley).setTitle(
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_INSERT_SMILEY));
+ menu.findItem(R.id.menu_end_conversation).setTitle(
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_END_CHAT));
+ menu.findItem(R.id.menu_view_profile).setTitle(
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_VIEW_PROFILE));
+ menu.findItem(R.id.menu_block_contact).setTitle(
+ brandingRes.getString(BrandingResourceIDs.STRING_MENU_BLOCK_CONTACT));
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ //XXX hide the invite menu, group chat is not supported by the server.
+ menu.findItem(R.id.menu_invite_contact).setVisible(false);
+
+ //XXX HACK: Yahoo! doesn't allow to block a friend. We can only block a temporary contact.
+ ProviderDef provider = mApp.getProvider(mChatView.getProviderId());
+ if ((provider != null) && Im.ProviderNames.YAHOO.equals(provider.mName)) {
+ if (Im.Contacts.TYPE_TEMPORARY != mChatView.mType) {
+ menu.findItem(R.id.menu_block_contact).setVisible(false);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_view_friend_list:
+ finish();
+ showRosterScreen();
+ return true;
+
+ case R.id.menu_insert_smiley:
+ showSmileyDialog();
+ return true;
+
+ case R.id.menu_end_conversation:
+ mChatView.closeChatSession();
+ return true;
+
+ case R.id.menu_switch_chats:
+ Dashboard.openDashboard(this, mChatView.getAccountId(),
+ mChatView.getUserName());
+ return true;
+
+ case R.id.menu_invite_contact:
+ startContactPicker();
+ return true;
+
+ case R.id.menu_view_profile:
+ mChatView.viewProfile();
+ return true;
+
+ case R.id.menu_block_contact:
+ mChatView.blockContact();
+ return true;
+
+ case R.id.menu_prev_chat:
+ switchChat(-1);
+ return true;
+
+ case R.id.menu_next_chat:
+ switchChat(1);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void showRosterScreen() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClass(this, ContactListActivity.class);
+ intent.putExtra(ImServiceConstants.EXTRA_INTENT_ACCOUNT_ID, mChatView.getAccountId());
+ startActivity(intent);
+ }
+
+ private void showSmileyDialog() {
+ if (mSmileyDialog == null) {
+ long providerId = mChatView.getProviderId();
+
+ final BrandingResources brandingRes = mApp.getBrandingResource(providerId);
+ int[] icons = brandingRes.getSmileyIcons();
+ String[] names = brandingRes.getStringArray(
+ BrandingResourceIDs.STRING_ARRAY_SMILEY_NAMES);
+ final String[] texts = brandingRes.getStringArray(
+ BrandingResourceIDs.STRING_ARRAY_SMILEY_TEXTS);
+
+ final int N = names.length;
+
+ List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>();
+ for (int i = 0; i < N; i++) {
+ // We might have different ASCII for the same icon, skip it if
+ // the icon is already added.
+ boolean added = false;
+ for (int j = 0; j < i; j++) {
+ if (icons[i] == icons[j]) {
+ added = true;
+ break;
+ }
+ }
+ if (!added) {
+ HashMap<String, Object> entry = new HashMap<String, Object>();
+
+ entry. put("icon", icons[i]);
+ entry. put("name", names[i]);
+ entry.put("text", texts[i]);
+
+ entries.add(entry);
+ }
+ }
+
+ final SimpleAdapter a = new SimpleAdapter(
+ this,
+ entries,
+ R.layout.smiley_menu_item,
+ new String[] {"icon", "name", "text"},
+ new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text});
+ SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
+ public boolean setViewValue(View view, Object data, String textRepresentation) {
+ if (view instanceof ImageView) {
+ Drawable img = brandingRes.getSmileyIcon((Integer)data);
+ ((ImageView)view).setImageDrawable(img);
+ return true;
+ }
+ return false;
+ }
+ };
+ a.setViewBinder(viewBinder);
+
+ AlertDialog.Builder b = new AlertDialog.Builder(this);
+
+ b.setTitle(brandingRes.getString(
+ BrandingResourceIDs.STRING_MENU_INSERT_SMILEY));
+
+ b.setCancelable(true);
+ b.setAdapter(a, new DialogInterface.OnClickListener() {
+ public final void onClick(DialogInterface dialog, int which) {
+ HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which);
+ mChatView.insertSmiley((String)item.get("text"));
+ }
+ });
+
+ mSmileyDialog = b.create();
+ }
+
+ mSmileyDialog.show();
+ }
+
+ private void switchChat(int delta) {
+ Cursor c = getContentResolver().query(Im.Contacts.CONTENT_URI_CHAT_CONTACTS,
+ CHAT_SWITCHER_PROJECTION, null, null, null);
+ if(c == null) {
+ return;
+ }
+
+ final int N = c.getCount();
+ if (N <= 1) {
+ c.close();
+ return;
+ }
+
+ int current = -1;
+ // find current position
+ for (int i = 0; i < N; i++) {
+ c.moveToNext();
+ long id = c.getLong(CHAT_SWITCHER_ID_COLUMN);
+ if (id == mChatView.getChatId()) {
+ current = i;
+ }
+ }
+ if (current == -1) {
+ c.close();
+ return;
+ }
+
+ int newPosition = (current + delta) % N;
+ if (newPosition < 0) {
+ newPosition += N;
+ }
+
+ c.moveToPosition(newPosition);
+
+ Intent intent;
+ long id = c.getLong(CHAT_SWITCHER_ID_COLUMN);
+ Uri uri = ContentUris.withAppendedId(Im.Chats.CONTENT_URI, id);
+ intent = new Intent(Intent.ACTION_VIEW, uri);
+
+ c.close();
+ startActivity(intent);
+ finish();
+ }
+
+ private void startContactPicker() {
+ Uri.Builder builder = Im.Contacts.CONTENT_URI_ONLINE_CONTACTS_BY.buildUpon();
+ ContentUris.appendId(builder, mChatView.getProviderId());
+ ContentUris.appendId(builder, mChatView.getAccountId());
+ Uri data = builder.build();
+
+ try {
+ Intent i = new Intent(Intent.ACTION_PICK, data);
+ i.putExtra(ContactsPickerActivity.EXTRA_EXCLUDED_CONTACTS,
+ mChatView.getCurrentChatSession().getPariticipants());
+ startActivityForResult(i, REQUEST_PICK_CONTACTS);
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent data) {
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_PICK_CONTACTS) {
+ String username = data.getStringExtra(
+ ContactsPickerActivity.EXTRA_RESULT_USERNAME);
+ try {
+ IChatSession chatSession = mChatView.getCurrentChatSession();
+ if (chatSession.isGroupChatSession()) {
+ chatSession.inviteContact(username);
+ showInvitationHasSent(username);
+ } else {
+ chatSession.convertToGroupChat();
+ new ContactInvitor(chatSession, username).start();
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+ }
+ }
+
+ void showInvitationHasSent(String contact) {
+ Toast.makeText(NewChatActivity.this,
+ getString(R.string.invitation_sent_prompt, contact),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ private class ContactInvitor extends ChatListenerAdapter {
+ private final IChatSession mChatSession;
+ String mContact;
+
+ public ContactInvitor(IChatSession session, String data) {
+ mChatSession = session;
+ mContact = data;
+ }
+
+ @Override
+ public void onConvertedToGroupChat(IChatSession ses) {
+ try {
+ final long chatId = mChatSession.getId();
+ mChatSession.inviteContact(mContact);
+ mHandler.post(new Runnable(){
+ public void run() {
+ mChatView.bindChat(chatId);
+ showInvitationHasSent(mContact);
+ }
+ });
+ mChatSession.unregisterChatListener(this);
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+
+ public void start() throws RemoteException {
+ mChatSession.registerChatListener(this);
+ }
+ }
+}
diff --git a/src/com/android/im/app/PreferenceActivity.java b/src/com/android/im/app/PreferenceActivity.java
new file mode 100644
index 0000000..99e3f6e
--- /dev/null
+++ b/src/com/android/im/app/PreferenceActivity.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import java.util.HashMap;
+
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.Im;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+
+import com.android.im.R;
+import com.android.im.imps.ImpsConnectionConfig.CirMethod;
+import com.android.im.imps.ImpsConnectionConfig.EncodingType;
+import com.android.im.imps.ImpsConnectionConfig.TransportType;
+import com.android.im.plugin.ImConfigNames;
+import com.android.im.plugin.ImpsConfigNames;
+
+public class PreferenceActivity extends Activity {
+
+ RadioGroup mRgDataChannel;
+ RadioGroup mRgDataEncoding;
+ RadioGroup mRgCirChannel;
+
+ EditText mEdtHost;
+ EditText mEdtMsisdn;
+
+ long mProviderId;
+ String mProviderName;
+ HashMap<String, String> mPref;
+
+ static final void log(String log) {
+ Log.d(ImApp.LOG_TAG, "<PreferenceActivity> " + log);
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ resolveIntent();
+ setTitle(getString(R.string.preference_title, mProviderName));
+ setContentView(R.layout.preference_activity);
+
+ mRgDataChannel = (RadioGroup) findViewById(R.id.rgDataChannel);
+ mRgDataEncoding = (RadioGroup) findViewById(R.id.rgDataEncoding);
+ mRgCirChannel = (RadioGroup) findViewById(R.id.rgCirChannel);
+ mEdtHost = (EditText) findViewById(R.id.etHost);
+ mEdtMsisdn = (EditText) findViewById(R.id.etMsisdn);
+
+ String dataChannel = getPreference(ImpsConfigNames.DATA_CHANNEL,
+ TransportType.HTTP.name());
+ if (TransportType.HTTP.name().equals(dataChannel)) {
+ mRgDataChannel.check(R.id.DATA_HTTP);
+ } else if (TransportType.SMS.name().equals(dataChannel)) {
+ mRgDataChannel.check(R.id.DATA_SMS);
+ }
+
+ String cirChannel = getPreference(ImpsConfigNames.CIR_CHANNEL,
+ CirMethod.STCP.name());
+ if (CirMethod.STCP.name().equals(cirChannel)) {
+ mRgCirChannel.check(R.id.CIR_STCP);
+ } else if (CirMethod.SHTTP.name().equals(cirChannel)) {
+ mRgCirChannel.check(R.id.CIR_SHTTP);
+ } else if (CirMethod.SSMS.name().equals(cirChannel)) {
+ mRgCirChannel.check(R.id.CIR_SSMS);
+ }
+
+ String dataEncoding = getPreference(ImpsConfigNames.DATA_ENCODING,
+ EncodingType.XML.name());
+ if (EncodingType.XML.name().equals(dataEncoding)) {
+ mRgDataEncoding.check(R.id.ENC_XML);
+ } else if (EncodingType.WBXML.name().equals(dataEncoding)) {
+ mRgDataEncoding.check(R.id.ENC_WBXML);
+ } else if (EncodingType.SMS.name().equals(dataEncoding)) {
+ mRgDataEncoding.check(R.id.ENC_SMS);
+ }
+
+ mEdtHost.setText(getPreference(ImpsConfigNames.HOST, "http://"));
+ mEdtMsisdn.setText(getPreference(ImpsConfigNames.MSISDN, ""));
+
+ final Button btnSave = (Button) findViewById(R.id.btnSave);
+ btnSave.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ savePreferences();
+ }
+ });
+ }
+
+ private String getPreference(String prefName, String defaultValue) {
+ String value = mPref.get(prefName);
+
+ return value == null ? defaultValue : value;
+ }
+
+ void resolveIntent() {
+ Intent i = getIntent();
+ if(i.getData() == null){
+ Log.w(ImApp.LOG_TAG, "No data passed to PreferenceActivity");
+ finish();
+ } else {
+ Cursor c = getContentResolver().query(i.getData(),
+ new String[]{Im.Provider._ID, Im.Provider.NAME}, null, null, null);
+ if (c == null || !c.moveToFirst()) {
+ Log.w(ImApp.LOG_TAG, "Can't query data from given URI.");
+ finish();
+ } else {
+ mProviderId = c.getLong(0);
+ mProviderName = c.getString(1);
+
+ c.close();
+
+ mPref = Im.ProviderSettings.queryProviderSettings(getContentResolver(), mProviderId);
+ }
+ }
+ }
+
+ void savePreferences() {
+ TransportType dataChannel;
+ switch (mRgDataChannel.getCheckedRadioButtonId()) {
+ case R.id.DATA_HTTP:
+ dataChannel = TransportType.HTTP;
+ break;
+ case R.id.DATA_SMS:
+ dataChannel = TransportType.SMS;
+ break;
+ default:
+ Log.w(ImApp.LOG_TAG, "Unexpected dataChannel button ID; defaulting to HTTP");
+ dataChannel = TransportType.HTTP;
+ break;
+ }
+
+ CirMethod cirChannel;
+ switch (mRgCirChannel.getCheckedRadioButtonId()) {
+ case R.id.CIR_STCP:
+ cirChannel = CirMethod.STCP;
+ break;
+ case R.id.CIR_SHTTP:
+ cirChannel = CirMethod.SHTTP;
+ break;
+ case R.id.CIR_SSMS:
+ cirChannel = CirMethod.SSMS;
+ break;
+ default:
+ Log.w(ImApp.LOG_TAG, "Unexpected cirChannel button ID; defaulting to STCP");
+ cirChannel = CirMethod.STCP;
+ break;
+ }
+
+ EncodingType dataEncoding;
+ switch (mRgDataEncoding.getCheckedRadioButtonId()) {
+ case R.id.ENC_WBXML:
+ dataEncoding = EncodingType.WBXML;
+ break;
+ case R.id.ENC_XML:
+ dataEncoding = EncodingType.XML;
+ break;
+ case R.id.ENC_SMS:
+ dataEncoding = EncodingType.SMS;
+ break;
+ default:
+ Log.w(ImApp.LOG_TAG, "Unexpected dataEncoding button ID; defaulting to WBXML");
+ dataEncoding = EncodingType.WBXML;
+ break;
+ }
+
+ String host = mEdtHost.getText().toString();
+ String msisdn = mEdtMsisdn.getText().toString();
+
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
+ log("set connection preference, DataChannel: " + dataChannel
+ + ", CirChannel: " + cirChannel
+ + ", DataEncoding: " + dataEncoding
+ + ", Host: " + host
+ + ", MSISDN: " + msisdn);
+ }
+ ContentValues[] valuesList = new ContentValues[7];
+ valuesList[0] = getValues(ImConfigNames.PROTOCOL_NAME, "IMPS");
+ valuesList[1] = getValues(ImpsConfigNames.DATA_CHANNEL, dataChannel.name());
+ valuesList[2] = getValues(ImpsConfigNames.DATA_ENCODING, dataEncoding.name());
+ valuesList[3] = getValues(ImpsConfigNames.CIR_CHANNEL, cirChannel.name());
+ valuesList[4] = getValues(ImpsConfigNames.HOST, host);
+ valuesList[6] = getValues(ImpsConfigNames.MSISDN, msisdn);
+
+ getContentResolver().bulkInsert(Im.ProviderSettings.CONTENT_URI, valuesList);
+
+ finish();
+ }
+
+ private ContentValues getValues(String name, String value) {
+ ContentValues values = new ContentValues();
+ values.put(Im.ProviderSettings.PROVIDER, mProviderId);
+ values.put(Im.ProviderSettings.NAME, name);
+ values.put(Im.ProviderSettings.VALUE, value);
+
+ return values;
+ }
+}
diff --git a/src/com/android/im/app/PresenceUtils.java b/src/com/android/im/app/PresenceUtils.java
new file mode 100644
index 0000000..dcc806c
--- /dev/null
+++ b/src/com/android/im/app/PresenceUtils.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.provider.Im;
+import android.util.Log;
+
+import com.android.im.engine.Presence;
+import com.android.im.plugin.BrandingResourceIDs;
+
+public final class PresenceUtils {
+ private PresenceUtils() {}
+
+ public static int convertStatus(int status) {
+ switch (status) {
+ case Presence.AVAILABLE:
+ return Im.Presence.AVAILABLE;
+
+ case Presence.AWAY:
+ return Im.Presence.AWAY;
+
+ case Presence.DO_NOT_DISTURB:
+ return Im.Presence.DO_NOT_DISTURB;
+
+ case Presence.IDLE:
+ return Im.Presence.IDLE;
+
+ case Presence.OFFLINE:
+ return Im.Presence.OFFLINE;
+
+ default:
+ Log.w(ImApp.LOG_TAG, "[ContactView] Unknown presence status " + status);
+ return Im.Presence.AVAILABLE;
+ }
+ }
+
+ public static int getStatusStringRes(int status) {
+ switch (status) {
+ case Im.Presence.AVAILABLE:
+ return BrandingResourceIDs.STRING_PRESENCE_AVAILABLE;
+
+ case Im.Presence.AWAY:
+ return BrandingResourceIDs.STRING_PRESENCE_AWAY;
+
+ case Im.Presence.DO_NOT_DISTURB:
+ return BrandingResourceIDs.STRING_PRESENCE_BUSY;
+
+ case Im.Presence.IDLE:
+ return BrandingResourceIDs.STRING_PRESENCE_IDLE;
+
+ case Im.Presence.INVISIBLE:
+ return BrandingResourceIDs.STRING_PRESENCE_INVISIBLE;
+
+ case Im.Presence.OFFLINE:
+ return BrandingResourceIDs.STRING_PRESENCE_OFFLINE;
+
+ default:
+ return BrandingResourceIDs.STRING_PRESENCE_AVAILABLE;
+ }
+ }
+
+ public static int getStatusIconId(int status) {
+ switch (status) {
+ case Im.Presence.AVAILABLE:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_ONLINE;
+
+ case Im.Presence.IDLE:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_AWAY;
+
+ case Im.Presence.AWAY:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_AWAY;
+
+ case Im.Presence.DO_NOT_DISTURB:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_BUSY;
+
+ case Im.Presence.INVISIBLE:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_INVISIBLE;
+
+ default:
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_OFFLINE;
+ }
+ }
+
+}
diff --git a/src/com/android/im/app/ProviderDef.java b/src/com/android/im/app/ProviderDef.java
new file mode 100644
index 0000000..00ad6fc
--- /dev/null
+++ b/src/com/android/im/app/ProviderDef.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+public class ProviderDef {
+ public long mId;
+ public String mName;
+ public String mFullName;
+ public String mSignUpUrl;
+
+ public ProviderDef(long id, String name, String fullName, String signUpUrl) {
+ mId = id;
+ mName = name;
+ if (fullName != null) {
+ mFullName = fullName;
+ } else {
+ mFullName = name;
+ }
+ mSignUpUrl = signUpUrl;
+ }
+}
diff --git a/src/com/android/im/app/ProviderListItem.java b/src/com/android/im/app/ProviderListItem.java
new file mode 100644
index 0000000..d7a7a61
--- /dev/null
+++ b/src/com/android/im/app/ProviderListItem.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import android.widget.LinearLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.provider.Im;
+import android.view.View;
+import android.os.RemoteException;
+import com.android.im.engine.Presence;
+import com.android.im.plugin.BrandingResourceIDs;
+import com.android.im.IImConnection;
+import com.android.im.R;
+
+public class ProviderListItem extends LinearLayout {
+ private ImApp mApp;
+ private ChooseAccountActivity mActivity;
+ private ImageView mProviderIcon;
+ private ImageView mStatusIcon;
+ private TextView mLine1;
+ private TextView mLine2;
+ private TextView mChatView;
+ private int mProviderIdColumn;
+ private int mProviderNameColumn;
+ private int mProviderFullnameColumn;
+ private int mActiveAccountIdColumn;
+ private int mActiveAccountUserNameColumn;
+
+ public ProviderListItem(Context context, ChooseAccountActivity activity) {
+ super(context);
+ mActivity = activity;
+ mApp = ImApp.getApplication(activity);
+ }
+
+ public void init(Cursor c) {
+ mProviderIcon = (ImageView) findViewById(R.id.providerIcon);
+ mStatusIcon = (ImageView) findViewById(R.id.statusIcon);
+ mLine1 = (TextView) findViewById(R.id.line1);
+ mLine2 = (TextView) findViewById(R.id.line2);
+ mChatView = (TextView) findViewById(R.id.conversations);
+
+ mProviderIdColumn = c.getColumnIndexOrThrow(Im.Provider._ID);
+ mProviderNameColumn = c.getColumnIndexOrThrow(Im.Provider.NAME);
+ mProviderFullnameColumn = c.getColumnIndexOrThrow(Im.Provider.FULLNAME);
+ mActiveAccountIdColumn = c.getColumnIndexOrThrow(
+ Im.Provider.ACTIVE_ACCOUNT_ID);
+ mActiveAccountUserNameColumn = c.getColumnIndexOrThrow(
+ Im.Provider.ACTIVE_ACCOUNT_USERNAME);
+ }
+
+ public void bindView(Cursor cursor) {
+ Resources r = getResources();
+ ImageView providerIcon = mProviderIcon;
+ ImageView statusIcon = mStatusIcon;
+ TextView line1 = mLine1;
+ TextView line2 = mLine2;
+ TextView chatView = mChatView;
+
+ int providerId = cursor.getInt(mProviderIdColumn);
+ String providerDisplayName = cursor.getString(mProviderFullnameColumn);
+
+ BrandingResources brandingRes = mApp.getBrandingResource(providerId);
+ providerIcon.setImageDrawable(
+ brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_LOGO));
+
+ if (!cursor.isNull(mActiveAccountIdColumn)) {
+ line1.setVisibility(View.VISIBLE);
+ line1.setText(r.getString(R.string.account_title, providerDisplayName));
+ line2.setText(cursor.getString(mActiveAccountUserNameColumn));
+
+ long accountId = cursor.getLong(mActiveAccountIdColumn);
+
+ if (mActivity.isSigningIn(accountId)) {
+ statusIcon.setVisibility(View.GONE);
+ chatView.setVisibility(View.VISIBLE);
+ chatView.setText(R.string.signing_in_wait);
+ } else if (mActivity.isSignedIn(accountId)) {
+ int presenceIconId = getPresenceIconId(accountId);
+ statusIcon.setImageDrawable(
+ brandingRes.getDrawable(presenceIconId));
+ statusIcon.setVisibility(View.VISIBLE);
+ ContentResolver cr = mActivity.getContentResolver();
+ int count = getConversationCount(cr, accountId);
+ if (count > 0) {
+ chatView.setVisibility(View.VISIBLE);
+ if (count == 1) {
+ chatView.setText(R.string.one_conversation);
+ } else {
+ chatView.setText(r.getString(R.string.conversations, count));
+ }
+ } else {
+ chatView.setVisibility(View.GONE);
+ }
+ } else {
+ statusIcon.setVisibility(View.GONE);
+ chatView.setVisibility(View.GONE);
+ }
+ } else {
+ // No active account, show add account
+ line1.setVisibility(View.GONE);
+ statusIcon.setVisibility(View.GONE);
+ chatView.setVisibility(View.GONE);
+
+ line2.setText(providerDisplayName);
+ }
+ }
+
+ private int getConversationCount(ContentResolver cr, long accountId) {
+ try {
+ IImConnection conn = mApp.getConnectionByAccount(accountId);
+ return (conn == null) ? 0 : conn.getChatSessionCount();
+ } catch (RemoteException e) {
+ return 0;
+ }
+ }
+
+ private int getPresenceIconId(long accountId) {
+ try {
+ IImConnection conn = mApp.getConnectionByAccount(accountId);
+ if (conn != null) {
+ Presence p = conn.getUserPresence();
+ if (p != null) {
+ int status = PresenceUtils.convertStatus(p.getStatus());
+ return PresenceUtils.getStatusIconId(status);
+ }
+ }
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_OFFLINE;
+ } catch (RemoteException e) {
+ return BrandingResourceIDs.DRAWABLE_PRESENCE_INVISIBLE;
+ }
+ }
+}
diff --git a/src/com/android/im/app/SettingActivity.java b/src/com/android/im/app/SettingActivity.java
new file mode 100644
index 0000000..1a5f73b
--- /dev/null
+++ b/src/com/android/im/app/SettingActivity.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.preference.CheckBoxPreference;
+import android.provider.Im;
+import android.util.Log;
+
+import com.android.im.R;
+import com.android.im.service.ImServiceConstants;
+
+public class SettingActivity extends android.preference.PreferenceActivity {
+ private static final String KEY_NOTIFICATION_SOUND = "notification-sound";
+ private static final String KEY_NOTIFICATION_VIBRATE = "notification-vibrate";
+ private static final String KEY_ENABLE_NOTIFICATIONS = "enable-notifications";
+ private static final String KEY_HIDE_OFFLINE_CONTACTS = "hide-offline-contacts";
+
+ private long mProviderId;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.preferences);
+ Intent intent = getIntent();
+ mProviderId = intent.getLongExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, -1);
+ if (mProviderId < 0) {
+ Log.e(ImApp.LOG_TAG,"SettingActivity intent requires provider id extra");
+ throw new RuntimeException("SettingActivity must be created with an provider id");
+ }
+ setInitialValues();
+ }
+
+ private void setInitialValues() {
+ Im.ProviderSettings.QueryMap settings = new Im.ProviderSettings.QueryMap(
+ getContentResolver(), mProviderId,
+ false /* keep updated */, null /* no handler */);
+
+ CheckBoxPreference pref = (CheckBoxPreference) findPreference(KEY_HIDE_OFFLINE_CONTACTS);
+ pref.setChecked(settings.getHideOfflineContacts());
+
+ pref = (CheckBoxPreference) findPreference(KEY_ENABLE_NOTIFICATIONS);
+ pref.setChecked(settings.getEnableNotification());
+
+ pref = (CheckBoxPreference) findPreference(KEY_NOTIFICATION_VIBRATE);
+ pref.setChecked(settings.getVibrate());
+
+ pref = (CheckBoxPreference) findPreference(KEY_NOTIFICATION_SOUND);
+ pref.setChecked(settings.getRingtoneURI() != null);
+ settings.close();
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ if (preference instanceof CheckBoxPreference) {
+ final Im.ProviderSettings.QueryMap settings = new Im.ProviderSettings.QueryMap(
+ getContentResolver(), mProviderId,
+ false /* keep updated */, null /* no handler */);
+ String key = preference.getKey();
+ boolean value = ((CheckBoxPreference) preference).isChecked();
+
+ if (key.equals(KEY_HIDE_OFFLINE_CONTACTS)) {
+ settings.setHideOfflineContacts(value);
+ } else if (key.equals(KEY_ENABLE_NOTIFICATIONS)) {
+ settings.setEnableNotification(value);
+ } else if (key.equals(KEY_NOTIFICATION_VIBRATE)) {
+ settings.setVibrate(value);
+ } else if (key.equals(KEY_NOTIFICATION_SOUND)){
+ if (!value) {
+ settings.setRingtoneURI(null);
+ }
+ }
+ settings.close();
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/com/android/im/app/SigningInActivity.java b/src/com/android/im/app/SigningInActivity.java
new file mode 100644
index 0000000..4257eaf
--- /dev/null
+++ b/src/com/android/im/app/SigningInActivity.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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.im.app;
+
+import com.android.im.IChatSession;
+import com.android.im.IChatSessionManager;
+import com.android.im.IConnectionListener;
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.app.adapter.ConnectionListenerAdapter;
+import com.android.im.engine.ImConnection;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.plugin.BrandingResourceIDs;
+import com.android.im.service.ImServiceConstants;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.provider.Im;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.widget.ImageView;
+
+public class SigningInActivity extends Activity {
+ private IImConnection mConn;
+ private IConnectionListener mListener;
+ private SimpleAlertHandler mHandler;
+ private ImApp mApp;
+ private String mToAddress;
+
+ protected static final int ID_CANCEL_SIGNIN = Menu.FIRST + 1;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ //setTheme(android.R.style.Theme_Dialog);
+ getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+ setContentView(R.layout.signing_in_activity);
+ Intent intent = getIntent();
+ mToAddress = intent.getStringExtra(ImApp.EXTRA_INTENT_SEND_TO_USER);
+
+ Uri data = intent.getData();
+ if (data == null) {
+ if(Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("Need account data to sign in");
+ }
+ finish();
+ return;
+ }
+ ContentResolver cr = getContentResolver();
+ Cursor c = cr.query(data, null, null, null, null);
+ if (c == null) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("Query fail:" + data);
+ }
+ finish();
+ return;
+ }
+ if (!c.moveToFirst()) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("No data for " + data);
+ }
+ c.close();
+ finish();
+ return;
+ }
+
+ long providerId = c.getLong(c.getColumnIndexOrThrow(Im.Account.PROVIDER));
+ final long accountId = c.getLong(c.getColumnIndexOrThrow(Im.Account._ID));
+ final String username = c.getString(c.getColumnIndexOrThrow(Im.Account.USERNAME));
+ String pwExtra = intent.getStringExtra(ImApp.EXTRA_INTENT_PASSWORD);
+ final String pw = pwExtra != null ? pwExtra
+ : c.getString(c.getColumnIndexOrThrow(Im.Account.PASSWORD));
+
+ c.close();
+ mApp = ImApp.getApplication(this);
+ final ProviderDef provider = mApp.getProvider(providerId);
+
+ BrandingResources brandingRes = mApp.getBrandingResource(providerId);
+ getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON,
+ brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_LOGO));
+
+ setTitle(getResources().getString(R.string.signing_in_to,
+ provider.mFullName));
+
+ ImageView splash = (ImageView)findViewById(R.id.splashscr);
+ splash.setImageDrawable(brandingRes.getDrawable(
+ BrandingResourceIDs.DRAWABLE_SPLASH_SCREEN));
+
+ mHandler = new SimpleAlertHandler(this);
+ mListener = new MyConnectionListener(mHandler, provider.mName);
+
+ mApp.callWhenServiceConnected(mHandler, new Runnable() {
+ public void run() {
+ signInAccount(provider, accountId, username, pw);
+ }
+ });
+
+ // assume we can sign in successfully.
+ setResult(RESULT_OK);
+ }
+
+ void signInAccount(ProviderDef provider, long accountId,
+ String username, String pw) {
+ try {
+ IImConnection conn = mApp.getConnection(provider.mId);
+ if (conn != null) {
+ mConn = conn;
+ // register listener before get state so that we won't miss
+ // any state change event.
+ conn.registerConnectionListener(mListener);
+ int state = conn.getState();
+ if (state != ImConnection.LOGGING_IN) {
+ // already signed in or failed
+ conn.unregisterConnectionListener(mListener);
+ handleConnectionEvent(provider.mName, state, null);
+ }
+ } else {
+ mConn = mApp.createConnection(provider.mId);
+ mConn.registerConnectionListener(mListener);
+ mConn.login(accountId, username, pw, true);
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ finish();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mApp != null) {
+ mApp.removePendingCall(mHandler);
+ }
+ if (mConn != null) {
+ try {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("unregisterConnectonListener");
+ }
+ mConn.unregisterConnectionListener(mListener);
+ } catch (RemoteException e) {
+ Log.w(ImApp.LOG_TAG, "<SigningInActivity> Connection disappeared!");
+ }
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, ID_CANCEL_SIGNIN, 0, R.string.menu_cancel_signin);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == ID_CANCEL_SIGNIN) {
+ if (mConn != null) {
+ try {
+ if (mConn.getState() == ImConnection.LOGGING_IN) {
+ if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
+ log("Cancelling sign in");
+ }
+ mConn.logout();
+ finish();
+ }
+ } catch (RemoteException e) {
+ Log.w(ImApp.LOG_TAG, "<SigningInActivity> Connection disappeared!");
+ }
+ }
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ void handleConnectionEvent(String serviceName, int state, ImErrorInfo error) {
+ if (isFinishing()) {
+ return;
+ }
+
+ if (state == ImConnection.LOGGED_IN) {
+ // sign in successfully, finish and switch to contact list
+ finish();
+ try {
+ Intent intent;
+ if (mToAddress != null) {
+ IChatSessionManager manager = mConn.getChatSessionManager();
+ IChatSession session = manager.getChatSession(mToAddress);
+ if(session == null) {
+ session = manager.createChatSession(mToAddress);
+ }
+ Uri data = ContentUris.withAppendedId(Im.Chats.CONTENT_URI,
+ session.getId());
+ intent = new Intent(Intent.ACTION_VIEW, data);
+ } else {
+ intent = new Intent(this, ContactListActivity.class);
+ intent.putExtra(ImServiceConstants.EXTRA_INTENT_ACCOUNT_ID,
+ mConn.getAccountId());
+ }
+ startActivity(intent);
+ } catch (RemoteException e) {
+ // Ouch! Service died! We'll just disappear.
+ Log.w(ImApp.LOG_TAG, "<SigningInActivity> Connection disappeared while signing in!");
+ }
+ } else if (state == ImConnection.DISCONNECTED) {
+ // sign in failed
+ Resources r = getResources();
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.error)
+ .setMessage(r.getString(R.string.login_service_failed, serviceName,
+ error == null? "": ErrorResUtils.getErrorRes(r, error.getCode())))
+ .setPositiveButton(R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ })
+ .setCancelable(false)
+ .show();
+ }
+ }
+
+ private static final void log(String msg) {
+ Log.d(ImApp.LOG_TAG, "<SigningInActivity>" + msg);
+ }
+
+ private final class MyConnectionListener extends ConnectionListenerAdapter {
+ private final String mServiceName;
+
+ MyConnectionListener(Handler handler, String serviceName) {
+ super(handler);
+ mServiceName = serviceName;
+ }
+
+ @Override
+ public void onConnectionStateChange(IImConnection connection,
+ int state, ImErrorInfo error) {
+ handleConnectionEvent(mServiceName, state, error);
+ }
+ }
+}
diff --git a/src/com/android/im/app/SimpleAlertHandler.java b/src/com/android/im/app/SimpleAlertHandler.java
new file mode 100644
index 0000000..e677ddf
--- /dev/null
+++ b/src/com/android/im/app/SimpleAlertHandler.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.widget.Toast;
+
+import com.android.im.R;
+import com.android.im.engine.Contact;
+import com.android.im.engine.ContactListListener;
+import com.android.im.engine.ImErrorInfo;
+
+public class SimpleAlertHandler extends Handler {
+
+ Activity mActivity;
+ Resources mRes;
+
+ public SimpleAlertHandler(Activity activity) {
+ mActivity = activity;
+ mRes = mActivity.getResources();
+ }
+
+ protected void promptDisconnectedEvent(Message msg) {
+ long providerId = ((long)msg.arg1 << 32) | msg.arg2;
+ ImApp app = ImApp.getApplication(mActivity);
+ ProviderDef provider = app.getProvider(providerId);
+ ImErrorInfo error = (ImErrorInfo) msg.obj;
+ String promptMsg;
+ if (error != null) {
+ promptMsg = mActivity.getString(R.string.signed_out_prompt_with_error,
+ provider.mName, ErrorResUtils.getErrorRes(mRes, error.getCode()));
+ } else {
+ promptMsg = mActivity.getString(R.string.signed_out_prompt, provider.mName);
+ }
+ Toast.makeText(mActivity, promptMsg, Toast.LENGTH_SHORT).show();
+ }
+
+ public void registerForBroadcastEvents() {
+ ImApp.getApplication(mActivity).registerForBroadcastEvent(
+ ImApp.EVENT_CONNECTION_DISCONNECTED,
+ this);
+ }
+
+ public void unregisterForBroadcastEvents() {
+ ImApp.getApplication(mActivity).unregisterForBroadcastEvent(
+ ImApp.EVENT_CONNECTION_DISCONNECTED,
+ this);
+ }
+
+ public void showAlert(int titleId, int messageId) {
+ showAlert(mRes.getString(titleId), mRes.getString(messageId));
+ }
+
+ public void showAlert(int titleId, CharSequence message) {
+ showAlert(mRes.getString(titleId), message);
+ }
+
+ public void showAlert(CharSequence title, int messageId) {
+ showAlert(title, mRes.getString(messageId));
+ }
+
+ public void showAlert(final CharSequence title, final CharSequence message) {
+ if (Looper.myLooper() == getLooper()) {
+ new AlertDialog.Builder(mActivity)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ } else {
+ post(new Runnable() {
+ public void run() {
+ new AlertDialog.Builder(mActivity)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+ });
+ }
+ }
+
+ public void showServiceErrorAlert() {
+ showAlert(R.string.error, R.string.service_error);
+ }
+
+ public void showContactError(int errorType, ImErrorInfo error,
+ String listName, Contact contact) {
+ int id = 0;
+ switch (errorType) {
+ case ContactListListener.ERROR_LOADING_LIST:
+ id = R.string.load_contact_list_failed;
+ break;
+
+ case ContactListListener.ERROR_CREATING_LIST:
+ id = R.string.add_list_failed;
+ break;
+
+ case ContactListListener.ERROR_BLOCKING_CONTACT:
+ id = R.string.block_contact_failed;
+ break;
+
+ case ContactListListener.ERROR_UNBLOCKING_CONTACT:
+ id = R.string.unblock_contact_failed;
+ break;
+ }
+
+ String errorInfo = ErrorResUtils.getErrorRes(mRes, error.getCode());
+ if (id != 0) {
+ errorInfo = mRes.getText(id) + "\n" + errorInfo;
+ }
+
+ showAlert(R.string.error, errorInfo);
+ }
+
+}
diff --git a/src/com/android/im/app/SimpleInputActivity.java b/src/com/android/im/app/SimpleInputActivity.java
new file mode 100644
index 0000000..84e3bad
--- /dev/null
+++ b/src/com/android/im/app/SimpleInputActivity.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2007 Esmertec AG.
+ * 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 com.android.im.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.android.im.R;
+
+public class SimpleInputActivity extends Activity {
+
+ public static final String EXTRA_TITLE = "title";
+ public static final String EXTRA_PROMPT = "prompt";
+ public static final String EXTRA_DEFAULT_CONTENT = "content";
+ public static final String EXTRA_OK_BUTTON_TEXT = "button_ok";
+
+ TextView mPrompt;
+ EditText mEdit;
+ Button mBtnOk;
+ Button mBtnCancel;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setTheme(android.R.style.Theme_Dialog);
+ setContentView(R.layout.simple_input_activity);
+
+ Bundle extras = getIntent().getExtras();
+
+ CharSequence title = extras.getCharSequence(EXTRA_TITLE);
+ if (title != null) {
+ setTitle(title);
+ } else {
+ setTitle(R.string.default_input_title);
+ }
+
+ CharSequence prompt = extras.getCharSequence(EXTRA_PROMPT);
+ mPrompt = (TextView) findViewById(R.id.prompt);
+ if (prompt != null) {
+ mPrompt.setText(prompt);
+ } else {
+ mPrompt.setVisibility(View.GONE);
+ }
+
+ mEdit = (EditText) findViewById(R.id.edit);
+ CharSequence defaultText = extras.getCharSequence(EXTRA_DEFAULT_CONTENT);
+ if (defaultText != null) {
+ mEdit.setText(defaultText);
+ }
+
+ mBtnOk = (Button) findViewById(R.id.btnOk);
+ mBtnOk.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ setResult(RESULT_OK,
+ (new Intent()).setAction(mEdit.getText().toString()));
+ finish();
+ }
+ });
+ CharSequence okText = extras.getCharSequence(EXTRA_OK_BUTTON_TEXT);
+ if (okText != null) {
+ mBtnOk.setText(okText);
+ }
+
+ mBtnCancel = (Button) findViewById(R.id.btnCancel);
+ mBtnCancel.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ // XXX Hack from GoogleLogin.java. The android:layout_width="fill_parent"
+ // defined in the layout xml doesn't seem to work for LinearLayout.
+ getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+}
diff --git a/src/com/android/im/app/Ticker.java b/src/com/android/im/app/Ticker.java
new file mode 100644
index 0000000..53d1ae1
--- /dev/null
+++ b/src/com/android/im/app/Ticker.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+public class Ticker extends FrameLayout {
+
+ public Ticker(Context context) {
+ super(context);
+ }
+
+ public Ticker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public Ticker(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void measureChild(View child, int parentWidthMeasureSpec,
+ int parentHeightMeasureSpec) {
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ int childWidthMeasureSpec;
+ int childHeightMeasureSpec;
+
+ // Let the child be as wide as it wants, regardless of our bounds
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingLeft
+ + mPaddingRight, lp.width);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+}
diff --git a/src/com/android/im/app/UserPresenceView.java b/src/com/android/im/app/UserPresenceView.java
new file mode 100644
index 0000000..d07a00d
--- /dev/null
+++ b/src/com/android/im/app/UserPresenceView.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app;
+
+import com.android.im.IImConnection;
+import com.android.im.R;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.engine.Presence;
+import com.google.android.collect.Lists;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.provider.Im;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import java.util.List;
+
+public class UserPresenceView extends LinearLayout {
+
+ private ImageButton mStatusDialogButton;
+
+ // views of the popup window
+ EditText mStatusEditor;
+
+ private final SimpleAlertHandler mHandler;
+
+ private IImConnection mConn;
+ private long mProviderId;
+ Presence mPresence;
+
+ private String mLastStatusEditText;
+ final List<StatusItem> mStatusItems = Lists.newArrayList();
+
+ public UserPresenceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mHandler = new SimpleAlertHandler((Activity)context);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mStatusDialogButton = (ImageButton)findViewById(R.id.statusDropDownButton);
+ mStatusEditor = (EditText)findViewById(R.id.statusEdit);
+
+ mStatusDialogButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ showStatusListDialog();
+ }
+ });
+
+ mStatusEditor.setOnKeyListener(new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (KeyEvent.ACTION_DOWN == event.getAction()) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ updateStatusText();
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ mStatusEditor.setOnFocusChangeListener(new View.OnFocusChangeListener(){
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus) {
+ updateStatusText();
+ }
+ }
+ });
+ }
+
+ private void showStatusListDialog() {
+ if (mConn == null) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setAdapter(getStatusAdapter(),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ StatusItem item = mStatusItems.get(which);
+ int oldStatus = mPresence.getStatus();
+ if (item.getStatus() != oldStatus) {
+ updatePresence(item.getStatus(), item.getText().toString());
+ }
+ }
+ });
+ builder.show();
+ }
+
+ private StatusIconAdapter getStatusAdapter() {
+ try {
+ mStatusItems.clear();
+ int[] supportedStatus = mConn.getSupportedPresenceStatus();
+ for (int i = 0; i < supportedStatus.length; i++) {
+ int s = PresenceUtils.convertStatus(supportedStatus[i]);
+ if (s == Im.Presence.OFFLINE) {
+ s = Im.Presence.INVISIBLE;
+ }
+ ImApp app = ImApp.getApplication((Activity)mContext);
+ BrandingResources brandingRes = app.getBrandingResource(mProviderId);
+ Drawable icon = brandingRes.getDrawable(PresenceUtils.getStatusIconId(s));
+ String text = brandingRes.getString(PresenceUtils.getStatusStringRes(s));
+ mStatusItems.add(new StatusItem(supportedStatus[i], icon, text));
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+
+ return new StatusIconAdapter(mContext, mStatusItems);
+ }
+
+ void updateStatusText() {
+ String newStatusText = mStatusEditor.getText().toString();
+ if (TextUtils.isEmpty(newStatusText)) {
+ newStatusText = "";
+ }
+ if (!newStatusText.equals(mLastStatusEditText)) {
+ updatePresence(-1, newStatusText);
+ }
+ }
+
+ public void setConnection(IImConnection conn) {
+ mConn = conn;
+ try {
+ mPresence = conn.getUserPresence();
+ mProviderId = conn.getProviderId();
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ if (mPresence == null) {
+ mPresence = new Presence();
+ }
+ updateView();
+ }
+
+ private void updateView() {
+ ImApp app = ImApp.getApplication((Activity)mContext);
+ BrandingResources brandingRes = app.getBrandingResource(mProviderId);
+ int status = PresenceUtils.convertStatus(mPresence.getStatus());
+ mStatusDialogButton.setImageDrawable(brandingRes.getDrawable(
+ PresenceUtils.getStatusIconId(status)));
+
+ String statusText = mPresence.getStatusText();
+ if (TextUtils.isEmpty(statusText)) {
+ statusText = brandingRes.getString(PresenceUtils.getStatusStringRes(status));
+ }
+ mStatusEditor.setText(statusText);
+ mLastStatusEditText = statusText;
+
+ // Disable the user to edit the custom status text because
+ // the AIM and MSN server don't support it now.
+ ProviderDef provider = app.getProvider(mProviderId);
+ String providerName = provider == null ? null : provider.mName;
+ if (Im.ProviderNames.AIM.equals(providerName)
+ || Im.ProviderNames.MSN.equals(providerName)) {
+ mStatusEditor.setFocusable(false);
+ }
+ }
+
+ void updatePresence(int status, String statusText) {
+ if (mPresence == null) {
+ // We haven't get the connection yet. Don't allow to update presence now.
+ return;
+ }
+
+ Presence newPresence = new Presence(mPresence);
+
+ if (status != -1) {
+ newPresence.setStatus(status);
+ }
+ newPresence.setStatusText(statusText);
+
+ try {
+ int res = mConn.updateUserPresence(newPresence);
+ if (res != ImErrorInfo.NO_ERROR) {
+ mHandler.showAlert(R.string.error,
+ ErrorResUtils.getErrorRes(getResources(), res));
+ } else {
+ mPresence = newPresence;
+ updateView();
+ }
+ } catch (RemoteException e) {
+ mHandler.showServiceErrorAlert();
+ }
+ }
+
+ private static class StatusItem implements ImageListAdapter.ImageListItem {
+ private final int mStatus;
+ private final Drawable mIcon;
+ private final String mText;
+
+ public StatusItem(int status, Drawable icon, String text) {
+ mStatus = status;
+ mIcon = icon;
+ mText = text;
+ }
+
+ public Drawable getDrawable() {
+ return mIcon;
+ }
+
+ public CharSequence getText() {
+ return mText;
+ }
+
+ public int getStatus() {
+ return mStatus;
+ }
+ }
+
+ private static class StatusIconAdapter extends ImageListAdapter {
+ public StatusIconAdapter(Context context, List<StatusItem> data) {
+ super(context, data);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ StatusItem item = (StatusItem)getItem(position);
+ return item.getStatus();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ return view;
+ }
+ }
+}
diff --git a/src/com/android/im/app/adapter/ChatListenerAdapter.java b/src/com/android/im/app/adapter/ChatListenerAdapter.java
new file mode 100644
index 0000000..6f6776f
--- /dev/null
+++ b/src/com/android/im/app/adapter/ChatListenerAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007-2008 Esmertec AG.
+ * Copyright (C) 2007-2008 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.im.app.adapter;
+
+import android.util.Log;
+
+import com.android.im.IChatListener;
+import com.android.im.IChatSession;
+import com.android.im.app.ImApp;
+import com.android.im.engine.Contact;
+import com.android.im.engine.ImErrorInfo;
+import com.android.im.engine.Message;
+
+public class ChatListenerAdapter extends IChatListener.Stub {
+
+ private static final String TAG = ImApp.LOG_TAG;
+
+ public void onContactJoined(IChatSession ses, Contact contact) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onContactJoined(" + ses + ", " + contact + ")");
+ }
+ }
+
+ public void onContactLeft(IChatSession ses, Contact contact) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onContactLeft(" + ses + ", " + contact + ")");
+ }
+ }
+
+ public void onIncomingMessage(IChatSession ses, Message msg) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onIncomingMessage(" + ses + ", " + msg + ")");
+ }
+ }
+
+ public void onSendMessageError(IChatSession ses, Message msg,
+ ImErrorInfo error) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onSendMessageError(" + ses + ", " + msg + ", " + error + ")");
+ }
+ }
+
+ public void onInviteError(IChatSession ses, ImErrorInfo error) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onInviteError(" + ses + ", " + error + ")");
+ }
+ }
+
+ public void onConvertedToGroupChat(IChatSession ses) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConvertedToGroupChat(" + ses + ")");
+ }
+ }
+
+}
diff --git a/src/com/android/im/app/adapter/ChatSessionListenerAdapter.java b/src/com/android/im/app/adapter/ChatSessionListenerAdapter.java
new file mode 100644
index 0000000..8fb4ba8
--- /dev/null
+++ b/src/com/android/im/app/adapter/ChatSessionListenerAdapter.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007 Esmertec AG.
+ * 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 com.android.im.app.adapter;
+
+import android.util.Log;
+
+import com.android.im.IChatSession;
+import com.android.im.IChatSessionListener;
+import com.android.im.app.ImApp;
+import com.android.im.engine.ImErrorInfo;
+
+public class ChatSessionListenerAdapter extends IChatSessionListener.Stub {
+
+ private static final String TAG = ImApp.LOG_TAG;
+
+ public void onChatSessionCreated(IChatSession session) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "notifyChatSessionCreated(" + session + ")");
+ }
+ }
+
+ public void onChatSessionCreateError(String name, ImErrorInfo error) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "notifyChatSessionCreateError(" + name + ", " + error + ")");
+ }
+ }
+
+}
diff --git a/src/com/android/im/app/adapter/ConnectionListenerAdapter.java b/src/com/android/im/app/adapter/ConnectionListenerAdapter.java
new file mode 100644
index 0000000..921e0fb
--- /dev/null
+++ b/src/com/android/im/app/adapter/ConnectionListenerAdapter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2007 Esmertec AG.
+ * 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 com.android.im.app.adapter;
+
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.im.IConnectionListener;
+import com.android.im.IImConnection;
+import com.android.im.app.ImApp;
+import com.android.im.engine.ImErrorInfo;
+
+public class ConnectionListenerAdapter extends IConnectionListener.Stub {
+
+ private static final String TAG = ImApp.LOG_TAG;
+ private Handler mHandler;
+
+ public ConnectionListenerAdapter(Handler handler) {
+ mHandler = handler;
+ }
+
+ public void onConnectionStateChange(IImConnection connection, int state, ImErrorInfo error) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionStateChange(" + state + ", " + error + ")");
+ }
+ }
+
+ public void onUpdateSelfPresenceError(IImConnection connection, ImErrorInfo error) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onUpdateSelfPresenceError(" + error + ")");
+ }
+ }
+
+ public void onSelfPresenceUpdated(IImConnection connection) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onSelfPresenceUpdated()");
+ }
+ }
+
+ final public void onStateChanged(final IImConnection conn,
+ final int state, final ImErrorInfo error) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ onConnectionStateChange(conn, state, error);
+ }
+ });
+ }
+
+ final public void onUpdatePresenceError(final IImConnection conn,
+ final ImErrorInfo error) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ onUpdateSelfPresenceError(conn, error);
+ }
+ });
+ }
+
+ final public void onUserPresenceUpdated(final IImConnection conn) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ onSelfPresenceUpdated(conn);
+ }
+ });
+ }
+}
diff --git a/src/com/android/im/app/adapter/ContactListListenerAdapter.java b/src/com/android/im/app/adapter/ContactListListenerAdapter.java
new file mode 100644
index 0000000..6c8229f
--- /dev/null
+++ b/src/com/android/im/app/adapter/ContactListListenerAdapter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2007 Esmertec AG.
+ * 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 com.android.im.app.adapter;
+
+import android.util.Log;
+
+import com.android.im.IContactList;
+import com.android.im.IContactListListener;
+import com.android.im.app.ImApp;
+import com.android.im.app.SimpleAlertHandler;
+import com.android.im.engine.Contact;
+import com.android.im.engine.ImErrorInfo;
+
+public class ContactListListenerAdapter extends IContactListListener.Stub {
+
+ private static final String TAG = ImApp.LOG_TAG;
+
+ private final SimpleAlertHandler mHandler;
+
+ public ContactListListenerAdapter(SimpleAlertHandler handler) {
+ mHandler = handler;
+ }
+
+ public void onContactChange(int type, IContactList list,
+ Contact contact) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onContactListChanged(" + type + ", " + list + ", "
+ + contact + ")");
+ }
+ }
+
+ public void onAllContactListsLoaded() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAllContactListsLoaded");
+ }
+ }
+
+ public void onContactsPresenceUpdate(Contact[] contacts) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onContactsPresenceUpdate(" + contacts.length + ")");
+ }
+ }
+
+ public void onContactError(int errorType, ImErrorInfo error,
+ String listName, Contact contact) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onContactError(" + errorType + ", " + error + ", "
+ + listName + ", " + contact + ")");
+ }
+ mHandler.showContactError(errorType, error, listName, contact);
+ }
+}