diff options
Diffstat (limited to 'src/com/android/im/app')
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); + } +} |