| /* |
| * Copyright (c) 2015, Motorola Mobility LLC |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * - Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * - Neither the name of Motorola Mobility nor the |
| * names of its contributors may be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MOTOROLA MOBILITY LLC BE LIABLE |
| * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS |
| * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
| * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
| * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH |
| * DAMAGE. |
| */ |
| |
| package com.android.service.ims.presence; |
| |
| import android.content.Context; |
| import android.telephony.SubscriptionManager; |
| import android.telephony.TelephonyManager; |
| import android.telephony.ims.RcsContactUceCapability; |
| import android.text.TextUtils; |
| |
| import com.android.ims.ResultCode; |
| import com.android.ims.internal.ContactNumberUtils; |
| import com.android.ims.internal.Logger; |
| import com.android.service.ims.RcsSettingUtils; |
| import com.android.service.ims.Task; |
| import com.android.service.ims.TaskManager; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public class PresenceSubscriber extends PresenceBase { |
| private Logger logger = Logger.getLogger(this.getClass().getName()); |
| |
| private SubscribePublisher mSubscriber; |
| private final Object mSubscriberLock = new Object(); |
| |
| private String mAvailabilityRetryNumber = null; |
| private int mAssociatedSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| |
| private final String[] mConfigVolteProvisionErrorOnSubscribeResponse; |
| private final String[] mConfigRcsProvisionErrorOnSubscribeResponse; |
| |
| /* |
| * Constructor |
| */ |
| public PresenceSubscriber(SubscribePublisher subscriber, Context context, |
| String[] configVolteProvisionErrorOnSubscribeResponse, |
| String[] configRcsProvisionErrorOnSubscribeResponse){ |
| super(context); |
| synchronized(mSubscriberLock) { |
| this.mSubscriber = subscriber; |
| } |
| mConfigVolteProvisionErrorOnSubscribeResponse |
| = configVolteProvisionErrorOnSubscribeResponse; |
| mConfigRcsProvisionErrorOnSubscribeResponse = configRcsProvisionErrorOnSubscribeResponse; |
| } |
| |
| public void updatePresenceSubscriber(SubscribePublisher subscriber) { |
| synchronized(mSubscriberLock) { |
| logger.print("Update PresencePublisher"); |
| this.mSubscriber = subscriber; |
| } |
| } |
| |
| public void removePresenceSubscriber() { |
| synchronized(mSubscriberLock) { |
| logger.print("Remove PresencePublisher:"); |
| this.mSubscriber = null; |
| } |
| } |
| |
| public void handleAssociatedSubscriptionChanged(int newSubId) { |
| if (mAssociatedSubscription == newSubId) { |
| return; |
| } |
| mAssociatedSubscription = newSubId; |
| } |
| |
| private String numberToUriString(String number) { |
| String formattedContact = number; |
| TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); |
| if (tm != null && !formattedContact.startsWith("sip:") |
| && !formattedContact.startsWith("tel:")){ |
| String domain = tm.getIsimDomain(); |
| logger.debug("domain=" + domain); |
| if (domain == null || domain.length() == 0){ |
| formattedContact = "tel:" + formattedContact; |
| } else { |
| formattedContact = "sip:" + formattedContact + "@" + domain; |
| } |
| } |
| |
| logger.print("numberToUriString formattedContact=" + formattedContact); |
| return formattedContact; |
| } |
| |
| private String numberToTelString(String number){ |
| String formatedContact = number; |
| if(!formatedContact.startsWith("sip:") && !formatedContact.startsWith("tel:")){ |
| formatedContact = "tel:" + formatedContact; |
| } |
| |
| logger.print("numberToTelString formatedContact=" + formatedContact); |
| return formatedContact; |
| } |
| |
| public int requestCapability(List<String> contactsNumber, |
| ContactCapabilityResponse listener) { |
| |
| SubscribePublisher subscriber = null; |
| synchronized(mSubscriberLock) { |
| subscriber = mSubscriber; |
| } |
| |
| if (subscriber == null) { |
| logger.error("requestCapability Subscribe not registered"); |
| return ResultCode.SUBSCRIBE_NOT_REGISTERED; |
| } |
| |
| int ret = subscriber.getStackStatusForCapabilityRequest(); |
| if (ret < ResultCode.SUCCESS) { |
| logger.error("requestCapability ret=" + ret); |
| return ret; |
| } |
| |
| if(contactsNumber == null || contactsNumber.size() ==0){ |
| ret = ResultCode.SUBSCRIBE_INVALID_PARAM; |
| return ret; |
| } |
| |
| logger.debug("check contact size ..."); |
| if (contactsNumber.size() > RcsSettingUtils.getMaxNumbersInRCL(mAssociatedSubscription)) { |
| ret = ResultCode.SUBSCRIBE_TOO_LARGE; |
| logger.error("requestCapability contctNumber size=" + contactsNumber.size()); |
| return ret; |
| } |
| |
| String[] formatedNumbers = ContactNumberUtils.getDefault().format(contactsNumber); |
| int formatResult = ContactNumberUtils.getDefault().validate(formatedNumbers); |
| if (formatResult != ContactNumberUtils.NUMBER_VALID) { |
| logger.error("requestCapability formatResult=" + formatResult); |
| return ResultCode.SUBSCRIBE_INVALID_PARAM; |
| } |
| |
| String[] formatedContacts = new String[formatedNumbers.length]; |
| for(int i=0; i<formatedContacts.length; i++){ |
| formatedContacts[i] = numberToTelString(formatedNumbers[i]); |
| } |
| // In ms |
| long timeout = RcsSettingUtils.getCapabPollListSubExp(mAssociatedSubscription) * 1000; |
| timeout += RcsSettingUtils.getSIPT1Timer(mAssociatedSubscription); |
| |
| // The terminal notification may be received shortly after the time limit of |
| // the subscription due to network delays or retransmissions. |
| // Device shall wait for 3sec after the end of the subscription period in order to |
| // accept such notifications without returning spurious errors (e.g. SIP 481) |
| timeout += 3000; |
| |
| logger.print("add to task manager, formatedNumbers=" + |
| PresenceUtils.toContactString(formatedNumbers)); |
| int taskId = TaskManager.getDefault().addCapabilityTask(mContext, formatedNumbers, |
| listener, timeout); |
| logger.print("taskId=" + taskId); |
| |
| ret = subscriber.requestCapability(formatedContacts, taskId); |
| if(ret < ResultCode.SUCCESS) { |
| logger.error("requestCapability ret=" + ret + " remove taskId=" + taskId); |
| TaskManager.getDefault().removeTask(taskId); |
| } |
| |
| ret = taskId; |
| |
| return ret; |
| } |
| |
| public int requestAvailability(String contactNumber, ContactCapabilityResponse listener, |
| boolean forceToNetwork) { |
| |
| String formatedContact = ContactNumberUtils.getDefault().format(contactNumber); |
| int ret = ContactNumberUtils.getDefault().validate(formatedContact); |
| if(ret != ContactNumberUtils.NUMBER_VALID){ |
| return ret; |
| } |
| |
| if(!forceToNetwork){ |
| logger.debug("check if we can use the value in cache"); |
| int availabilityExpire = |
| RcsSettingUtils.getAvailabilityCacheExpiration(mAssociatedSubscription); |
| availabilityExpire = availabilityExpire>0?availabilityExpire*1000: |
| 60*1000; // by default is 60s |
| logger.print("requestAvailability availabilityExpire=" + availabilityExpire); |
| |
| TaskManager.getDefault().clearTimeoutAvailabilityTask(availabilityExpire); |
| |
| Task task = TaskManager.getDefault().getAvailabilityTaskByContact(formatedContact); |
| if(task != null && task instanceof PresenceAvailabilityTask) { |
| PresenceAvailabilityTask availabilityTask = (PresenceAvailabilityTask)task; |
| if(availabilityTask.getNotifyTimestamp() == 0) { |
| // The previous one didn't get response yet. |
| logger.print("requestAvailability: the request is pending in queue"); |
| return ResultCode.SUBSCRIBE_ALREADY_IN_QUEUE; |
| }else { |
| // not expire yet. Can use the previous value. |
| logger.print("requestAvailability: the prevous valuedoesn't be expired yet"); |
| return ResultCode.SUBSCRIBE_TOO_FREQUENTLY; |
| } |
| } |
| } |
| |
| // Only poll/fetch capability/availability on LTE |
| TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); |
| if(tm == null || (tm.getDataNetworkType() != TelephonyManager.NETWORK_TYPE_LTE)) { |
| logger.error("requestAvailability return ERROR_SERVICE_NOT_AVAILABLE" + |
| " for it is not LTE network"); |
| return ResultCode.ERROR_SERVICE_NOT_AVAILABLE; |
| } |
| |
| SubscribePublisher subscriber = null; |
| synchronized(mSubscriberLock) { |
| subscriber = mSubscriber; |
| } |
| |
| if (subscriber == null) { |
| logger.error("requestAvailability Subscribe not registered"); |
| return ResultCode.SUBSCRIBE_NOT_REGISTERED; |
| } |
| |
| ret = subscriber.getStackStatusForCapabilityRequest(); |
| if (ret < ResultCode.SUCCESS) { |
| logger.error("requestAvailability=" + ret); |
| return ret; |
| } |
| |
| // user number format in TaskManager. |
| int taskId = TaskManager.getDefault().addAvailabilityTask(formatedContact, listener); |
| |
| // Change it to URI format. |
| formatedContact = numberToUriString(formatedContact); |
| |
| logger.print("addAvailabilityTask formatedContact=" + formatedContact); |
| |
| ret = subscriber.requestAvailability(formatedContact, taskId); |
| if (ret < ResultCode.SUCCESS) { |
| logger.error("requestAvailability ret=" + ret + " remove taskId=" + taskId); |
| TaskManager.getDefault().removeTask(taskId); |
| } |
| |
| ret = taskId; |
| |
| return ret; |
| } |
| |
| private int translateResponse403(String reasonPhrase){ |
| if(reasonPhrase == null){ |
| // No retry. The PS provisioning has not occurred correctly. UX Decision to show errror. |
| return ResultCode.SUBSCRIBE_GENIRIC_FAILURE; |
| } |
| |
| reasonPhrase = reasonPhrase.toLowerCase(); |
| if(reasonPhrase.contains("user not registered")){ |
| // Register to IMS then retry the single resource subscription if capability polling. |
| // availability fetch: no retry. ignore the availability and allow LVC? (PLM decision) |
| return ResultCode.SUBSCRIBE_NOT_REGISTERED; |
| } |
| |
| if(reasonPhrase.contains("not authorized for presence")){ |
| // No retry. |
| return ResultCode.SUBSCRIBE_NOT_AUTHORIZED_FOR_PRESENCE; |
| } |
| |
| // unknown phrase: handle it as the same as no phrase |
| return ResultCode.SUBSCRIBE_FORBIDDEN; |
| } |
| |
| private int translateResponseCode(int responseCode, String reasonPhrase) { |
| // pSipResponse should not be null. |
| logger.debug("translateResponseCode getSipResponseCode=" +responseCode); |
| int ret = ResultCode.SUBSCRIBE_GENIRIC_FAILURE; |
| |
| if(responseCode < 100 || responseCode > 699){ |
| logger.debug("internal error code sipCode=" + responseCode); |
| ret = ResultCode.SUBSCRIBE_TEMPORARY_ERROR; //it is internal issue. ignore it. |
| return ret; |
| } |
| |
| switch(responseCode){ |
| case 200: |
| ret = ResultCode.SUCCESS; |
| break; |
| |
| case 403: |
| ret = translateResponse403(reasonPhrase); |
| break; |
| |
| case 404: |
| // Target MDN is not provisioned for VoLTE or it is not known as VzW IMS subscriber |
| // Device shall not retry. Device shall remove the VoLTE status of the target MDN |
| // and update UI |
| ret = ResultCode.SUBSCRIBE_NOT_FOUND; |
| break; |
| |
| case 408: |
| // Request Timeout |
| // Device shall retry with exponential back-off |
| ret = ResultCode.SUBSCRIBE_TEMPORARY_ERROR; |
| break; |
| |
| case 413: |
| // Too Large. |
| // Application need shrink the size of request contact list and resend the request |
| ret = ResultCode.SUBSCRIBE_TOO_LARGE; |
| break; |
| |
| case 423: |
| // Interval Too Short. Requested expiry interval too short and server rejects it |
| // Device shall re-attempt subscription after changing the expiration interval in |
| // the Expires header field to be equal to or greater than the expiration interval |
| // within the Min-Expires header field of the 423 response |
| ret = ResultCode.SUBSCRIBE_TEMPORARY_ERROR; |
| break; |
| |
| case 500: |
| // 500 Server Internal Error |
| // capability polling: exponential back-off retry (same rule as resource list) |
| // availability fetch: no retry. ignore the availability and allow LVC |
| // (PLM decision) |
| ret = ResultCode.SUBSCRIBE_TEMPORARY_ERROR; |
| break; |
| |
| case 503: |
| // capability polling: exponential back-off retry (same rule as resource list) |
| // availability fetch: no retry. ignore the availability and allow LVC? |
| // (PLM decision) |
| ret = ResultCode.SUBSCRIBE_TEMPORARY_ERROR; |
| break; |
| |
| // capability polling: Device shall retry with exponential back-off |
| // Availability Fetch: device shall ignore the error and shall not retry |
| case 603: |
| ret = ResultCode.SUBSCRIBE_TEMPORARY_ERROR; |
| break; |
| |
| default: |
| // Other 4xx/5xx/6xx |
| // Device shall not retry |
| ret = ResultCode.SUBSCRIBE_GENIRIC_FAILURE; |
| } |
| |
| logger.debug("translateResponseCode ret=" + ret); |
| return ret; |
| } |
| |
| public void onSipResponse(int requestId, int responseCode, String reasonPhrase) { |
| SubscribePublisher subscriber = null; |
| synchronized(mSubscriberLock) { |
| subscriber = mSubscriber; |
| } |
| |
| if(isInConfigList(responseCode, reasonPhrase, |
| mConfigVolteProvisionErrorOnSubscribeResponse)) { |
| logger.print("volte provision sipCode=" + responseCode + " phrase=" + reasonPhrase); |
| if (subscriber != null) { |
| subscriber.updatePublisherState(PUBLISH_STATE_VOLTE_PROVISION_ERROR); |
| } |
| |
| notifyDm(); |
| } else if(isInConfigList(responseCode, reasonPhrase, |
| mConfigRcsProvisionErrorOnSubscribeResponse)) { |
| logger.print("rcs proRcsPresence.vision sipCode=" + responseCode + " phrase=" |
| + reasonPhrase); |
| if (subscriber != null) { |
| subscriber.updatePublisherState(PUBLISH_STATE_RCS_PROVISION_ERROR); |
| } |
| } |
| |
| int errorCode = translateResponseCode(responseCode, reasonPhrase); |
| logger.print("handleSipResponse errorCode=" + errorCode); |
| |
| if(errorCode == ResultCode.SUBSCRIBE_NOT_REGISTERED){ |
| logger.debug("setPublishState to unknown for subscribe error 403 not registered"); |
| if (subscriber != null) { |
| subscriber.updatePublisherState(PUBLISH_STATE_OTHER_ERROR); |
| } |
| } |
| |
| if(errorCode == ResultCode.SUBSCRIBE_NOT_AUTHORIZED_FOR_PRESENCE) { |
| logger.debug("ResultCode.SUBSCRIBE_NOT_AUTHORIZED_FOR_PRESENCE"); |
| } |
| |
| if(errorCode == ResultCode.SUBSCRIBE_FORBIDDEN){ |
| logger.debug("ResultCode.SUBSCRIBE_FORBIDDEN"); |
| } |
| |
| // Suppose the request ID had been set when IQPresListener_CMDStatus |
| Task task = TaskManager.getDefault().getTaskByRequestId(requestId); |
| logger.debug("handleSipResponse task=" + task); |
| if(task != null){ |
| task.mSipResponseCode = responseCode; |
| task.mSipReasonPhrase = reasonPhrase; |
| TaskManager.getDefault().putTask(task.mTaskId, task); |
| } |
| |
| if(errorCode == ResultCode.SUBSCRIBE_NOT_REGISTERED && |
| task != null && task.mCmdId == TaskManager.TASK_TYPE_GET_AVAILABILITY) { |
| String[] contacts = ((PresenceTask)task).mContacts; |
| if(contacts != null && contacts.length>0){ |
| mAvailabilityRetryNumber = contacts[0]; |
| } |
| logger.debug("retry to get availability for " + mAvailabilityRetryNumber); |
| } |
| |
| // 404 error for single contact only as per requirement |
| // need handle 404 for multiple contacts as per CV 3.24. |
| if(errorCode == ResultCode.SUBSCRIBE_NOT_FOUND && |
| task != null && ((PresenceTask)task).mContacts != null) { |
| String[] contacts = ((PresenceTask)task).mContacts; |
| ArrayList<RcsContactUceCapability> contactCapabilities = new ArrayList<>(); |
| |
| for(int i=0; i<contacts.length; i++){ |
| if(TextUtils.isEmpty(contacts[i])){ |
| continue; |
| } |
| logger.debug("onSipResponse: contact= " + contacts[i] + ", not found."); |
| // Build contacts with no capabilities. |
| contactCapabilities.add(new RcsContactUceCapability.Builder( |
| PresenceUtils.convertContactNumber(contacts[i])).build()); |
| } |
| handleCapabilityUpdate(task, contactCapabilities, true); |
| |
| } else if(errorCode == ResultCode.SUBSCRIBE_GENIRIC_FAILURE) { |
| updateAvailabilityToUnknown(task); |
| } |
| |
| handleCallback(task, errorCode, false); |
| } |
| |
| private void handleCapabilityUpdate(Task task, List<RcsContactUceCapability> capabilities, |
| boolean updateLastTimestamp) { |
| if (task == null || task.mListener == null ) { |
| logger.warn("handleCapabilityUpdate, invalid listener!"); |
| return; |
| } |
| task.mListener.onCapabilitiesUpdated(task.mTaskId, capabilities, updateLastTimestamp); |
| } |
| |
| public void retryToGetAvailability() { |
| if(mAvailabilityRetryNumber == null){ |
| return; |
| } |
| requestAvailability(mAvailabilityRetryNumber, null, true); |
| //retry one time only |
| mAvailabilityRetryNumber = null; |
| } |
| |
| public void updatePresence(RcsContactUceCapability capabilities) { |
| if(mContext == null){ |
| logger.error("updatePresence mContext == null"); |
| return; |
| } |
| |
| ArrayList<RcsContactUceCapability> presenceInfos = new ArrayList<>(); |
| presenceInfos.add(capabilities); |
| |
| String contactNumber = capabilities.getContactUri().getSchemeSpecificPart(); |
| // For single contact number we got 1 NOTIFY only. So regard it as terminated. |
| TaskManager.getDefault().onTerminated(contactNumber); |
| |
| PresenceAvailabilityTask availabilityTask = TaskManager.getDefault(). |
| getAvailabilityTaskByContact(contactNumber); |
| if (availabilityTask != null) { |
| availabilityTask.updateNotifyTimestamp(); |
| } |
| Task task = TaskManager.getDefault().getTaskForSingleContactQuery(contactNumber); |
| handleCapabilityUpdate(task, presenceInfos, true); |
| } |
| |
| public void updatePresences(int requestId, List<RcsContactUceCapability> contactsCapabilities, |
| boolean isTerminated, String terminatedReason) { |
| if(mContext == null){ |
| logger.error("updatePresences: mContext == null"); |
| return; |
| } |
| |
| if (isTerminated) { |
| TaskManager.getDefault().onTerminated(requestId, terminatedReason); |
| } |
| |
| Task task = TaskManager.getDefault().getTaskByRequestId(requestId); |
| if (contactsCapabilities.size() > 0 || task != null) { |
| handleCapabilityUpdate(task, contactsCapabilities, true); |
| } |
| } |
| |
| public void onCommandStatusUpdated(int taskId, int requestId, int resultCode) { |
| Task taskTmp = TaskManager.getDefault().getTask(taskId); |
| logger.print("handleCmdStatus resultCode=" + resultCode); |
| PresenceTask task = null; |
| if(taskTmp != null && (taskTmp instanceof PresenceTask)){ |
| task = (PresenceTask)taskTmp; |
| task.mSipRequestId = requestId; |
| task.mCmdStatus = resultCode; |
| TaskManager.getDefault().putTask(task.mTaskId, task); |
| |
| // handle error as the same as temporary network error |
| // set availability to false, keep old capability |
| if(resultCode != ResultCode.SUCCESS && task.mContacts != null){ |
| updateAvailabilityToUnknown(task); |
| } |
| } |
| |
| handleCallback(task, resultCode, true); |
| } |
| |
| private void updateAvailabilityToUnknown(Task inTask){ |
| //only used for serviceState is offline or unknown. |
| if(mContext == null){ |
| logger.error("updateAvailabilityToUnknown mContext=null"); |
| return; |
| } |
| |
| if(inTask == null){ |
| logger.error("updateAvailabilityToUnknown task=null"); |
| return; |
| } |
| |
| if(!(inTask instanceof PresenceTask)){ |
| logger.error("updateAvailabilityToUnknown not PresencTask"); |
| return; |
| } |
| |
| PresenceTask task = (PresenceTask)inTask; |
| |
| if(task.mContacts == null || task.mContacts.length ==0){ |
| logger.error("updateAvailabilityToUnknown no contacts"); |
| return; |
| } |
| |
| ArrayList<RcsContactUceCapability> presenceInfoList = new ArrayList<>(); |
| for (int i = 0; i< task.mContacts.length; i++) { |
| if(TextUtils.isEmpty(task.mContacts[i])){ |
| continue; |
| } |
| // Add each contacts with no capabilities. |
| presenceInfoList.add(new RcsContactUceCapability.Builder( |
| PresenceUtils.convertContactNumber(task.mContacts[i])).build()); |
| } |
| |
| if(presenceInfoList.size() > 0) { |
| handleCapabilityUpdate(task, presenceInfoList, false); |
| } |
| } |
| } |
| |