/*
* Copyright 2014 OpenMarket Ltd
*
* 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 org.matrix.androidsdk.util;
import android.text.TextUtils;
import org.matrix.androidsdk.util.Log;
import org.matrix.androidsdk.MXDataHandler;
import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.data.MyUser;
import org.matrix.androidsdk.data.Room;
import org.matrix.androidsdk.listeners.IMXNetworkEventListener;
import org.matrix.androidsdk.network.NetworkConnectivityReceiver;
import org.matrix.androidsdk.rest.callback.ApiCallback;
import org.matrix.androidsdk.rest.callback.SimpleApiCallback;
import org.matrix.androidsdk.rest.client.BingRulesRestClient;
import org.matrix.androidsdk.rest.model.Event;
import org.matrix.androidsdk.rest.model.MatrixError;
import org.matrix.androidsdk.rest.model.Message;
import org.matrix.androidsdk.rest.model.bingrules.BingRule;
import org.matrix.androidsdk.rest.model.bingrules.BingRuleSet;
import org.matrix.androidsdk.rest.model.bingrules.BingRulesResponse;
import org.matrix.androidsdk.rest.model.bingrules.Condition;
import org.matrix.androidsdk.rest.model.bingrules.ContainsDisplayNameCondition;
import org.matrix.androidsdk.rest.model.bingrules.ContentRule;
import org.matrix.androidsdk.rest.model.bingrules.EventMatchCondition;
import org.matrix.androidsdk.rest.model.bingrules.RoomMemberCountCondition;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* Object that gets and processes bing rules from the server.
*/
public class BingRulesManager {
private static final String LOG_TAG = "BingRulesManager";
/**
* Bing rule listener
*/
public interface onBingRuleUpdateListener {
/**
* The manager succeeds to update the bingrule enable status.
*/
void onBingRuleUpdateSuccess();
/**
* The manager fails to update the bingrule enable status.
* @param errorMessage the error message.
*/
void onBingRuleUpdateFailure(String errorMessage);
}
// general members
private BingRulesRestClient mApiClient;
private MXSession mSession;
private String mMyUserId;
private MXDataHandler mDataHandler;
// the rules set to apply
private BingRuleSet mRulesSet = new BingRuleSet();
// the rules list
private List<BingRule> mRules = new ArrayList<>();
// the default bing rule
private BingRule mDefaultBingRule = new BingRule(true);
// tell if the bing rules set is initialized
private boolean mIsInitialized = false;
// network management
private NetworkConnectivityReceiver mNetworkConnectivityReceiver;
private IMXNetworkEventListener mNetworkListener;
private ApiCallback<Void> mLoadRulesCallback;
/**
* Constructor
* @param session the session
* @param networkConnectivityReceiver the network events listener
*/
public BingRulesManager(MXSession session, NetworkConnectivityReceiver networkConnectivityReceiver) {
mSession = session;
mApiClient = session.getBingRulesApiClient();
mMyUserId = session.getCredentials().userId;
mDataHandler = session.getDataHandler();
mNetworkListener = new IMXNetworkEventListener() {
@Override
public void onNetworkConnectionUpdate(boolean isConnected) {
// mLoadRulesCallback is set when a loadRules failed
// so when a network is available, trigger again loadRules
if (isConnected && (null != mLoadRulesCallback)) {
loadRules(mLoadRulesCallback);
}
}
};
mNetworkConnectivityReceiver = networkConnectivityReceiver;
networkConnectivityReceiver.addEventListener(mNetworkListener);
}
/**
* @return true if it is ready to be used (i.e initializedà
*/
public boolean isReady() {
return mIsInitialized;
}
/**
* Remove the network events listener.
* This listener is only used to initialize the rules at application launch.
*/
private void removeNetworkListener() {
if ((null != mNetworkConnectivityReceiver) && (null != mNetworkListener)) {
mNetworkConnectivityReceiver.removeEventListener(mNetworkListener);
mNetworkConnectivityReceiver = null;
mNetworkListener = null;
}
}
/**
* Load the bing rules from the server.
* @param callback an async callback called when the rules are loaded
*/
public void loadRules(final ApiCallback<Void> callback) {
mLoadRulesCallback = null;
mApiClient.getAllBingRules(new ApiCallback<BingRulesResponse>() {
@Override
public void onSuccess(BingRulesResponse info) {
buildRules(info);
mIsInitialized = true;
if (callback != null) {
callback.onSuccess(null);
}
removeNetworkListener();
}
private void onError() {
// the callback will be called when the request will succeed
mLoadRulesCallback = callback;
}
@Override
public void onNetworkError(Exception e) {
onError();
}
@Override
public void onMatrixError(MatrixError e) {
onError();
}
@Override
public void onUnexpectedError(Exception e) {
onError();
}
});
}
/**
* Update the rule enable status.
* @param kind the rule kind.
* @param ruleId the rule ID.
* @param status the new enable status.
* @param callback an async callback.
*/
public void updateEnableRuleStatus(String kind, String ruleId, boolean status, final ApiCallback<Void> callback) {
mApiClient.updateEnableRuleStatus(kind, ruleId, status, callback);
}
/**
* Returns whether a string contains an occurrence of another, as a standalone word, regardless of case.
* @param subString the string to search for
* @param longString the string to search in
* @return whether a match was found
*/
private static boolean caseInsensitiveFind(String subString, String longString) {
// sanity check
if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) {
return false;
}
boolean found = false;
try {
Pattern pattern = Pattern.compile("(\\W|^)" + subString + "(\\W|$)", Pattern.CASE_INSENSITIVE);
found = pattern.matcher(longString).find();
} catch (Exception e) {
Log.e(LOG_TAG, "caseInsensitiveFind : pattern.matcher failed with " + e.getLocalizedMessage());
}
return found;
}
/**
* Returns the first notifiable bing rule which fulfills its condition with this event.
* @param event the event
* @return the first matched bing rule, null if none
*/
public BingRule fulfilledBingRule(Event event) {
// sanity check
if (null == event) {
return null;
}
if (!mIsInitialized) {
return null;
}
// do not trigger notification for oneself messages
if ((null != event.getSender()) && TextUtils.equals(event.getSender(), mMyUserId)) {
return null;
}
if (mRules != null) {
// GA issue
final ArrayList<BingRule> rules;
synchronized (this) {
rules = new ArrayList<>(mRules);
}
// Go down the rule list until we find a match
for (BingRule bingRule : rules) {
if (bingRule.isEnabled) {
boolean isFullfilled = false;
// some rules have no condition
// so their ruleId defines the method
if (BingRule.RULE_ID_CONTAIN_USER_NAME.equals(bingRule.ruleId) || BingRule.RULE_ID_CONTAIN_DISPLAY_NAME.equals(bingRule.ruleId)) {
if (Event.EVENT_TYPE_MESSAGE.equals(event.getType())) {
Message message = JsonUtils.toMessage(event.getContent());
MyUser myUser = mSession.getMyUser();
String pattern = myUser.displayname;
if (BingRule.RULE_ID_CONTAIN_USER_NAME.equals(bingRule.ruleId)) {
if (mMyUserId.indexOf(":") >= 0) {
pattern = mMyUserId.substring(1, mMyUserId.indexOf(":"));
} else {
pattern = mMyUserId;
}
}
if (!TextUtils.isEmpty(pattern)) {
isFullfilled = caseInsensitiveFind(pattern, message.body);
}
}
} else if (BingRule.RULE_ID_FALLBACK.equals(bingRule.ruleId)) {
isFullfilled = true;
} else {
// some default rules define conditions
// so use them instead of doing a custom treatment
// RULE_ID_ONE_TO_ONE_ROOM
// RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS
isFullfilled = eventMatchesConditions(event, bingRule.conditions);
}
if (isFullfilled) {
return bingRule;
}
}
}
// no rules are fulfilled
return null;
} else {
// The default is to bing
return mDefaultBingRule;
}
}
/**
* Check if an event matches a conditions set
* @param event the evnt to test
* @param conditions the conditions set
* @return true if the event matches all the conditions set.
*/
private boolean eventMatchesConditions(Event event, List<Condition> conditions) {
try {
if ((conditions != null) && (event != null)) {
for (Condition condition : conditions) {
if (condition instanceof EventMatchCondition) {
if (!((EventMatchCondition) condition).isSatisfied(event)) {
return false;
}
} else if (condition instanceof ContainsDisplayNameCondition) {
if (event.roomId != null) {
Room room = mDataHandler.getRoom(event.roomId, false);
// sanity checks
if ((null != room) && (null != room.getMember(mMyUserId))) {
// Best way to get your display name for now
String myDisplayName = room.getMember(mMyUserId).displayname;
if (!((ContainsDisplayNameCondition) condition).isSatisfied(event, myDisplayName)) {
return false;
}
}
}
} else if (condition instanceof RoomMemberCountCondition) {
if (event.roomId != null) {
Room room = mDataHandler.getRoom(event.roomId, false);
if (!((RoomMemberCountCondition) condition).isSatisfied(room)) {
return false;
}
}
}
// FIXME: Handle device rules
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "## eventMatchesConditions() failed " + e.getMessage());
return false;
}
return true;
}
/**
* Build the internal build rules
* @param bingRulesResponse the server request response.
*/
public void buildRules(BingRulesResponse bingRulesResponse) {
if (null != bingRulesResponse) {
updateRules(bingRulesResponse.global);
}
}
/**
* @return the rules set
*/
public BingRuleSet pushRules() {
return mRulesSet;
}
/**
* Update mRulesSet with the new one.
* @param ruleSet the new ruleSet to apply
*/
private void updateRules(BingRuleSet ruleSet) {
synchronized (this) {
// clear the rules list
// it is
mRules.clear();
// sanity check
if (null == ruleSet) {
mRulesSet = new BingRuleSet();
return;
}
// Replace the list by ArrayList to be able to add/remove rules
// Add the rule kind in each rule
// Ensure that the null pointers are replaced by an empty list
if (ruleSet.override != null) {
ruleSet.override = new ArrayList<>(ruleSet.override);
for (BingRule rule : ruleSet.override) {
rule.kind = BingRule.KIND_OVERRIDE;
}
mRules.addAll(ruleSet.override);
} else {
ruleSet.override = new ArrayList<>(ruleSet.override);
}
if (ruleSet.content != null) {
ruleSet.content = new ArrayList<>(ruleSet.content);
for (BingRule rule : ruleSet.content) {
rule.kind = BingRule.KIND_CONTENT;
}
addContentRules(ruleSet.content);
} else {
ruleSet.content = new ArrayList<>();
}
if (ruleSet.room != null) {
ruleSet.room = new ArrayList<>(ruleSet.room);
for (BingRule rule : ruleSet.room) {
rule.kind = BingRule.KIND_ROOM;
}
addRoomRules(ruleSet.room);
} else {
ruleSet.room = new ArrayList<>();
}
if (ruleSet.sender != null) {
ruleSet.sender = new ArrayList<>(ruleSet.sender);
for (BingRule rule : ruleSet.sender) {
rule.kind = BingRule.KIND_SENDER;
}
addSenderRules(ruleSet.sender);
} else {
ruleSet.sender = new ArrayList<>();
}
if (ruleSet.underride != null) {
ruleSet.underride = new ArrayList<>(ruleSet.underride);
for (BingRule rule : ruleSet.underride) {
rule.kind = BingRule.KIND_UNDERRIDE;
}
mRules.addAll(ruleSet.underride);
} else {
ruleSet.underride = new ArrayList<>();
}
mRulesSet = ruleSet;
}
}
/**
* Create a content EventMatchConditions list from a ContentRules list
* @param rules the ContentRules list
*/
private void addContentRules(List<ContentRule> rules) {
// sanity check
if (null != rules) {
for (ContentRule rule : rules) {
EventMatchCondition condition = new EventMatchCondition();
condition.kind = Condition.KIND_EVENT_MATCH;
condition.key = "content.body";
condition.pattern = rule.pattern;
rule.addCondition(condition);
mRules.add(rule);
}
}
}
/**
* Create a room EventMatchConditions list from a BingRule list
* @param rules the BingRule list
*/
private void addRoomRules(List<BingRule> rules) {
if (null != rules) {
for (BingRule rule : rules) {
EventMatchCondition condition = new EventMatchCondition();
condition.kind = Condition.KIND_EVENT_MATCH;
condition.key = "room_id";
condition.pattern = rule.ruleId;
rule.addCondition(condition);
mRules.add(rule);
}
}
}
/**
* Create a sender EventMatchConditions list from a BingRule list
* @param rules the BingRule list
*/
private void addSenderRules(List<BingRule> rules) {
if (null != rules) {
for (BingRule rule : rules) {
EventMatchCondition condition = new EventMatchCondition();
condition.kind = Condition.KIND_EVENT_MATCH;
condition.key = "user_id";
condition.pattern = rule.ruleId;
rule.addCondition(condition);
mRules.add(rule);
}
}
}
/**
* Toogle a rule.
* @param rule the bing rule to toggle.
* @param listener the rule update listener.
* @return the matched bing rule or null it doesn't exist.
*/
public BingRule toggleRule(final BingRule rule, final onBingRuleUpdateListener listener) {
if (null != rule) {
updateEnableRuleStatus(rule.kind, rule.ruleId, !rule.isEnabled, new SimpleApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
rule.isEnabled = !rule.isEnabled;
updateRules(mRulesSet);
if (listener != null) {
try {
listener.onBingRuleUpdateSuccess();
} catch (Exception e) {
Log.e(LOG_TAG, "## toggleRule : onBingRuleUpdateSuccess failed " + e.getMessage());
}
}
}
private void onError(String message) {
if (null != listener) {
try {
listener.onBingRuleUpdateFailure(message);
} catch (Exception e) {
Log.e(LOG_TAG, "## onError : onBingRuleUpdateFailure failed " + e.getMessage());
}
}
}
/**
* Called if there is a network error.
* @param e the exception
*/
@Override
public void onNetworkError(Exception e) {
onError(e.getLocalizedMessage());
}
/**
* Called in case of a Matrix error.
* @param e the Matrix error
*/
@Override
public void onMatrixError(MatrixError e) {
onError(e.getLocalizedMessage());
}
/**
* Called for some other type of error.
* @param e the exception
*/
@Override
public void onUnexpectedError(Exception e) {
onError(e.getLocalizedMessage());
}
});
}
return rule;
}
/**
* Delete the rule.
* @param rule the rule to delete.
* @param listener the rule update listener.
*/
public void deleteRule(final BingRule rule, final onBingRuleUpdateListener listener) {
// null case
if (null == rule) {
if (listener != null) {
try {
listener.onBingRuleUpdateSuccess();
} catch (Exception e) {
Log.e(LOG_TAG, "## deleteRule : onBingRuleUpdateSuccess failed " + e.getMessage());
}
}
return;
}
mApiClient.deleteRule(rule.kind, rule.ruleId, new SimpleApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
if (null != mRulesSet) {
mRulesSet.remove(rule);
updateRules(mRulesSet);
}
if (listener != null) {
try {
listener.onBingRuleUpdateSuccess();
} catch (Exception e) {
Log.e(LOG_TAG, "## deleteRule : onBingRuleUpdateSuccess failed " + e.getMessage());
}
}
}
private void onError(String message) {
if (null != listener) {
try {
listener.onBingRuleUpdateFailure(message);
} catch (Exception e) {
Log.e(LOG_TAG, "## onError : onBingRuleUpdateFailure failed " + e.getMessage());
}
}
}
/**
* Called if there is a network error.
* @param e the exception
*/
@Override
public void onNetworkError(Exception e) {
onError(e.getLocalizedMessage());
}
/**
* Called in case of a Matrix error.
* @param e the Matrix error
*/
@Override
public void onMatrixError(MatrixError e) {
onError(e.getLocalizedMessage());
}
/**
* Called for some other type of error.
* @param e the exception
*/
@Override
public void onUnexpectedError(Exception e) {
onError(e.getLocalizedMessage());
}
});
}
/**
* Delete a rules list.
* @param rules the rules to delete
* @param listener the listener when the rules are deleted
*/
public void deleteRules(final List<BingRule> rules, final onBingRuleUpdateListener listener) {
deleteRules(rules, 0, listener);
}
/**
* Recursive rules deletion method.
* @param rules the rules to delete
* @param index the rule index
* @param listener the listener when the rules are deleted
*/
private void deleteRules(final List<BingRule> rules, final int index, final onBingRuleUpdateListener listener) {
// sanity checks
if ((null == rules) || (index >= rules.size())) {
if (null != listener) {
try {
listener.onBingRuleUpdateSuccess();
} catch (Exception e) {
Log.e(LOG_TAG, "## deleteRules() : onBingRuleUpdateSuccess failed " + e.getMessage());
}
}
return;
}
// delete the rule
deleteRule(rules.get(index), new onBingRuleUpdateListener() {
@Override
public void onBingRuleUpdateSuccess() {
deleteRules(rules, index+1, listener);
}
@Override
public void onBingRuleUpdateFailure(String errorMessage) {
if (null != listener) {
try {
listener.onBingRuleUpdateFailure(errorMessage);
} catch (Exception e) {
Log.e(LOG_TAG, "## deleteRules() : onBingRuleUpdateFailure failed " + e.getMessage());
}
}
}
});
}
/**
* Add a rule.
* @param rule the rule to delete.
* @param listener the rule update listener.
*/
public void addRule(final BingRule rule, final onBingRuleUpdateListener listener) {
// null case
if (null == rule) {
if (listener != null) {
try {
listener.onBingRuleUpdateSuccess();
} catch (Exception e) {
Log.e(LOG_TAG, "## addRule : onBingRuleUpdateSuccess failed " + e.getMessage());
}
}
return;
}
mApiClient.addRule(rule, new SimpleApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
if (null != mRulesSet) {
mRulesSet.addAtTop(rule);
updateRules(mRulesSet);
}
if (listener != null) {
try {
listener.onBingRuleUpdateSuccess();
} catch (Exception e) {
Log.e(LOG_TAG, "## addRule : onBingRuleUpdateSuccess failed " + e.getMessage());
}
}
}
private void onError(String message) {
if (null != listener) {
try {
listener.onBingRuleUpdateFailure(message);
} catch (Exception e) {
Log.e(LOG_TAG, "## addRule : onBingRuleUpdateFailure failed " + e.getMessage());
}
}
}
/**
* Called if there is a network error.
* @param e the exception
*/
@Override
public void onNetworkError(Exception e) {
onError(e.getLocalizedMessage());
}
/**
* Called in case of a Matrix error.
* @param e the Matrix error
*/
@Override
public void onMatrixError(MatrixError e) {
onError(e.getLocalizedMessage());
}
/**
* Called for some other type of error.
* @param e the exception
*/
@Override
public void onUnexpectedError(Exception e) {
onError(e.getLocalizedMessage());
}
});
}
/**
* Search the pushrules for the room
* @param room the room
* @return the room rules list
*/
public ArrayList<BingRule> getPushRulesForRoom(Room room) {
ArrayList<BingRule> rules = new ArrayList<>();
// sanity checks
if ((null != room) && (null != mRulesSet)) {
// the webclient defines two ways to set a room rule
// mention only : the user won't have any push for the room except if a content rule is fullfilled
// mute : no notification for this room
// mute rules are defined in override groups
if (null != mRulesSet.override) {
for (BingRule roomRule : mRulesSet.override) {
if (TextUtils.equals(roomRule.ruleId, room.getRoomId())) {
rules.add(roomRule);
}
}
}
// mention only are defined in room group
if (null != mRulesSet.room) {
for (BingRule roomRule : mRulesSet.room) {
if (TextUtils.equals(roomRule.ruleId, room.getRoomId())) {
rules.add(roomRule);
}
}
}
}
return rules;
}
/**
* Test if the room has a dedicated rule which disables notification.
* @return true if there is a rule to disable notifications.
*/
public boolean isRoomNotificationsDisabled(Room room) {
ArrayList<BingRule> roomRules = getPushRulesForRoom(room);
if (0 != roomRules.size()) {
for(BingRule rule : roomRules) {
if (!rule.shouldNotify() && rule.isEnabled) {
return true;
}
}
}
return false;
}
/**
* Mute / unmute the room notifications.
* Only the room rules are checked.
*
* @param room the room to mute / unmute.
* @param isMuted set to true to mute the notification
* @param listener the listener.
*/
public void muteRoomNotifications(final Room room, final boolean isMuted, final onBingRuleUpdateListener listener) {
ArrayList<BingRule> bingRules = getPushRulesForRoom(room);
// the mobile client only supports to define a "mention only" rule i.e a rule defined in the room rules set.
// delete the rule and create a new one
deleteRules(bingRules, new onBingRuleUpdateListener() {
@Override
public void onBingRuleUpdateSuccess() {
if (isMuted) {
addRule(new BingRule(BingRule.KIND_ROOM, room.getRoomId(), false, false, false), listener);
} else if (null != listener) {
try {
listener.onBingRuleUpdateSuccess();
} catch (Exception e) {
Log.e(LOG_TAG, "## muteRoomNotifications() : onBingRuleUpdateSuccess failed " + e.getMessage());
}
}
}
@Override
public void onBingRuleUpdateFailure(String errorMessage) {
if (null != listener) {
try {
listener.onBingRuleUpdateFailure(errorMessage);
} catch (Exception e) {
Log.e(LOG_TAG, "## muteRoomNotifications() : onBingRuleUpdateFailure failed " + e.getMessage());
}
}
}
});
}
}