/** * 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.utils; import android.Manifest.permission; import android.app.Activity; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.text.TextUtils; import com.csipsimple.api.SipManager; import com.csipsimple.api.SipProfile; import java.util.HashMap; import java.util.List; import java.util.Map; public class CallHandlerPlugin { private static final String THIS_FILE = "CallHandlerPlugin"; public static final String EXTRA_REMOTE_INTENT_TOKEN = "android.intent.extra.remote_intent_token"; private OnLoadListener listener; private PendingIntent pendingIntent = null; private Bitmap icon = null; private String nextExclude = null; private String label = null; private long accountId = SipProfile.INVALID_ID; private final Context context; private static Map<String, String> AVAILABLE_HANDLERS = null; private final static String VIRTUAL_ACC_MAX_ENTRIES = "maxVirtualAcc"; private final static String VIRTUAL_ACC_PREFIX = "vAcc_"; public CallHandlerPlugin(Context ctxt) { context = ctxt; } private static Handler sThreadHandler = null; /** * Load plugin from a given plugin component name * @param componentName Fully qualified component name to call * @param number Optional number to call * @param l Listener to fire on load completion */ public void loadFrom(final String componentName, String number, OnLoadListener l) { listener = l; ComponentName cn = ComponentName.unflattenFromString(componentName); Intent it = new Intent(SipManager.ACTION_GET_PHONE_HANDLERS); it.putExtra(Intent.EXTRA_PHONE_NUMBER, number); it.setComponent(cn); context.sendOrderedBroadcast(it, permission.PROCESS_OUTGOING_CALLS, new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Bundle resolvedInfos = getResultExtras(true); fillWith(componentName, resolvedInfos); if (listener != null) { listener.onLoad(CallHandlerPlugin.this); } } }, sThreadHandler, Activity.RESULT_OK, null, null); } /** * Load plugin from a given account id. * @param accountId Fake (< -1) account id to load plugin from * @param number Optional number to call * @param l Listener to fire on load completion */ public void loadFrom(final Long accountId, String number, OnLoadListener l) { Map<String, String> callHandlers = getAvailableCallHandlers(context); for (String packageName : callHandlers.keySet()) { if (accountId == getAccountIdForCallHandler(context, packageName)) { loadFrom(packageName, number, l); return; } } } public void fillWith(String packageName, Bundle resolvedInfos) { pendingIntent = (PendingIntent) resolvedInfos.getParcelable(EXTRA_REMOTE_INTENT_TOKEN); icon = (Bitmap) resolvedInfos.getParcelable(Intent.EXTRA_SHORTCUT_ICON); nextExclude = resolvedInfos.getString(Intent.EXTRA_PHONE_NUMBER); label = resolvedInfos.getString(Intent.EXTRA_TITLE); if(TextUtils.isEmpty(label)) { if(AVAILABLE_HANDLERS != null && AVAILABLE_HANDLERS.containsKey(packageName)) { label = AVAILABLE_HANDLERS.get(packageName); } } accountId = getAccountIdForCallHandler(context, packageName); } /** * Retrieve internal id of call handler as saved in databases It should be * some negative < SipProfile.INVALID_ID number * * @param ctxt Application context * @param packageName name of the call handler package * @return the id of this call handler in databases */ public static Long getAccountIdForCallHandler(Context ctxt, String packageName) { SharedPreferences prefs = ctxt.getSharedPreferences("handlerCache", Context.MODE_PRIVATE); long accountId = SipProfile.INVALID_ID; try { accountId = prefs.getLong(VIRTUAL_ACC_PREFIX + packageName, SipProfile.INVALID_ID); } catch (Exception e) { Log.e(THIS_FILE, "Can't retrieve call handler cache id - reset"); } if (accountId == SipProfile.INVALID_ID) { // We never seen this one, add a new entry for account id int maxAcc = prefs.getInt(VIRTUAL_ACC_MAX_ENTRIES, 0x0); int currentEntry = maxAcc + 1; accountId = SipProfile.INVALID_ID - (long) currentEntry; Editor edt = prefs.edit(); edt.putLong(VIRTUAL_ACC_PREFIX + packageName, accountId); edt.putInt(VIRTUAL_ACC_MAX_ENTRIES, currentEntry); edt.commit(); } return accountId; } /** * Retrieve outgoing call handlers available as plugin for csipsimple Also * contains stock call handler if available * * @param ctxt context of application * @return A map of package name => Fancy name of call handler */ public static Map<String, String> getAvailableCallHandlers(Context ctxt) { if (AVAILABLE_HANDLERS == null) { AVAILABLE_HANDLERS = new HashMap<String, String>(); PackageManager packageManager = ctxt.getPackageManager(); Intent it = new Intent(SipManager.ACTION_GET_PHONE_HANDLERS); List<ResolveInfo> availables = packageManager.queryBroadcastReceivers(it, 0); for (ResolveInfo resInfo : availables) { ActivityInfo actInfos = resInfo.activityInfo; Log.d(THIS_FILE, "Found call handler " + actInfos.packageName + " " + actInfos.name); if (packageManager.checkPermission(permission.PROCESS_OUTGOING_CALLS, actInfos.packageName) == PackageManager.PERMISSION_GRANTED) { String packagedActivityName = (new ComponentName(actInfos.packageName, actInfos.name)).flattenToString(); AVAILABLE_HANDLERS.put(packagedActivityName, (String) resInfo.loadLabel(packageManager)); } } } return AVAILABLE_HANDLERS; } /** * Reset cache of outgoing call handlers */ public static void clearAvailableCallHandlers() { AVAILABLE_HANDLERS = null; } /** * Interface for listener about load state of remote call handler plugin */ public interface OnLoadListener { /** * Fired when call handler has been loaded * * @param ch the call handler object that has been loaded */ void onLoad(CallHandlerPlugin ch); } /** * Get the display label name for this call handler * * @return A string to display to represent this call handler */ public String getLabel() { return label; } /** * Get the icon bitmap for this call handler * * @return the bitmap icon representing the call handler */ public Bitmap getIcon() { return icon; } /** * Get the icon drawable for this call handler * * @return the drawable icon representing the call handler */ public Drawable getIconDrawable() { if (icon != null) { //return new BitmapDrawable(icon); return new BitmapDrawable(context.getResources(), icon); } return null; } /** * The pending intent to fire when user select this call handler This is * only populated once loadFromXXX has been launched and called back * * @return the intent to fire */ public PendingIntent getIntent() { return pendingIntent; } /** * The number to exclude from treatment in next run of outgoing call if any. * This is useful if the call handler also launch a make call intent to * ignore this intent from processing This is only populated once * loadFromXXX has been launched and called back * * @return The phone number to ignore or null if none to ignore */ public String getNextExcludeTelNumber() { return nextExclude; } /** * Get the call id as stored in db for this call handler Should be < * SipProfile.INVALID_ID * * @return the sip account id */ public long getAccountId() { return accountId; } /** * Build a fake sip profile object for this plugin call handler It will * contain id for this callhandler, display name and icon * * @return the SipProfile equivalent object for this CallHandler */ public SipProfile getFakeProfile() { SipProfile profile = new SipProfile(); profile.id = accountId; profile.display_name = label; profile.icon = icon; return profile; } public static void initHandler() { if(sThreadHandler == null) { HandlerThread thread = new HandlerThread("CallHandlerPluginWorker"); thread.start(); sThreadHandler = new Handler(thread.getLooper()); } } }