diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:06:04 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:06:04 -0800 |
commit | eef1589d1f658de1b50a5617c4f5edae0daa0769 (patch) | |
tree | 4ccbde7444d3de98cfae0650d93953fd251aea30 /src/com | |
parent | ef4989eb3bd594f384dd24ac3b2e7e13e180a026 (diff) | |
download | ImProvider-eef1589d1f658de1b50a5617c4f5edae0daa0769.tar.gz |
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/providers/im/BrandingResources.java | 160 | ||||
-rw-r--r-- | src/com/android/providers/im/ImProvider.java | 362 | ||||
-rw-r--r-- | src/com/android/providers/im/LandingPage.java | 689 | ||||
-rw-r--r-- | src/com/android/providers/im/ProviderListItem.java | 185 |
4 files changed, 1304 insertions, 92 deletions
diff --git a/src/com/android/providers/im/BrandingResources.java b/src/com/android/providers/im/BrandingResources.java new file mode 100644 index 0000000..e232a96 --- /dev/null +++ b/src/com/android/providers/im/BrandingResources.java @@ -0,0 +1,160 @@ +/* + * 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.providers.im; + +import android.content.Context; +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.util.Map; + +/** + * The provider specific branding resources. + */ +public class BrandingResources { + private static final String TAG = "IM"; + private static final boolean LOCAL_DEBUG = false; + + private Map<Integer, Integer> mResMapping; + private Resources mPackageRes; + + 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 provider the name of the IM service provider. + * @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, LandingPage.PluginInfo pluginInfo, String provider, + BrandingResources defaultRes) { + String packageName = null; + mDefaultRes = defaultRes; + + try { + mResMapping = pluginInfo.mPlugin.getResourceMapForProvider(provider); + packageName = pluginInfo.mPlugin.getResourcePackageNameForProvider(provider); + } catch (RemoteException e) { + Log.e(TAG, "Failed load the plugin resource map", e); + } + + if (packageName == null) { + packageName = pluginInfo.mPackageName; + } + + PackageManager pm = context.getPackageManager(); + try { + if (LOCAL_DEBUG) log("load resources from " + packageName); + mPackageRes = pm.getResourcesForApplication(packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Can not load resources from " + packageName); + } + } + + /** + * 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 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; + } + + private void log(String msg) { + Log.d(TAG, "[BrandingRes] " + msg); + } +} diff --git a/src/com/android/providers/im/ImProvider.java b/src/com/android/providers/im/ImProvider.java index c3b59a7..636ecc1 100644 --- a/src/com/android/providers/im/ImProvider.java +++ b/src/com/android/providers/im/ImProvider.java @@ -20,7 +20,7 @@ import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; -import android.content.res.XmlResourceParser; +import android.content.ContentResolver; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteConstraintException; @@ -32,12 +32,9 @@ import android.os.ParcelFileDescriptor; import android.provider.Im; import android.text.TextUtils; import android.util.Log; -import com.android.internal.util.XmlUtils; -import org.xmlpull.v1.XmlPullParserException; import java.io.FileNotFoundException; -import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; @@ -73,9 +70,10 @@ public class ImProvider extends ContentProvider { private static final String TABLE_MESSAGES = "messages"; private static final String TABLE_OUTGOING_RMQ_MESSAGES = "outgoingRmqMessages"; private static final String TABLE_LAST_RMQ_ID = "lastrmqid"; + private static final String TABLE_ACCOUNT_STATUS = "accountStatus"; private static final String DATABASE_NAME = "im.db"; - private static final int DATABASE_VERSION = 44; + private static final int DATABASE_VERSION = 45; protected static final int MATCH_PROVIDERS = 1; protected static final int MATCH_PROVIDERS_BY_ID = 2; @@ -132,6 +130,9 @@ public class ImProvider extends ContentProvider { protected static final int MATCH_OUTGOING_RMQ_MESSAGE = 111; protected static final int MATCH_OUTGOING_HIGHEST_RMQ_ID = 112; protected static final int MATCH_LAST_RMQ_ID = 113; + protected static final int MATCH_ACCOUNTS_STATUS = 114; + protected static final int MATCH_ACCOUNT_STATUS = 115; + protected final UriMatcher mUrlMatcher = new UriMatcher(UriMatcher.NO_MATCH); private final String mTransientDbName; @@ -142,7 +143,10 @@ public class ImProvider extends ContentProvider { private static final HashMap<String, String> sBlockedListProjectionMap; private static final String PROVIDER_JOIN_ACCOUNT_TABLE = - "providers LEFT OUTER JOIN accounts ON (providers._id = accounts.provider AND accounts.active = 1)"; + "providers LEFT OUTER JOIN accounts ON " + + "(providers._id = accounts.provider AND accounts.active = 1) " + + "LEFT OUTER JOIN accountStatus ON (accounts._id = accountStatus.account)"; + private static final String CONTACT_JOIN_PRESENCE_TABLE = "contacts LEFT OUTER JOIN presence ON (contacts._id = presence.contact_id)"; @@ -179,6 +183,14 @@ public class ImProvider extends ContentProvider { private final String mDatabaseName; private final int mDatabaseVersion; + private final String[] BACKFILL_PROJECTION = { + Im.Chats._ID, Im.Chats.SHORTCUT, Im.Chats.LAST_MESSAGE_DATE + }; + + private final String[] FIND_SHORTCUT_PROJECTION = { + Im.Chats._ID, Im.Chats.SHORTCUT + }; + private class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) { @@ -194,6 +206,7 @@ public class ImProvider extends ContentProvider { "_id INTEGER PRIMARY KEY," + "name TEXT," + // eg AIM "fullname TEXT," + // eg AOL Instance Messenger + "category TEXT," + // a category used for forming intent "signup_url TEXT" + // web url to visit to create a new account ");"); @@ -210,7 +223,7 @@ public class ImProvider extends ContentProvider { "UNIQUE (provider, username)" + ");"); - createContactsTables(db, null); + createContactsTables(db); db.execSQL("CREATE TABLE " + TABLE_AVATARS + " (" + "_id INTEGER PRIMARY KEY," + @@ -229,7 +242,6 @@ public class ImProvider extends ContentProvider { "value TEXT," + "UNIQUE (provider, name)" + ");"); - loadProviderSettings(db); db.execSQL("create TABLE " + TABLE_OUTGOING_RMQ_MESSAGES + " (" + "_id INTEGER PRIMARY KEY," + @@ -260,62 +272,46 @@ public class ImProvider extends ContentProvider { "END"); } - /** - * Load settings of providers from the initial configuration file. Should only - * be done once, as the table of providerSettings is a persistent table. - * - * NOTE: This method must be called after table provider and table providerSettings - * have been created. - * @param db - */ - private void loadProviderSettings(SQLiteDatabase db) { - XmlResourceParser parser = getContext().getResources() - .getXml(R.xml.im_service_providers); - - try { - XmlUtils.beginDocument(parser, "im_providers"); - - while (true) { - XmlUtils.nextElement(parser); - String tag = parser.getName(); - if (!"provider".equals(tag)) { - break; + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); + + switch (oldVersion) { + case 43: // this is the db version shipped in Dream 1.0 + // no-op: no schema changed from 43 to 44. The db version was changed to flush + // old provider settings, so new provider setting (including new name/value + // pairs) could be inserted by the plugins. + + // follow thru. + case 44: + if (newVersion <= 44) { + return; } - int numAttrs = parser.getAttributeCount(); - - HashMap<String, String> prefs = new HashMap<String, String>(); - for (int i = 0; i < numAttrs; i++) { - String attrName = parser.getAttributeName(i); - String value = parser.getAttributeValue(i); - - prefs.put(attrName, value); + db.beginTransaction(); + try { + // add category column to the providers table + db.execSQL("ALTER TABLE " + TABLE_PROVIDERS + " ADD COLUMN category TEXT;"); + // add otr column to the contacts table + db.execSQL("ALTER TABLE " + TABLE_CONTACTS + " ADD COLUMN otr INTEGER;"); + + db.setTransactionSuccessful(); + } catch (Throwable ex) { + Log.e(LOG_TAG, ex.getMessage(), ex); + break; // force to destroy all old data; + } finally { + db.endTransaction(); } - String name = prefs.get("name"); - String fullname = prefs.get("full_name"); - String signup_url = prefs.get("signup_url"); - - // insert to table provider - ContentValues values = new ContentValues(); - values.put(Im.Provider.NAME, name); - values.put(Im.Provider.FULLNAME, fullname); - values.put(Im.Provider.SIGNUP_URL, signup_url); - - db.insert(TABLE_PROVIDERS, "name", values); - } - } catch (XmlPullParserException e) { - } catch (IOException e) { - } finally { - parser.close(); + return; } - } - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Log.w(LOG_TAG, "Upgrading database from version " + oldVersion + - " to " + newVersion + ", which will destroy all old data"); + Log.w(LOG_TAG, "Couldn't upgrade db to " + newVersion + ". Destroying old data."); + destroyOldTables(db); + onCreate(db); + } + private void destroyOldTables(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDERS); db.execSQL("DROP TABLE IF EXISTS " + TABLE_ACCOUNTS); db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACT_LIST); @@ -325,19 +321,11 @@ public class ImProvider extends ContentProvider { db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDER_SETTINGS); db.execSQL("DROP TABLE IF EXISTS " + TABLE_OUTGOING_RMQ_MESSAGES); db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_RMQ_ID); - - onCreate(db); } - private void createContactsTables(SQLiteDatabase db, String tablePrefix) { + private void createContactsTables(SQLiteDatabase db) { StringBuilder buf = new StringBuilder(); - String contactsTableName; - - if (tablePrefix != null) { - contactsTableName = tablePrefix + TABLE_CONTACTS; - } else { - contactsTableName = TABLE_CONTACTS; - } + String contactsTableName = TABLE_CONTACTS; // creating the "contacts" table buf.append("CREATE TABLE IF NOT EXISTS "); @@ -360,7 +348,10 @@ public class ImProvider extends ContentProvider { // qc: quick contact (derived from message count) // rejected: if the contact has ever been rejected by the user buf.append("qc INTEGER,"); - buf.append("rejected INTEGER"); + buf.append("rejected INTEGER,"); + + // Off the record status + buf.append("otr INTEGER"); buf.append(");"); @@ -370,9 +361,6 @@ public class ImProvider extends ContentProvider { // creating contact etag table buf.append("CREATE TABLE IF NOT EXISTS "); - if (tablePrefix != null) { - buf.append(tablePrefix); - } buf.append(TABLE_CONTACTS_ETAG); buf.append(" ("); buf.append("_id INTEGER PRIMARY KEY,"); @@ -386,9 +374,6 @@ public class ImProvider extends ContentProvider { // creating the "contactList" table buf.append("CREATE TABLE IF NOT EXISTS "); - if (tablePrefix != null) { - buf.append(tablePrefix); - } buf.append(TABLE_CONTACT_LIST); buf.append(" ("); buf.append("_id INTEGER PRIMARY KEY,"); @@ -403,9 +388,6 @@ public class ImProvider extends ContentProvider { // creating the "blockedList" table buf.append("CREATE TABLE IF NOT EXISTS "); - if (tablePrefix != null) { - buf.append(tablePrefix); - } buf.append(TABLE_BLOCKED_LIST); buf.append(" ("); buf.append("_id INTEGER PRIMARY KEY,"); @@ -500,21 +482,22 @@ public class ImProvider extends ContentProvider { "groupchat INTEGER," + // 1 if group chat, 0 if not TODO: remove this column "last_unread_message TEXT," + // the last unread message "last_message_date INTEGER," + // in seconds - "unsent_composed_message TEXT" + // a composed, but not sent message - + "unsent_composed_message TEXT," + // a composed, but not sent message + "shortcut INTEGER" + // which of 10 slots (if any) this chat occupies ");"); + db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_ACCOUNT_STATUS + " (" + + "_id INTEGER PRIMARY KEY," + + "account INTEGER UNIQUE," + + "presenceStatus INTEGER," + + "connStatus INTEGER" + + ");" + ); + /* when we moved the contact table out of transient_db and into the main db, the contact_cleanup and group_cleanup triggers don't work anymore. It seems we can't create triggers that reference objects in a different database! - // creates in-memory contacts tables. Because we need to download the - // contacts after each login, there is no need to store them in - // persistent storage. When the client server protocol supports the - // client storing contacts offline, we can move these tables to - // persistent storage - createContactsTables(db, cpDbName); - String contactsTableName = TABLE_CONTACTS; if (USE_CONTACT_PRESENCE_TRIGGER) { @@ -568,12 +551,20 @@ public class ImProvider extends ContentProvider { "providers.name AS name"); sProviderAccountsProjectionMap.put(Im.Provider.FULLNAME, "providers.fullname AS fullname"); + sProviderAccountsProjectionMap.put(Im.Provider.CATEGORY, + "providers.category AS category"); sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_ID, "accounts._id AS account_id"); sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_USERNAME, "accounts.username AS account_username"); sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_PW, "accounts.pw AS account_pw"); + sProviderAccountsProjectionMap.put(Im.Provider.ACTIVE_ACCOUNT_LOCKED, + "accounts.locked AS account_locked"); + sProviderAccountsProjectionMap.put(Im.Provider.ACCOUNT_PRESENCE_STATUS, + "accountStatus.presenceStatus AS account_presenceStatus"); + sProviderAccountsProjectionMap.put(Im.Provider.ACCOUNT_CONNECTION_STATUS, + "accountStatus.connStatus AS account_connStatus"); // contacts projection map sContactsProjectionMap = new HashMap<String, String>(); @@ -583,6 +574,7 @@ public class ImProvider extends ContentProvider { sContactsProjectionMap.put(Im.Contacts._COUNT, "COUNT(*) AS _count"); // contacts column + sContactsProjectionMap.put(Im.Contacts._ID, "contacts._id as _id"); sContactsProjectionMap.put(Im.Contacts.USERNAME, "contacts.username as username"); sContactsProjectionMap.put(Im.Contacts.NICKNAME, "contacts.nickname as nickname"); sContactsProjectionMap.put(Im.Contacts.PROVIDER, "contacts.provider as provider"); @@ -619,6 +611,7 @@ public class ImProvider extends ContentProvider { "chats.last_message_date AS last_message_date"); sContactsProjectionMap.put(Im.Contacts.UNSENT_COMPOSED_MESSAGE, "chats.unsent_composed_message AS unsent_composed_message"); + sContactsProjectionMap.put(Im.Contacts.SHORTCUT, "chats.SHORTCUT AS shortcut"); // Avatars columns sContactsProjectionMap.put(Im.Contacts.AVATAR_HASH, "avatars.hash AS avatars_hash"); @@ -724,6 +717,9 @@ public class ImProvider extends ContentProvider { mUrlMatcher.addURI(authority, "outgoingRmqMessages/#", MATCH_OUTGOING_RMQ_MESSAGE); mUrlMatcher.addURI(authority, "outgoingHighestRmqId", MATCH_OUTGOING_HIGHEST_RMQ_ID); mUrlMatcher.addURI(authority, "lastRmqId", MATCH_LAST_RMQ_ID); + + mUrlMatcher.addURI(authority, "accountStatus", MATCH_ACCOUNTS_STATUS); + mUrlMatcher.addURI(authority, "accountStatus/#", MATCH_ACCOUNT_STATUS); } @Override @@ -736,7 +732,7 @@ public class ImProvider extends ContentProvider { public final int update(final Uri url, final ContentValues values, final String selection, final String[] selectionArgs) { - int result; + int result = 0; SQLiteDatabase db = mOpenHelper.getWritableDatabase(); db.beginTransaction(); try { @@ -1061,6 +1057,16 @@ public class ImProvider extends ContentProvider { limit = "1"; break; + case MATCH_ACCOUNTS_STATUS: + qb.setTables(TABLE_ACCOUNT_STATUS); + break; + + case MATCH_ACCOUNT_STATUS: + qb.setTables(TABLE_ACCOUNT_STATUS); + appendWhere(whereClause, Im.AccountStatus.ACCOUNT, "=", + url.getPathSegments().get(1)); + break; + default: throw new IllegalArgumentException("Unknown URL " + url); } @@ -1111,6 +1117,9 @@ public class ImProvider extends ContentProvider { case MATCH_PROVIDERS: return Im.Provider.CONTENT_TYPE; + case MATCH_PROVIDERS_BY_ID: + return Im.Provider.CONTENT_ITEM_TYPE; + case MATCH_ACCOUNTS: return Im.Account.CONTENT_TYPE; @@ -1191,6 +1200,12 @@ public class ImProvider extends ContentProvider { case MATCH_PROVIDER_SETTINGS: return Im.ProviderSettings.CONTENT_TYPE; + case MATCH_ACCOUNTS_STATUS: + return Im.AccountStatus.CONTENT_TYPE; + + case MATCH_ACCOUNT_STATUS: + return Im.AccountStatus.CONTENT_ITEM_TYPE; + default: throw new IllegalArgumentException("Unknown URL"); } @@ -1630,6 +1645,7 @@ public class ImProvider extends ContentProvider { boolean notifyContactContentUri = false; boolean notifyMessagesContentUri = false; boolean notifyGroupMessagesContentUri = false; + boolean notifyProviderAccountContentUri = false; final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int match = mUrlMatcher.match(url); @@ -1642,6 +1658,7 @@ public class ImProvider extends ContentProvider { if (rowID > 0) { resultUri = Uri.parse(Im.Provider.CONTENT_URI + "/" + rowID); } + notifyProviderAccountContentUri = true; break; case MATCH_ACCOUNTS: @@ -1650,6 +1667,7 @@ public class ImProvider extends ContentProvider { if (rowID > 0) { resultUri = Uri.parse(Im.Account.CONTENT_URI + "/" + rowID); } + notifyProviderAccountContentUri = true; break; case MATCH_CONTACTS_BY_PROVIDER: @@ -1773,9 +1791,11 @@ public class ImProvider extends ContentProvider { // fall through case MATCH_CHATS: // Insert into the chats table + initialValues.put(Im.Chats.SHORTCUT, -1); rowID = db.replace(TABLE_CHATS, Im.Chats.CONTACT_ID, initialValues); if (rowID > 0) { resultUri = Uri.parse(Im.Chats.CONTENT_URI + "/" + rowID); + addToQuickSwitch(rowID); } notifyContactContentUri = true; break; @@ -1829,28 +1849,44 @@ public class ImProvider extends ContentProvider { } break; + case MATCH_ACCOUNTS_STATUS: + rowID = db.replace(TABLE_ACCOUNT_STATUS, null, initialValues); + if (rowID > 0) { + resultUri = Uri.parse(Im.AccountStatus.CONTENT_URI + "/" + rowID); + } + notifyProviderAccountContentUri = true; + break; + default: throw new UnsupportedOperationException("Cannot insert into URL: " + url); } // TODO: notify the data change observer? if (resultUri != null) { + ContentResolver resolver = getContext().getContentResolver(); + // In most case, we query contacts with presence and chats joined, thus // we should also notify that contacts changes when presence or chats changed. if (notifyContactContentUri) { - getContext().getContentResolver().notifyChange(Im.Contacts.CONTENT_URI, null); + resolver.notifyChange(Im.Contacts.CONTENT_URI, null); } if (notifyContactListContentUri) { - getContext().getContentResolver().notifyChange(Im.ContactList.CONTENT_URI, null); + resolver.notifyChange(Im.ContactList.CONTENT_URI, null); } if (notifyMessagesContentUri) { - getContext().getContentResolver().notifyChange(Im.Messages.CONTENT_URI, null); + resolver.notifyChange(Im.Messages.CONTENT_URI, null); } if (notifyGroupMessagesContentUri) { - getContext().getContentResolver().notifyChange(Im.GroupMessages.CONTENT_URI, null); + resolver.notifyChange(Im.GroupMessages.CONTENT_URI, null); + } + + if (notifyProviderAccountContentUri) { + if (DBG) log("notify insert for " + Im.Provider.CONTENT_URI_WITH_ACCOUNT); + resolver.notifyChange(Im.Provider.CONTENT_URI_WITH_ACCOUNT, + null); } } return resultUri; @@ -1868,6 +1904,105 @@ public class ImProvider extends ContentProvider { } } + // Quick-switch management + // The chat UI provides slots (0, 9, .., 1) for the first 10 chats. This allows you to + // quickly switch between these chats by chording menu+#. We number from the right end of + // the number row and move leftward to make an easier two-hand chord with the menu button + // on the left side of the keyboard. + private void addToQuickSwitch(long newRow) { + // Since there are fewer than 10, there must be an empty slot. Let's find it. + int slot = findEmptyQuickSwitchSlot(); + + if (slot == -1) { + return; + } + + updateSlotForChat(newRow, slot); + } + + // If there are more than 10 chats and one with a quick switch slot ends then pick a chat + // that doesn't have a slot and have it inhabit the newly emptied slot. + private void backfillQuickSwitchSlots() { + // Find all the chats without a quick switch slot, and order + Cursor c = query(Im.Chats.CONTENT_URI, + BACKFILL_PROJECTION, + Im.Chats.SHORTCUT + "=-1", null, Im.Chats.LAST_MESSAGE_DATE + "DESC"); + + try { + if (c.getCount() < 1) { + return; + } + + int slot = findEmptyQuickSwitchSlot(); + + if (slot != -1) { + c.moveToFirst(); + + long id = c.getLong(c.getColumnIndex(Im.Chats._ID)); + + updateSlotForChat(id, slot); + } + } finally { + c.close(); + } + } + + private int updateSlotForChat(long chatId, int slot) { + ContentValues values = new ContentValues(); + + values.put(Im.Chats.SHORTCUT, slot); + + return update(Im.Chats.CONTENT_URI, values, Im.Chats._ID + "=?", + new String[] { Long.toString(chatId) }); + } + + private int findEmptyQuickSwitchSlot() { + Cursor c = queryInternal(Im.Chats.CONTENT_URI, FIND_SHORTCUT_PROJECTION, null, null, null); + final int N = c.getCount(); + + try { + // If there are 10 or more chats then all the quick switch slots are already filled + if (N >= 10) { + return -1; + } + + int slots = 0; + int column = c.getColumnIndex(Im.Chats.SHORTCUT); + + // The map is here because numbers go from 0-9, but we want to assign slots in + // 0, 9, 8, ..., 1 order to match the right-to-left reading of the number row + // on the keyboard. + int[] map = new int[] { 0, 9, 8, 7, 6, 5, 4, 3, 2, 1 }; + + // Mark all the slots that are in use + // The shortcuts represent actual keyboard number row keys, and not ordinals. + // So 7 would mean the shortcut is the 7 key on the keyboard and NOT the 7th + // shortcut. The passing of slot through map[] below maps these keyboard key + // shortcuts into an ordinal bit position in the 'slots' bitfield. + for (c.moveToFirst(); ! c.isAfterLast(); c.moveToNext()) { + int slot = c.getInt(column); + + if (slot != -1) { + slots |= (1 << map[slot]); + } + } + + // Try to find an empty one + // As we exit this, the push of i through map[] maps the ordinal bit position + // in the 'slots' bitfield onto a key on the number row of the device keyboard. + // The keyboard key is what is used to designate the shortcut. + for (int i = 0; i < 10; i++) { + if ((slots & (1 << i)) == 0) { + return map[i]; + } + } + + return -1; + } finally { + c.close(); + } + } + /** * manual trigger for deleting contacts */ @@ -1936,14 +2071,18 @@ public class ImProvider extends ContentProvider { boolean notifyMessagesContentUri = false; boolean notifyGroupMessagesContentUri = false; boolean notifyContactListContentUri = false; + boolean notifyProviderAccountContentUri = false; int match = mUrlMatcher.match(url); boolean contactDeleted = false; long deletedContactId = 0; + boolean backfillQuickSwitchSlots = false; + switch (match) { case MATCH_PROVIDERS: tableToChange = TABLE_PROVIDERS; + notifyProviderAccountContentUri = true; break; case MATCH_ACCOUNTS_BY_ID: @@ -1951,6 +2090,15 @@ public class ImProvider extends ContentProvider { // fall through case MATCH_ACCOUNTS: tableToChange = TABLE_ACCOUNTS; + notifyProviderAccountContentUri = true; + break; + + case MATCH_ACCOUNT_STATUS: + changedItemId = url.getPathSegments().get(1); + // fall through + case MATCH_ACCOUNTS_STATUS: + tableToChange = TABLE_ACCOUNT_STATUS; + notifyProviderAccountContentUri = true; break; case MATCH_CONTACTS: @@ -2080,6 +2228,7 @@ public class ImProvider extends ContentProvider { case MATCH_CHATS: tableToChange = TABLE_CHATS; + backfillQuickSwitchSlots = true; break; case MATCH_CHATS_BY_ACCOUNT: @@ -2208,6 +2357,14 @@ public class ImProvider extends ContentProvider { getContext().getContentResolver().notifyChange(Im.GroupMessages.CONTENT_URI, null); } else if (notifyContactListContentUri) { getContext().getContentResolver().notifyChange(Im.ContactList.CONTENT_URI, null); + } else if (notifyProviderAccountContentUri) { + if (DBG) log("notify delete for " + Im.Provider.CONTENT_URI_WITH_ACCOUNT); + getContext().getContentResolver().notifyChange(Im.Provider.CONTENT_URI_WITH_ACCOUNT, + null); + } + + if (backfillQuickSwitchSlots) { + backfillQuickSwitchSlots(); } } @@ -2229,14 +2386,31 @@ public class ImProvider extends ContentProvider { boolean notifyMessagesContentUri = false; boolean notifyGroupMessagesContentUri = false; boolean notifyContactListContentUri = false; + boolean notifyProviderAccountContentUri = false; int match = mUrlMatcher.match(url); switch (match) { + case MATCH_PROVIDERS_BY_ID: + changedItemId = url.getPathSegments().get(1); + // fall through + case MATCH_PROVIDERS: + tableToChange = TABLE_PROVIDERS; + break; + case MATCH_ACCOUNTS_BY_ID: changedItemId = url.getPathSegments().get(1); // fall through case MATCH_ACCOUNTS: tableToChange = TABLE_ACCOUNTS; + notifyProviderAccountContentUri = true; + break; + + case MATCH_ACCOUNT_STATUS: + changedItemId = url.getPathSegments().get(1); + // fall through + case MATCH_ACCOUNTS_STATUS: + tableToChange = TABLE_ACCOUNT_STATUS; + notifyProviderAccountContentUri = true; break; case MATCH_CONTACTS: @@ -2420,12 +2594,16 @@ public class ImProvider extends ContentProvider { || match == MATCH_CONTACTS_BAREBONE) { getContext().getContentResolver().notifyChange(Im.Contacts.CONTENT_URI, null); } else if (notifyMessagesContentUri) { - log("notify change for " + Im.Messages.CONTENT_URI); + if (DBG) log("notify change for " + Im.Messages.CONTENT_URI); getContext().getContentResolver().notifyChange(Im.Messages.CONTENT_URI, null); } else if (notifyGroupMessagesContentUri) { getContext().getContentResolver().notifyChange(Im.GroupMessages.CONTENT_URI, null); } else if (notifyContactListContentUri) { getContext().getContentResolver().notifyChange(Im.ContactList.CONTENT_URI, null); + } else if (notifyProviderAccountContentUri) { + if (DBG) log("notify change for " + Im.Provider.CONTENT_URI_WITH_ACCOUNT); + getContext().getContentResolver().notifyChange(Im.Provider.CONTENT_URI_WITH_ACCOUNT, + null); } } diff --git a/src/com/android/providers/im/LandingPage.java b/src/com/android/providers/im/LandingPage.java new file mode 100644 index 0000000..47cddc3 --- /dev/null +++ b/src/com/android/providers/im/LandingPage.java @@ -0,0 +1,689 @@ +/* + * 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.providers.im; + +import android.app.ListActivity; +import android.app.ActivityManagerNative; +import android.app.ActivityThread; +import android.app.Application; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.database.Cursor; +import android.im.IImPlugin; +import android.im.ImPluginConsts; +import android.im.BrandingResourceIDs; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.IBinder; +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 java.util.List; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; + +import com.android.providers.im.R; + +import dalvik.system.PathClassLoader; + +public class LandingPage extends ListActivity implements View.OnCreateContextMenuListener { + private static final String TAG = "IM"; + private final static boolean LOCAL_DEBUG = false; + + 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; + + private ProviderAdapter mAdapter; + private Cursor mProviderCursor; + + private static final String[] PROVIDER_PROJECTION = { + Im.Provider._ID, + Im.Provider.NAME, + Im.Provider.FULLNAME, + Im.Provider.CATEGORY, + Im.Provider.ACTIVE_ACCOUNT_ID, + Im.Provider.ACTIVE_ACCOUNT_USERNAME, + Im.Provider.ACTIVE_ACCOUNT_PW, + Im.Provider.ACTIVE_ACCOUNT_LOCKED, + Im.Provider.ACCOUNT_PRESENCE_STATUS, + Im.Provider.ACCOUNT_CONNECTION_STATUS, + }; + + static final int PROVIDER_ID_COLUMN = 0; + static final int PROVIDER_NAME_COLUMN = 1; + static final int PROVIDER_FULLNAME_COLUMN = 2; + static final int PROVIDER_CATEGORY_COLUMN = 3; + static final int ACTIVE_ACCOUNT_ID_COLUMN = 4; + static final int ACTIVE_ACCOUNT_USERNAME_COLUMN = 5; + static final int ACTIVE_ACCOUNT_PW_COLUMN = 6; + static final int ACTIVE_ACCOUNT_LOCKED = 7; + static final int ACCOUNT_PRESENCE_STATUS = 8; + static final int ACCOUNT_CONNECTION_STATUS = 9; + + private HashMap<String, PluginInfo> mProviderToPluginMap; + private HashMap<Long, PluginInfo> mAccountToPluginMap; + private HashMap<Long, BrandingResources> mBrandingResources; + private BrandingResources mDefaultBrandingResources; + + public class PluginInfo { + public IImPlugin mPlugin; + /** + * The name of the package that the plugin is in. + */ + public String mPackageName; + + /** + * The name of the class that implements {@link @ImFrontDoorPlugin} in this plugin. + */ + public String mClassName; + + /** + * The full path to the location of the package that the plugin is in. + */ + public String mSrcPath; + + public PluginInfo(IImPlugin plugin, String packageName, String className, + String srcPath) { + mPackageName = packageName; + mClassName = className; + mSrcPath = srcPath; + mPlugin = plugin; + } + }; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setTitle(R.string.landing_page_title); + + if (!loadPlugins()) { + Log.e(TAG, "load plugin failed, no plugin found!"); + finish(); + return; + } + + startPlugins(); + + mProviderCursor = managedQuery(Im.Provider.CONTENT_URI_WITH_ACCOUNT, + PROVIDER_PROJECTION, + null /* selection */, + null /* selection args */, + Im.Provider.DEFAULT_SORT_ORDER); + mAdapter = new ProviderAdapter(this, mProviderCursor); + setListAdapter(mAdapter); + + rebuildAccountToPluginMap(); + + mBrandingResources = new HashMap<Long, BrandingResources>(); + loadDefaultBrandingRes(); + loadBrandingResources(); + + registerForContextMenu(getListView()); + } + + @Override + protected void onRestart() { + super.onRestart(); + + // refresh the accountToPlugin map after mProviderCursor is requeried + rebuildAccountToPluginMap(); + + startPlugins(); + } + + @Override + protected void onStop() { + super.onStop(); + stopPlugins(); + } + + private boolean loadPlugins() { + mProviderToPluginMap = new HashMap<String, PluginInfo>(); + + PackageManager pm = getPackageManager(); + List<ResolveInfo> plugins = pm.queryIntentServices( + new Intent(ImPluginConsts.PLUGIN_ACTION_NAME), + PackageManager.GET_META_DATA); + for (ResolveInfo info : plugins) { + if (Log.isLoggable(TAG, Log.DEBUG)) log("loadPlugins: found plugin " + info); + + ServiceInfo serviceInfo = info.serviceInfo; + if (serviceInfo == null) { + Log.e(TAG, "Ignore bad IM frontdoor plugin: " + info); + continue; + } + + IImPlugin plugin = null; + + // 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(serviceInfo.applicationInfo.sourceDir, + getClassLoader()); + try { + if (Log.isLoggable(TAG, Log.DEBUG)) { + log("loadPlugin: load class " + serviceInfo.name); + } + Class cls = classLoader.loadClass(serviceInfo.name); + Object newInstance = cls.newInstance(); + Method m; + + // call "attach" method, so the plugin will get initialized with the proper context + m = cls.getMethod("attach", Context.class, ActivityThread.class, String.class, + IBinder.class, Application.class, Object.class); + m.invoke(newInstance, + new Object[] {this, null, serviceInfo.name, null, getApplication(), + ActivityManagerNative.getDefault()}); + + // call "bind" to get the plugin object + m = cls.getMethod("onBind", Intent.class); + plugin = (IImPlugin)m.invoke(newInstance, new Object[]{null}); + } catch (ClassNotFoundException e) { + Log.e(TAG, "Failed load the plugin", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "Failed load the plugin", e); + } catch (InstantiationException e) { + Log.e(TAG, "Failed load the plugin", e); + } catch (SecurityException e) { + Log.e(TAG, "Failed load the plugin", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Failed load the plugin", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed load the plugin", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Failed load the plugin", e); + } + + if (plugin != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) log("loadPlugin: plugin " + plugin + " loaded"); + ArrayList<String> providers = getSupportedProviders(plugin); + + if (providers == null || providers.size() == 0) { + Log.e(TAG, "Ignore bad IM frontdoor plugin: " + info + ". No providers found"); + continue; + } + + PluginInfo pluginInfo = new PluginInfo(plugin, + serviceInfo.packageName, + serviceInfo.name, + serviceInfo.applicationInfo.sourceDir); + + for (String providerName : providers) { + mProviderToPluginMap.put(providerName, pluginInfo); + } + } + } + + return mProviderToPluginMap.size() > 0; + } + + private void startPlugins() { + Iterator<PluginInfo> itor = mProviderToPluginMap.values().iterator(); + + while (itor.hasNext()) { + PluginInfo pluginInfo = itor.next(); + try { + pluginInfo.mPlugin.onStart(); + } catch (RemoteException e) { + Log.e(TAG, "Could not start plugin " + pluginInfo.mPackageName, e); + } + } + } + + private void stopPlugins() { + Iterator<PluginInfo> itor = mProviderToPluginMap.values().iterator(); + + while (itor.hasNext()) { + PluginInfo pluginInfo = itor.next(); + try { + pluginInfo.mPlugin.onStop(); + } catch (RemoteException e) { + Log.e(TAG, "Could not stop plugin " + pluginInfo.mPackageName, e); + } + } + } + + private ArrayList<String> getSupportedProviders(IImPlugin plugin) { + ArrayList<String> providers = null; + + try { + providers = (ArrayList<String>) plugin.getSupportedProviders(); + } catch (RemoteException ex) { + Log.e(TAG, "getSupportedProviders caught ", ex); + } + + return providers; + } + + 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.STRING_MENU_CONTACT_LIST, + R.string.menu_view_contact_list); + + mDefaultBrandingResources = new BrandingResources(this, resMapping, null /* default res */); + } + + private void loadBrandingResources() { + mProviderCursor.moveToFirst(); + do { + long providerId = mProviderCursor.getLong(PROVIDER_ID_COLUMN); + String providerName = mProviderCursor.getString(PROVIDER_NAME_COLUMN); + PluginInfo pluginInfo = mProviderToPluginMap.get(providerName); + + if (pluginInfo == null) { + Log.w(TAG, "[LandingPage] loadBrandingResources: no plugin found for " + providerName); + continue; + } + + if (!mBrandingResources.containsKey(providerId)) { + BrandingResources res = new BrandingResources(this, pluginInfo, providerName, + mDefaultBrandingResources); + mBrandingResources.put(providerId, res); + } + } while (mProviderCursor.moveToNext()) ; + } + + public BrandingResources getBrandingResource(long providerId) { + BrandingResources res = mBrandingResources.get(providerId); + return res == null ? mDefaultBrandingResources : res; + } + + private void rebuildAccountToPluginMap() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + log("rebuildAccountToPluginMap"); + } + + if (mAccountToPluginMap != null) { + mAccountToPluginMap.clear(); + } + + mAccountToPluginMap = new HashMap<Long, PluginInfo>(); + + mProviderCursor.moveToFirst(); + do { + long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN); + + if (accountId == 0) { + continue; + } + + String name = mProviderCursor.getString(PROVIDER_NAME_COLUMN); + PluginInfo pluginInfo = mProviderToPluginMap.get(name); + if (pluginInfo != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + log("rebuildAccountToPluginMap: add plugin for acct=" + accountId + ", provider=" + name); + } + mAccountToPluginMap.put(accountId, pluginInfo); + } else { + Log.w(TAG, "[LandingPage] no plugin found for " + name); + } + } while (mProviderCursor.moveToNext()) ; + } + + private void signIn(long accountId) { + if (accountId == 0) { + Log.w(TAG, "signIn: account id is 0, bail"); + return; + } + + boolean isAccountEditible = mProviderCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0; + if (isAccountEditible && mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) { + // no password, edit the account + if (Log.isLoggable(TAG, Log.DEBUG)) log("no pw for account " + accountId); + Intent intent = getEditAccountIntent(); + startActivity(intent); + return; + } + + + PluginInfo pluginInfo = mAccountToPluginMap.get(accountId); + if (pluginInfo == null) { + Log.e(TAG, "signIn: cannot find plugin for account " + accountId); + return; + } + + try { + if (Log.isLoggable(TAG, Log.DEBUG)) log("sign in for account " + accountId); + pluginInfo.mPlugin.signIn(accountId); + } catch (RemoteException ex) { + Log.e(TAG, "signIn failed", ex); + } + } + + boolean isSigningIn(Cursor cursor) { + int connectionStatus = cursor.getInt(ACCOUNT_CONNECTION_STATUS); + return connectionStatus == Im.ConnectionStatus.CONNECTING; + } + + boolean isSignedIn(Cursor cursor) { + int connectionStatus = cursor.getInt(ACCOUNT_CONNECTION_STATUS); + return connectionStatus == Im.ConnectionStatus.ONLINE; + } + + private boolean allAccountsSignedOut() { + mProviderCursor.moveToFirst(); + do { + if (isSignedIn(mProviderCursor)) { + return false; + } + } while (mProviderCursor.moveToNext()) ; + + return true; + } + + private void signoutAll() { + do { + long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN); + signOut(accountId); + } while (mProviderCursor.moveToNext()) ; + } + + private void signOut(long accountId) { + if (accountId == 0) { + Log.w(TAG, "signOut: account id is 0, bail"); + return; + } + + PluginInfo pluginInfo = mAccountToPluginMap.get(accountId); + if (pluginInfo == null) { + Log.e(TAG, "signOut: cannot find plugin for account " + accountId); + return; + } + + try { + if (Log.isLoggable(TAG, Log.DEBUG)) log("sign out for account " + accountId); + pluginInfo.mPlugin.signOut(accountId); + } catch (RemoteException ex) { + Log.e(TAG, "signOut failed", ex); + } + } + + @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: + signoutAll(); + 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(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); + boolean isLoggingIn = isSigningIn(providerCursor); + boolean isLoggedIn = isSignedIn(providerCursor); + + if (!isLoggedIn) { + menu.add(0, ID_SIGN_IN, 0, R.string.sign_in).setIcon(R.drawable.ic_menu_login); + } else { + BrandingResources brandingRes = 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); + } + + boolean isAccountEditible = providerCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0; + if (isAccountEditible && !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(TAG, "bad menuInfo", e); + return false; + } + long providerId = info.id; + Cursor providerCursor = (Cursor) getListAdapter().getItem(info.position); + long accountId = providerCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN); + + switch (item.getItemId()) { + case ID_EDIT_ACCOUNT: + { + startActivity(getEditAccountIntent()); + return true; + } + + case ID_REMOVE_ACCOUNT: + { + Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId); + getContentResolver().delete(accountUri, null, null); + // Requery the cursor to force refreshing screen + providerCursor.requery(); + return true; + } + + case ID_VIEW_CONTACT_LIST: + { + Intent intent = getViewContactsIntent(); + startActivity(intent); + return true; + } + case ID_ADD_ACCOUNT: + { + startActivity(getCreateAccountIntent()); + return true; + } + + case ID_SIGN_IN: + { + signIn(accountId); + return true; + } + + case ID_SIGN_OUT: + { + // TODO: progress bar + signOut(accountId); + return true; + } + + case ID_SETTINGS: + { + Intent intent = new Intent(Intent.ACTION_VIEW, Im.ProviderSettings.CONTENT_URI); + intent.addCategory(getProviderCategory(providerCursor)); + intent.putExtra("providerId", providerId); + startActivity(intent); + return true; + } + + } + + return false; + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + Intent intent = null; + mProviderCursor.moveToPosition(position); + + if (mProviderCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) { + // add account + intent = getCreateAccountIntent(); + } else { + int state = mProviderCursor.getInt(ACCOUNT_CONNECTION_STATUS); + + if (state == Im.ConnectionStatus.OFFLINE || state == Im.ConnectionStatus.CONNECTING) { + boolean isAccountEditible = mProviderCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0; + if (isAccountEditible && mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) { + // no password, edit the account + intent = getEditAccountIntent(); + } else { + long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN); + signIn(accountId); + } + } else { + intent = getViewContactsIntent(); + } + } + + if (intent != null) { + startActivity(intent); + } + } + + Intent getCreateAccountIntent() { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_INSERT); + + long providerId = mProviderCursor.getLong(PROVIDER_ID_COLUMN); + intent.setData(ContentUris.withAppendedId(Im.Provider.CONTENT_URI, providerId)); + intent.addCategory(getProviderCategory(mProviderCursor)); + return intent; + } + + Intent getEditAccountIntent() { + Intent intent = new Intent(Intent.ACTION_EDIT, + ContentUris.withAppendedId(Im.Account.CONTENT_URI, + mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN))); + intent.addCategory(getProviderCategory(mProviderCursor)); + return intent; + } + + Intent getViewContactsIntent() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Im.Contacts.CONTENT_URI); + intent.addCategory(getProviderCategory(mProviderCursor)); + intent.putExtra("accountId", mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN)); + return intent; + } + + private String getProviderCategory(Cursor cursor) { + return cursor.getString(PROVIDER_CATEGORY_COLUMN); + } + + + static void log(String msg) { + Log.d(TAG, "[LandingPage]" + 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, LandingPage.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); + } + } + +} diff --git a/src/com/android/providers/im/ProviderListItem.java b/src/com/android/providers/im/ProviderListItem.java new file mode 100644 index 0000000..4e5c78a --- /dev/null +++ b/src/com/android/providers/im/ProviderListItem.java @@ -0,0 +1,185 @@ +/* + * 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.providers.im; + +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.im.BrandingResourceIDs; +import android.provider.Im; +import android.view.View; +import android.util.Log; +import com.android.providers.im.R; + +public class ProviderListItem extends LinearLayout { + private static final String TAG = "IM"; + private static final boolean LOCAL_DEBUG = false; + + private LandingPage mActivity; + private ImageView mProviderIcon; + private ImageView mStatusIcon; + private TextView mLine1; + private TextView mLine2; + private TextView mChatView; + private int mProviderIdColumn; + private int mProviderFullnameColumn; + private int mActiveAccountIdColumn; + private int mActiveAccountUserNameColumn; + private int mAccountPresenceStatusColumn; + private int mAccountConnectionStatusColumn; + + public ProviderListItem(Context context, LandingPage activity) { + super(context); + mActivity = 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); + mProviderFullnameColumn = c.getColumnIndexOrThrow(Im.Provider.FULLNAME); + mActiveAccountIdColumn = c.getColumnIndexOrThrow( + Im.Provider.ACTIVE_ACCOUNT_ID); + mActiveAccountUserNameColumn = c.getColumnIndexOrThrow( + Im.Provider.ACTIVE_ACCOUNT_USERNAME); + mAccountPresenceStatusColumn = c.getColumnIndexOrThrow( + Im.Provider.ACCOUNT_PRESENCE_STATUS); + mAccountConnectionStatusColumn = c.getColumnIndexOrThrow( + Im.Provider.ACCOUNT_CONNECTION_STATUS); + } + + 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 = mActivity.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); + + int connectionStatus = cursor.getInt(mAccountConnectionStatusColumn); + + switch (connectionStatus) { + case Im.ConnectionStatus.CONNECTING: + statusIcon.setVisibility(View.GONE); + chatView.setVisibility(View.VISIBLE); + chatView.setText(R.string.signing_in_wait); + break; + + case Im.ConnectionStatus.ONLINE: + int presenceIconId = getPresenceIconId(cursor); + 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); + } + break; + + default: + statusIcon.setVisibility(View.GONE); + chatView.setVisibility(View.GONE); + break; + } + } 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) { + // TODO: this is code used to get Google Talk's chat count. Not sure if this will work + // for IMPS chat count. + StringBuilder where = new StringBuilder(); + where.append(Im.Chats.CONTACT_ID); + where.append(" in (select _id from contacts where "); + where.append(Im.Contacts.ACCOUNT); + where.append("="); + where.append(accountId); + where.append(")"); + + Cursor cursor = cr.query(Im.Chats.CONTENT_URI, null, where.toString(), null, null); + + try { + return cursor.getCount(); + } finally { + cursor.close(); + } + } + + private int getPresenceIconId(Cursor cursor) { + int presenceStatus = cursor.getInt(mAccountPresenceStatusColumn); + + if (LOCAL_DEBUG) log("getPresenceIconId: presenceStatus=" + presenceStatus); + + switch (presenceStatus) { + case Im.Presence.AVAILABLE: + return BrandingResourceIDs.DRAWABLE_PRESENCE_ONLINE; + + case Im.Presence.IDLE: + 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; + } + } + + private void log(String msg) { + Log.d(TAG, msg); + } +} |