/**
* Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
* This file is part of CSipSimple.
*
* CSipSimple is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* If you own a pjsip commercial license you can also redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as an android library.
*
* CSipSimple is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CSipSimple. If not, see <http://www.gnu.org/licenses/>.
*/
package com.csipsimple.widgets;
import android.content.ComponentName;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.drawable.BitmapDrawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.actionbarsherlock.internal.view.View_HasStateListenerSupport;
import com.actionbarsherlock.internal.view.View_OnAttachStateChangeListener;
import com.actionbarsherlock.internal.view.menu.MenuBuilder;
import com.actionbarsherlock.internal.view.menu.MenuPopupHelper;
import com.actionbarsherlock.view.MenuItem;
import com.actionbarsherlock.view.MenuItem.OnMenuItemClickListener;
import com.csipsimple.R;
import com.csipsimple.api.SipProfile;
import com.csipsimple.utils.AccountListUtils;
import com.csipsimple.utils.AccountListUtils.AccountStatusDisplay;
import com.csipsimple.utils.CallHandlerPlugin;
import com.csipsimple.utils.CallHandlerPlugin.OnLoadListener;
import com.csipsimple.utils.Compatibility;
import com.csipsimple.utils.Log;
import com.csipsimple.wizards.WizardUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class AccountChooserButton extends LinearLayout implements OnClickListener, View_HasStateListenerSupport {
protected static final String THIS_FILE = "AccountChooserButton";
private static final String[] ACC_PROJECTION = new String[] {
SipProfile.FIELD_ID,
SipProfile.FIELD_ACC_ID, // Needed for default domain
SipProfile.FIELD_REG_URI, // Needed for default domain
SipProfile.FIELD_PROXY, // Needed for default domain
SipProfile.FIELD_DEFAULT_URI_SCHEME, // Needed for default scheme
SipProfile.FIELD_DISPLAY_NAME,
SipProfile.FIELD_WIZARD
};
private final TextView textView;
private final ImageView imageView;
private SipProfile account = null;
private Long targetAccountId = null;
private boolean showExternals = true;
private final ComponentName telCmp;
private OnAccountChangeListener onAccountChange = null;
/**
* Interface definition for a callback to be invoked when PjSipAccount is
* choosen
*/
public interface OnAccountChangeListener {
/**
* Called when the user make an action
*
* @param keyCode keyCode pressed
* @param dialTone corresponding dialtone
*/
void onChooseAccount(SipProfile account);
}
public AccountChooserButton(Context context) {
this(context, null);
}
public AccountChooserButton(Context context, AttributeSet attrs) {
super(context, attrs);
telCmp = new ComponentName(getContext(), com.csipsimple.plugins.telephony.CallHandler.class);
// UI management
setClickable(true);
setFocusable(true);
setBackgroundResource(R.drawable.abs__spinner_ab_holo_dark);
setOrientation(VERTICAL);
setPadding(6, 0, 6, 0);
setGravity(Gravity.CENTER);
// Inflate sub views
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.account_chooser_button, this, true);
setOnClickListener(this);
textView = (TextView) findViewById(R.id.quickaction_text);
imageView = (ImageView) findViewById(R.id.quickaction_icon);
mMenuBuilder = new MenuBuilder(getContext());
// Init accounts
setAccount(null);
}
public AccountChooserButton(Context context, AttributeSet attrs, int style) {
this(context, attrs);
}
private final Handler mHandler = new Handler();
private AccountStatusContentObserver statusObserver = null;
private boolean canChangeIfValid = true;
private MenuPopupHelper mPopupMenu;
private MenuBuilder mMenuBuilder;
private final Set<View_OnAttachStateChangeListener> mListeners = new HashSet<View_OnAttachStateChangeListener>();
/**
* Observer for changes of account registration status
*/
class AccountStatusContentObserver extends ContentObserver {
public AccountStatusContentObserver(Handler h) {
super(h);
}
public void onChange(boolean selfChange) {
Log.d(THIS_FILE, "Accounts status.onChange( " + selfChange + ")");
updateRegistration();
}
}
/**
* Allow this widget to automatically change the current account without
* user interaction if a new account is registered with higher priority
*
* @param changeable Whether the widget is allowed to change selected
* account by itself
*/
public void setChangeable(boolean changeable) {
canChangeIfValid = changeable;
}
/**
* Set the account id that should be tried to be adopted by this button if
* available If null it will try to select account with higher priority
*
* @param aTargetAccountId id of the account to try to select
*/
public void setTargetAccount(Long aTargetAccountId) {
targetAccountId = aTargetAccountId;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if(statusObserver == null) {
statusObserver = new AccountStatusContentObserver(mHandler);
getContext().getContentResolver().registerContentObserver(SipProfile.ACCOUNT_STATUS_URI,
true, statusObserver);
}
if(!isInEditMode()) {
updateRegistration();
}
for (View_OnAttachStateChangeListener listener : mListeners) {
listener.onViewAttachedToWindow(this);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (statusObserver != null) {
getContext().getContentResolver().unregisterContentObserver(statusObserver);
statusObserver = null;
}
for (View_OnAttachStateChangeListener listener : mListeners) {
listener.onViewDetachedFromWindow(this);
}
}
@Override
public void onClick(View v) {
Log.d(THIS_FILE, "Click the account chooser button");
if(mPopupMenu == null) {
mPopupMenu = new MenuPopupHelper(getContext(), mMenuBuilder, this, false);
mPopupMenu.setForceShowIcon(true);
}
mMenuBuilder.removeGroup(R.id.menu_accbtn_accounts);
Cursor c = getContext().getContentResolver().query(SipProfile.ACCOUNT_URI, ACC_PROJECTION, SipProfile.FIELD_ACTIVE + "=?", new String[] {
"1"
}, null);
boolean hasSomeSip = false;
if (c != null) {
try {
if (c.moveToFirst()) {
do {
final SipProfile account = new SipProfile(c);
AccountStatusDisplay accountStatusDisplay = AccountListUtils
.getAccountDisplay(getContext(), account.id);
if (accountStatusDisplay.availableForCalls) {
BitmapDrawable drawable = new BitmapDrawable(getResources(),
WizardUtils.getWizardBitmap(getContext(), account));
MenuItem item = mMenuBuilder.add(R.id.menu_accbtn_accounts, MenuBuilder.NONE, MenuBuilder.NONE, account.display_name);
item.setIcon(drawable);
item.setOnMenuItemClickListener(new OnAccountMenuItemListener(account));
hasSomeSip = true;
}
} while (c.moveToNext());
}
} catch (Exception e) {
Log.e(THIS_FILE, "Error on looping over sip profiles", e);
} finally {
c.close();
}
}
if(!hasSomeSip) {
MenuItem item = mMenuBuilder.add(R.id.menu_accbtn_accounts, MenuBuilder.NONE, MenuBuilder.NONE, R.string.acct_inactive);
item.setIcon(android.R.drawable.ic_dialog_alert);
}
if (showExternals) {
// Add external rows
Map<String, String> callHandlers = CallHandlerPlugin.getAvailableCallHandlers(getContext());
boolean includeGsm = Compatibility.canMakeGSMCall(getContext());
for (String packageName : callHandlers.keySet()) {
Log.d(THIS_FILE, "Compare "+packageName+" to "+telCmp.flattenToString());
// We ensure that GSM integration is not prevented
if(!includeGsm && packageName.equals(telCmp.flattenToString())) {
continue;
}
// Else we can add
CallHandlerPlugin ch = new CallHandlerPlugin(getContext());
ch.loadFrom(packageName, null, new OnPluginLoadListener());
}
}
mPopupMenu.show();
}
private class OnPluginLoadListener implements OnLoadListener {
@Override
public void onLoad(CallHandlerPlugin ch) {
mHandler.post(new PluginButtonManager(ch));
}
}
/**
* This runnable is intended to be run in UI thread (so in handler).
*/
private class PluginButtonManager implements Runnable {
CallHandlerPlugin ch;
PluginButtonManager(CallHandlerPlugin callHandler) {
ch = callHandler;
}
@Override
public void run() {
MenuItem item = mMenuBuilder.add(R.id.menu_accbtn_accounts, Menu.NONE, Menu.NONE, ch.getLabel().toString());
item.setIcon(ch.getIconDrawable());
item.setOnMenuItemClickListener(new OnAccountMenuItemListener(ch.getFakeProfile()));
}
}
/**
* Set the currently selected account for this widget
* It will change internal state,
* Change icon and label of the account
* @param aAccount
*/
public void setAccount(SipProfile aAccount) {
account = aAccount;
if (account == null) {
if(isInEditMode() || Compatibility.canMakeGSMCall(getContext())) {
textView.setText(getResources().getString(R.string.gsm));
imageView.setImageResource(R.drawable.ic_wizard_gsm);
}else {
textView.setText(getResources().getString(R.string.acct_inactive));
imageView.setImageResource(android.R.drawable.ic_dialog_alert);
}
} else {
textView.setText(account.display_name);
imageView.setImageDrawable(new BitmapDrawable(getResources(), WizardUtils.getWizardBitmap(getContext(),
account)));
}
if (onAccountChange != null) {
onAccountChange.onChooseAccount(account);
}
}
/**
* Update user interface when registration of account has changed
* This include change selected account if we are in canChangeIfValid mode
*/
private void updateRegistration() {
Cursor c = getContext().getContentResolver().query(SipProfile.ACCOUNT_URI, ACC_PROJECTION, SipProfile.FIELD_ACTIVE + "=?", new String[] {
"1"
}, null);
SipProfile toSelectAcc = null;
SipProfile firstAvail = null;
if (c != null) {
try {
if (c.getCount() > 0 && c.moveToFirst()) {
do {
final SipProfile acc = new SipProfile(c);
AccountStatusDisplay accountStatusDisplay = AccountListUtils
.getAccountDisplay(getContext(), acc.id);
if (accountStatusDisplay.availableForCalls) {
if (firstAvail == null) {
firstAvail = acc;
}
if (canChangeIfValid) {
// We can change even if valid, so select this
// account if valid for outgoings
if(targetAccountId != null) {
// Check if this is the target one
if(targetAccountId == acc.id) {
toSelectAcc = acc;
break;
}
}else {
// Select first
toSelectAcc = acc;
break;
}
} else if (account != null && account.id == acc.id) {
// Current is valid
toSelectAcc = acc;
break;
}
}
} while (c.moveToNext());
}
} catch (Exception e) {
Log.e(THIS_FILE, "Error on looping over sip profiles", e);
} finally {
c.close();
}
}
if (toSelectAcc == null) {
// Nothing to force select, fallback to first avail
toSelectAcc = firstAvail;
}
// Finally, set the account to be valid
setAccount(toSelectAcc);
}
/**
* Retrieve account that is currently selected by this widget
* @return The SipProfile selected
*/
public SipProfile getSelectedAccount() {
if (account == null) {
SipProfile retAcc = new SipProfile();
if(showExternals) {
Map<String, String> handlers = CallHandlerPlugin.getAvailableCallHandlers(getContext());
boolean includeGsm = Compatibility.canMakeGSMCall(getContext());
if(includeGsm) {
for (String callHandler : handlers.keySet()) {
// Try to prefer the GSM handler
if (callHandler.equalsIgnoreCase(telCmp.flattenToString())) {
retAcc.id = CallHandlerPlugin.getAccountIdForCallHandler(getContext(), callHandler);
return retAcc;
}
}
}
// Fast way to get first if exists
for (String callHandler : handlers.values()) {
// Ignore tel handler if we do not include gsm in settings
if(callHandler.equals(telCmp.flattenToString()) && !includeGsm) {
continue;
}
retAcc.id = CallHandlerPlugin.getAccountIdForCallHandler(getContext(), callHandler);
return retAcc;
}
}
retAcc.id = SipProfile.INVALID_ID;
return retAcc;
}
return account;
}
/**
* Attach listened to the widget that will fire when account selection change
* @param anAccountChangeListener the listener
*/
public void setOnAccountChangeListener(OnAccountChangeListener anAccountChangeListener) {
onAccountChange = anAccountChangeListener;
}
/**
* Set whether this button should consider plugins account as selectable accounts
* @param b true if you want the widget to show external accounts
*/
public void setShowExternals(boolean b) {
showExternals = b;
}
/* (non-Javadoc)
* @see com.actionbarsherlock.internal.view.View_HasStateListenerSupport#addOnAttachStateChangeListener(com.actionbarsherlock.internal.view.View_OnAttachStateChangeListener)
*/
@Override
public void addOnAttachStateChangeListener(View_OnAttachStateChangeListener listener) {
mListeners.add(listener);
}
/* (non-Javadoc)
* @see com.actionbarsherlock.internal.view.View_HasStateListenerSupport#removeOnAttachStateChangeListener(com.actionbarsherlock.internal.view.View_OnAttachStateChangeListener)
*/
@Override
public void removeOnAttachStateChangeListener(View_OnAttachStateChangeListener listener) {
mListeners.remove(listener);
}
private class OnAccountMenuItemListener implements OnMenuItemClickListener {
private SipProfile mAccount;
OnAccountMenuItemListener(SipProfile account){
mAccount = account;
}
/* (non-Javadoc)
* @see com.actionbarsherlock.view.MenuItem.OnMenuItemClickListener#onMenuItemClick(com.actionbarsherlock.view.MenuItem)
*/
@Override
public boolean onMenuItemClick(MenuItem item) {
setAccount(mAccount);
return true;
}
}
public MenuItem addExtraMenuItem(int titleRes) {
return mMenuBuilder.add(R.id.menu_accbtn_extras, MenuBuilder.NONE, 100, titleRes);
}
}