summaryrefslogtreecommitdiff
path: root/src/com
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-12-17 18:06:04 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2008-12-17 18:06:04 -0800
commiteef1589d1f658de1b50a5617c4f5edae0daa0769 (patch)
tree4ccbde7444d3de98cfae0650d93953fd251aea30 /src/com
parentef4989eb3bd594f384dd24ac3b2e7e13e180a026 (diff)
downloadImProvider-eef1589d1f658de1b50a5617c4f5edae0daa0769.tar.gz
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/providers/im/BrandingResources.java160
-rw-r--r--src/com/android/providers/im/ImProvider.java362
-rw-r--r--src/com/android/providers/im/LandingPage.java689
-rw-r--r--src/com/android/providers/im/ProviderListItem.java185
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);
+ }
+}