/** * 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.ui.account; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.provider.BaseColumns; import android.support.v4.content.AsyncTaskLoader; import android.text.TextUtils; import com.csipsimple.api.SipProfile; import com.csipsimple.models.Filter; 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.Log; import com.csipsimple.utils.PreferencesProviderWrapper; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class AccountsLoader extends AsyncTaskLoader<Cursor> { public static final String FIELD_FORCE_CALL = "force_call"; public static final String FIELD_NBR_TO_CALL = "nbr_to_call"; public static final String FIELD_STATUS_OUTGOING = "status_for_outgoing"; public static final String FIELD_STATUS_COLOR = "status_color"; private static final String THIS_FILE = "OutgoingAccountsLoader"; private Cursor currentResult; private final String numberToCall; private final boolean ignoreRewritting; private final boolean loadStatus; private final boolean onlyActive; private final boolean loadCallHandlerPlugins; /** * Constructor for loader for outgoing call context. <br/> * This one will care of rewriting number and keep track of accounts status. * @param context Your app context * @param number Phone number for outgoing call * @param ignoreRewrittingRules Should we ignore rewriting rules. */ public AccountsLoader(Context context, String number, boolean ignoreRewrittingRules) { super(context); numberToCall = number; ignoreRewritting = ignoreRewrittingRules; loadStatus = true; onlyActive = true; loadCallHandlerPlugins = true; initHandlers(); } public AccountsLoader(Context context, boolean onlyActiveAccounts, boolean withCallHandlerPlugins) { super(context); numberToCall = ""; ignoreRewritting = true; loadStatus = false; onlyActive = onlyActiveAccounts; loadCallHandlerPlugins = withCallHandlerPlugins; initHandlers(); } private void initHandlers() { CallHandlerPlugin.initHandler(); loaderObserver = new ForceLoadContentObserver(); } private ContentObserver loaderObserver; private ArrayList<FilteredProfile> finalAccounts; @Override public Cursor loadInBackground() { // First register for status updates if(loadStatus) { getContext().getContentResolver().registerContentObserver(SipProfile.ACCOUNT_STATUS_URI, true, loaderObserver); } ArrayList<FilteredProfile> prefinalAccounts = new ArrayList<FilteredProfile>(); // Get all sip profiles ArrayList<SipProfile> accounts; PreferencesProviderWrapper prefsWrapper = new PreferencesProviderWrapper(getContext()); if(onlyActive && !prefsWrapper.isValidConnectionForOutgoing() ) { accounts = new ArrayList<SipProfile>(); }else { accounts = SipProfile.getAllProfiles(getContext(), onlyActive, new String[] { SipProfile.FIELD_ID, SipProfile.FIELD_ACC_ID, SipProfile.FIELD_ACTIVE, SipProfile.FIELD_DISPLAY_NAME, SipProfile.FIELD_WIZARD }); } // And all external call handlers Map<String, String> externalHandlers; if(loadCallHandlerPlugins) { externalHandlers = CallHandlerPlugin.getAvailableCallHandlers(getContext()); }else { externalHandlers = new HashMap<String, String>(); } if(TextUtils.isEmpty(numberToCall)) { // In case of empty number to call, just add everything without any other question for(SipProfile acc : accounts) { prefinalAccounts.add(new FilteredProfile(acc, false)); } for(Entry<String, String> extEnt : externalHandlers.entrySet() ) { prefinalAccounts.add(new FilteredProfile(extEnt.getKey(), false)); } }else { // If there is a number to call, add only those callable, and flag must call entries // Note that we keep processing all call handlers voluntarily cause we may encounter a sip account that doesn't register // But is in force call mode for(SipProfile acc : accounts) { if(Filter.isCallableNumber(getContext(), acc.id, numberToCall)) { boolean forceCall = Filter.isMustCallNumber(getContext(), acc.id, numberToCall); prefinalAccounts.add(new FilteredProfile(acc, forceCall)); } } for(Entry<String, String> extEnt : externalHandlers.entrySet() ) { long accId = CallHandlerPlugin.getAccountIdForCallHandler(getContext(), extEnt.getKey()); if(Filter.isCallableNumber(getContext(), accId, numberToCall)) { boolean forceCall = Filter.isMustCallNumber(getContext(), accId, numberToCall); prefinalAccounts.add(new FilteredProfile(extEnt.getKey(), forceCall)); if(forceCall) { break; } } } } // Build final cursor based on final filtered accounts Cursor[] cursorsToMerge = new Cursor[prefinalAccounts.size()]; int i = 0; for (FilteredProfile acc : prefinalAccounts) { cursorsToMerge[i++] = createCursorForAccount(acc); } if(cursorsToMerge.length > 0) { MergeCursor mg = new MergeCursor(cursorsToMerge); mg.registerContentObserver(loaderObserver); finalAccounts = prefinalAccounts; return mg; }else { finalAccounts = prefinalAccounts; return null; } } /** * Class to hold information about a possible call handler entry. * This could be either a sip profile or a call handler plugin */ private class FilteredProfile { /** * Sip profile constructor. * To use when input is a sip profile * @param acc The corresponding sip profile * @param forceCall The force call flag in current context. */ public FilteredProfile(SipProfile acc, boolean forceCall) { account = acc; isForceCall = forceCall; AccountStatusDisplay displayState = AccountListUtils.getAccountDisplay(getContext(), acc.id); statusColor = displayState.statusColor; statusForOutgoing = displayState.availableForCalls; callHandlerPlugin = null; } /** * Call handler plugin constructor. * To use when input is a call handler plugin. * @param componentName The component name of the plugin * @param forceCall The force call flag in current context. */ public FilteredProfile(String componentName, boolean forceCall) { account = new SipProfile(); long accId = CallHandlerPlugin.getAccountIdForCallHandler(getContext(), componentName); account.id = accId; account.wizard = "EXPERT"; CallHandlerPlugin ch = new CallHandlerPlugin(getContext()); final Semaphore semaphore = new Semaphore(0); String toCall = numberToCall; if(!ignoreRewritting) { toCall = Filter.rewritePhoneNumber(getContext(), accId, numberToCall); } ch.loadFrom(accId, toCall, new OnLoadListener() { @Override public void onLoad(CallHandlerPlugin ch) { Log.d(THIS_FILE, "Callhandler loaded"); semaphore.release(); } }); boolean succeedInLoading = false; try { succeedInLoading = semaphore.tryAcquire(3L, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e(THIS_FILE, "Not possible to bind callhandler plugin"); } if(!succeedInLoading) { Log.e(THIS_FILE, "Unreachable callhandler plugin " + componentName); } account.display_name = ch.getLabel(); account.icon = ch.getIcon(); isForceCall = forceCall; statusColor = getContext().getResources().getColor(android.R.color.white); statusForOutgoing = true; callHandlerPlugin = ch; } final SipProfile account; final boolean isForceCall; final private boolean statusForOutgoing; final private int statusColor; final CallHandlerPlugin callHandlerPlugin; /** * Rewrite a number for this calling entry * @param number The number to rewrite * @return Rewritten number. */ public String rewriteNumber(String number) { if(ignoreRewritting) { return number; }else { return Filter.rewritePhoneNumber(getContext(), account.id, number); } } /** * Is the account available for outgoing calls * @return True if a call can be made using this calling entry */ public boolean getStatusForOutgoing() { return statusForOutgoing; } /** * The color representing the calling entry status. green for registered * sip accounts, red for invalid sip accounts, orange for sip accounts * with ongoing registration, white for call handler plugins * * @return the color for this entry status. */ public int getStatusColor() { return statusColor; } /** * Get the eventual associated call handler plugin object. * * @return The call handler plugin object if any associated to this * calling entry. Null if representing a sip account. */ public CallHandlerPlugin getCallHandlerPlugin() { return callHandlerPlugin; } } /** * Called when there is new data to deliver to the client. The super class * will take care of delivering it; the implementation here just adds a * little more logic. */ @Override public void deliverResult(Cursor c) { if (isReset()) { // An async query came in while the loader is stopped. We // don't need the result. if (currentResult != null) { onReleaseResources(currentResult); } } currentResult = c; if (isStarted()) { // If the Loader is currently started, we can immediately // deliver its results. super.deliverResult(c); } } /** * Handles a request to start the Loader. */ @Override protected void onStartLoading() { if (currentResult != null && !takeContentChanged()) { // If we currently have a result available, deliver it // immediately. deliverResult(currentResult); }else { forceLoad(); } } /** * Handles a request to stop the Loader. */ @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); } /** * Handles a request to cancel a load. */ @Override public void onCanceled(Cursor c) { super.onCanceled(c); // At this point we can release the resources associated with 'apps' // if needed. onReleaseResources(c); } /** * Handles a request to completely reset the Loader. */ @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); // At this point we can release the resources associated with 'apps' // if needed. if (currentResult != null) { onReleaseResources(currentResult); currentResult = null; } } /** * Helper function to take care of releasing resources associated with an * actively loaded data set. */ protected void onReleaseResources(Cursor c) { if(c != null) { c.unregisterContentObserver(loaderObserver); c.close(); } if(loadStatus) { getContext().getContentResolver().unregisterContentObserver(loaderObserver); } } private static String[] COLUMN_HEADERS = new String[] { BaseColumns._ID, SipProfile.FIELD_ID, SipProfile.FIELD_DISPLAY_NAME, SipProfile.FIELD_WIZARD, FIELD_FORCE_CALL, FIELD_NBR_TO_CALL, FIELD_STATUS_OUTGOING, FIELD_STATUS_COLOR }; /** * Creates a cursor that contains a single row and maps the section to the * given value. */ private Cursor createCursorForAccount(FilteredProfile fa) { MatrixCursor matrixCursor = new MatrixCursor(COLUMN_HEADERS); matrixCursor.addRow(new Object[] { fa.account.id, fa.account.id, fa.account.display_name, fa.account.wizard, fa.isForceCall ? 1 : 0, fa.rewriteNumber(numberToCall), fa.getStatusForOutgoing() ? 1 : 0, fa.getStatusColor() }); return matrixCursor; } /** * Get the cached call handler plugin loaded for a given position * @param position The position to search at * @return The call handler plugin if any for this position */ public CallHandlerPlugin getCallHandlerWithAccountId(long accId) { for(FilteredProfile filteredAcc :finalAccounts) { if(filteredAcc.account.id == accId) return filteredAcc.getCallHandlerPlugin(); } return null; } }