blob: 2c09b02f9fcc2c307df94d7eac787c976e717b5e [file] [log] [blame]
/*
* 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,
};
private static final int PROVIDER_ID_COLUMN = 0;
private static final int PROVIDER_NAME_COLUMN = 1;
private static final int PROVIDER_FULLNAME_COLUMN = 2;
private static final int PROVIDER_CATEGORY_COLUMN = 3;
private static final int ACTIVE_ACCOUNT_ID_COLUMN = 4;
private static final int ACTIVE_ACCOUNT_USERNAME_COLUMN = 5;
private static final int ACTIVE_ACCOUNT_PW_COLUMN = 6;
private static final int ACTIVE_ACCOUNT_LOCKED = 7;
private static final int ACCOUNT_PRESENCE_STATUS = 8;
private static final int ACCOUNT_CONNECTION_STATUS = 9;
private static final String PROVIDER_SELECTION = "providers.name!=?";
private HashMap<String, PluginInfo> mProviderToPluginMap;
private HashMap<Long, PluginInfo> mAccountToPluginMap;
private HashMap<Long, BrandingResources> mBrandingResources;
private BrandingResources mDefaultBrandingResources;
private String[] mProviderSelectionArgs = new String[1];
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, "[onCreate] load plugin failed, no plugin found!");
finish();
return;
}
startPlugins();
// get everything except for Google Talk.
mProviderSelectionArgs[0] = Im.ProviderNames.GTALK;
mProviderCursor = managedQuery(Im.Provider.CONTENT_URI_WITH_ACCOUNT,
PROVIDER_PROJECTION,
PROVIDER_SELECTION /* selection */,
mProviderSelectionArgs /* 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
if (!rebuildAccountToPluginMap()) {
Log.w(TAG, "[onRestart] rebuiltAccountToPluginMap failed, reload plugins...");
if (!loadPlugins()) {
Log.e(TAG, "[onRestart] load plugin failed, no plugin found!");
finish();
return;
}
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 boolean rebuildAccountToPluginMap() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
log("rebuildAccountToPluginMap");
}
if (mAccountToPluginMap != null) {
mAccountToPluginMap.clear();
}
mAccountToPluginMap = new HashMap<Long, PluginInfo>();
mProviderCursor.moveToFirst();
boolean retVal = true;
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);
retVal = false;
}
} while (mProviderCursor.moveToNext()) ;
return retVal;
}
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(com.android.internal.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);
}
}
}