All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.leanplum.ActionContext Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2022, Leanplum, Inc. All rights reserved.
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 com.leanplum;

import androidx.annotation.NonNull;
import android.text.TextUtils;

import com.leanplum.actions.LeanplumActions;
import com.leanplum.actions.internal.ActionDidExecute;
import com.leanplum.actions.internal.ActionManagerDefinitionKt;
import com.leanplum.actions.internal.ActionManagerTriggeringKt;
import com.leanplum.actions.internal.ActionDidDismiss;
import com.leanplum.actions.internal.Priority;
import com.leanplum.internal.ActionManager;
import com.leanplum.internal.BaseActionContext;
import com.leanplum.internal.CollectionUtil;
import com.leanplum.internal.Constants;
import com.leanplum.internal.FileManager;
import com.leanplum.internal.JsonConverter;
import com.leanplum.internal.LeanplumInternal;
import com.leanplum.internal.Log;
import com.leanplum.internal.OperationQueue;
import com.leanplum.internal.VarCache;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The context in which an action or message is executed.
 *
 * @author Andrew First
 */
public class ActionContext extends BaseActionContext implements Comparable {
  private final String name;
  private ActionContext parentContext;
  private final int contentVersion;
  private String key;
  private boolean preventRealtimeUpdating = false;
  private ContextualValues contextualValues;
  private ActionDidDismiss actionDidDismiss;
  private ActionDidExecute actionDidExecute;
  private boolean isChainedMessage;

  public static class ContextualValues {
    /**
     * Parameters from the triggering event or state.
     */
    public Map parameters;

    /**
     * Arguments from the triggering event or state.
     */
    public Map arguments;

    /**
     * The previous user attribute value.
     */
    public Object previousAttributeValue;

    /**
     * The current user attribute value.
     */
    public Object attributeValue;
  }

  public ActionContext(String name, Map args, String messageId) {
    this(name, args, messageId, null, Constants.Messaging.DEFAULT_PRIORITY);
  }

  public ActionContext(String name, Map args, final String messageId,
      final String originalMessageId, int priority) {
    super(messageId, originalMessageId);
    this.name = name;
    this.args = args;
    this.contentVersion = VarCache.contentVersion();
    this.priority = priority;
  }

  public void preventRealtimeUpdating() {
    preventRealtimeUpdating = true;
  }

  public void setContextualValues(ContextualValues values) {
    contextualValues = values;
  }

  public ContextualValues getContextualValues() {
    return contextualValues;
  }

  private Map getDefinition(String actionName) {
    Map definition =
        ActionManagerDefinitionKt.getActionDefinitionMap(ActionManager.getInstance(), actionName);
    if (definition == null) {
      return new HashMap<>();
    }
    return definition;
  }

  private Map defaultValues() {
    Map values = CollectionUtil.uncheckedCast(getDefinition(name).get("values"));
    if (values == null) {
      return new HashMap<>();
    }
    return values;
  }

  private Map kinds() {
    Map kinds = CollectionUtil.uncheckedCast(getDefinition(name).get("kinds"));
    if (kinds == null) {
      return new HashMap<>();
    }
    return kinds;
  }

  public void update() {
    this.updateArgs(args, "", defaultValues());
  }

  @SuppressWarnings("unchecked")
  private void updateArgs(Map args,
      String prefix, Map defaultValues) {
    Map kinds = kinds();
    for (Map.Entry entry : args.entrySet()) {
      String arg = entry.getKey();
      Object value = entry.getValue();
      Object defaultValue = defaultValues != null ? defaultValues.get(arg) : null;
      String kind = kinds.get(prefix + arg);
      if ((kind == null || !kind.equals(Constants.Kinds.ACTION)) && value instanceof Map &&
          !((Map) value).containsKey(Constants.Values.ACTION_ARG)) {
        Map defaultValueMap = (defaultValue instanceof Map) ?
            (Map) defaultValue : null;
        this.updateArgs((Map) value, prefix + arg + ".", defaultValueMap);
      } else {
        if (kind != null && kind.equals(Constants.Kinds.FILE) ||
            arg.contains(Constants.Values.FILE_PREFIX)) {
          FileManager.maybeDownloadFile(false, value.toString(),
              defaultValue != null ? defaultValue.toString() : null, null, null);

          // Need to check for null because server actions like push notifications aren't
          // defined in the SDK, and so there's no associated metadata.
        } else if (kind == null || kind.equals(Constants.Kinds.ACTION)) {
          Object actionArgsObj = objectNamed(prefix + arg);
          if (!(actionArgsObj instanceof Map)) {
            continue;
          }
          Map actionArgs = (Map) actionArgsObj;
          ActionContext context = new ActionContext(
              (String) actionArgs.get(Constants.Values.ACTION_ARG),
              actionArgs, messageId);
          context.update();
        }
      }
    }
  }

  public String actionName() {
    return name;
  }

  public  T objectNamed(String name) {
    if (TextUtils.isEmpty(name)) {
      Log.e("objectNamed - Invalid name parameter provided.");
      return null;
    }
    try {
      if (!preventRealtimeUpdating && VarCache.contentVersion() > contentVersion) {
        ActionContext parent = parentContext;
        if (parent != null) {
          args = parent.getChildArgs(key);
        } else if (messageId != null) {
          // This is sort of a best effort to display the most recent version of the message, if
          // this happens to be null, it probably means that it got changed somehow in between the
          // time when it was activated and displayed (e.g. by forceContentUpdate), in which case
          // we just ignore it and display the latest stable version.
          Map message = CollectionUtil.uncheckedCast(VarCache.messages().get
              (messageId));
          if (message != null) {
            args = CollectionUtil.uncheckedCast(message.get(Constants.Keys.VARS));
          }
        }
      }
      return VarCache.getMergedValueFromComponentArray(
          VarCache.getNameComponents(name), args);
    } catch (Throwable t) {
      Log.exception(t);
      return null;
    }
  }

  public String stringNamed(String name) {
    if (TextUtils.isEmpty(name)) {
      Log.e("stringNamed - Invalid name parameter provided.");
      return null;
    }
    Object object = objectNamed(name);
    if (object == null) {
      return null;
    }
    try {
      return fillTemplate(object.toString());
    } catch (Throwable t) {
      Log.exception(t);
      return object.toString();
    }
  }

  public String fillTemplate(String value) {
    if (contextualValues == null || value == null || !value.contains("##")) {
      return value;
    }
    if (contextualValues.parameters != null) {
      Map parameters = contextualValues.parameters;
      for (Map.Entry entry : parameters.entrySet()) {
        String placeholder = "##Parameter " + entry.getKey() + "##";
        value = value.replace(placeholder, "" + entry.getValue());
      }
    }
    if (contextualValues.previousAttributeValue != null) {
      value = value.replace("##Previous Value##",
          contextualValues.previousAttributeValue.toString());
    }
    if (contextualValues.attributeValue != null) {
      value = value.replace("##Value##", contextualValues.attributeValue.toString());
    }
    return value;
  }

  private String getDefaultValue(String name) {
    String[] components = name.split("\\.");
    Map defaultValues = defaultValues();
    for (int i = 0; i < components.length; i++) {
      if (defaultValues == null) {
        return null;
      }
      if (i == components.length - 1) {
        Object value = defaultValues.get(components[i]);
        return value == null ? null : value.toString();
      }
      defaultValues = CollectionUtil.uncheckedCast(defaultValues.get(components[i]));
    }
    return null;
  }

  public InputStream streamNamed(String name) {
    try {
      if (TextUtils.isEmpty(name)) {
        Log.e("streamNamed - Invalid name parameter provided.");
        return null;
      }
      String stringValue = stringNamed(name);
      String defaultValue = getDefaultValue(name);
      if ((stringValue == null || stringValue.length() == 0) &&
          (defaultValue == null || defaultValue.length() == 0)) {
        return null;
      }
      InputStream stream = FileManager.stream(false, null, null,
          FileManager.fileValue(stringValue, defaultValue, null), defaultValue, null);
      if (stream == null) {
        Log.e("Could not open stream named " + name);
      }
      return stream;
    } catch (Throwable t) {
      Log.exception(t);
      return null;
    }
  }

  public boolean booleanNamed(String name) {
    if (TextUtils.isEmpty(name)) {
      Log.e("booleanNamed - Invalid name parameter provided.");
      return false;
    }
    Object object = objectNamed(name);
    try {
      if (object == null) {
        return false;
      } else if (object instanceof Boolean) {
        return (Boolean) object;
      }
      return convertToBoolean(object.toString());
    } catch (Throwable t) {
      Log.exception(t);
      return false;
    }
  }

  /**
   * In contrast to Boolean.valueOf this function also converts 1, yes or similar string values
   * correctly to Boolean, e.g.: "1", "yes", "true", "on" --> true; "0", "no", "false", "off" -->
   * false; else null.
   *
   * @param value the text to convert to Boolean.
   * @return Boolean
   */
  private static boolean convertToBoolean(String value) {
    return "1".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value) ||
        "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value);
  }

  public Number numberNamed(String name) {
    if (TextUtils.isEmpty(name)) {
      Log.e("numberNamed - Invalid name parameter provided.");
      return null;
    }
    Object object = objectNamed(name);
    try {
      if (object == null || TextUtils.isEmpty(object.toString())) {
        return 0.0;
      }
      if (object instanceof Number) {
        return (Number) object;
      }
      return Double.valueOf(object.toString());
    } catch (Throwable t) {
      Log.exception(t);
      return 0.0;
    }
  }

  private Map getChildArgs(String name) {
    Object actionArgsObj = objectNamed(name);
    if (!(actionArgsObj instanceof Map)) {
      return null;
    }
    Map actionArgs = CollectionUtil.uncheckedCast(actionArgsObj);
    Map defaultArgs = CollectionUtil.uncheckedCast(getDefinition(
        (String) actionArgs.get(Constants.Values.ACTION_ARG)).get("values"));
    actionArgs = CollectionUtil.uncheckedCast(VarCache.mergeHelper(defaultArgs, actionArgs));
    return actionArgs;
  }

  public void runActionNamed(String name) {
    if (TextUtils.isEmpty(name)) {
      Log.e("runActionNamed - Invalid name parameter provided.");
      return;
    }
    Map args = getChildArgs(name);

    if (actionDidExecute != null) {
      ActionContext actionNamedContext = new ActionContext(name, args, messageId);
      actionNamedContext.parentContext = this;
      actionDidExecute.onActionExecuted(actionNamedContext);
    }

    if (args == null) {
      return;
    }

    performChainedAction(name, args);
  }

  private void performChainedAction(String name, Map args) {
    String chainedMessageId = getChainedMessageId(args);

    if (chainedMessageId != null) { // Chain to existing message
      if (shouldFetchChainedMessage(args)) {
        boolean previousPauseState = ActionManager.getInstance().isPaused();
        ActionManager.getInstance().setPaused(true);
        // Message doesn't seem to be on the device,
        // so let's forceContentUpdate and retry showing it.
        Leanplum.forceContentUpdate(success -> {
          if (success) {
            executeChainedMessage(chainedMessageId, name);
          }
          if (LeanplumActions.getUseWorkerThreadForDecisionHandlers()) {
            OperationQueue.sharedInstance().addActionOperation(() -> {
              OperationQueue.sharedInstance().addUiOperation(() -> {
                ActionManager.getInstance().setPaused(previousPauseState); // will resume queue
              });
            });
          } else {
            ActionManager.getInstance().setPaused(previousPauseState); // will resume queue
          }
        });
      } else {
        executeChainedMessage(chainedMessageId, name);
        // queue will be resumed from onDidDismiss callback in case executeChainedMessage fails
      }

    } else { // Chain to embedded message
      Object messageAction = args.get(Constants.Values.ACTION_ARG);
      if (messageAction != null) {
        ActionContext embeddedContext = createChainedContext(
            messageAction.toString(),
            args,
            messageId,
            name);
        ActionManagerTriggeringKt.trigger(
            ActionManager.getInstance(),
            embeddedContext,
            Priority.HIGH);
      }
    }
  }

  private ActionContext createChainedContext(
      String messageAction,
      Map messageArgs,
      String messageId,
      String name) {

      ActionContext actionContext = new ActionContext(messageAction, messageArgs, messageId);
      actionContext.contextualValues = contextualValues;
      actionContext.preventRealtimeUpdating = preventRealtimeUpdating;
      actionContext.isRooted = isRooted;
      actionContext.key = name;
      actionContext.parentContext = this;
      return actionContext;
  }

  public static boolean shouldFetchChainedMessage(
      @NonNull ActionContext context,
      String actionName) {
    if (TextUtils.isEmpty(actionName)) {
      return false;
    }
    Map args = context.getChildArgs(actionName);
    return shouldFetchChainedMessage(args);
  }

  /**
   * Return true if there is a chained message in the actionMap that is not yet loaded onto the device.
   */
  static boolean shouldFetchChainedMessage(Map actionMap) {
    if (actionMap == null) {
      return false;
    }
    String chainedMessageId = getChainedMessageId(actionMap);
    if (chainedMessageId == null) {
      return false;
    }
    return VarCache.messages() == null
        || !VarCache.messages().containsKey(chainedMessageId);
  }

  /**
   * Extract chained messageId from parent message's actionMap.  If it doesn't exist then return null.
   */
  static String getChainedMessageId(Map actionMap) {
    if (actionMap != null) {
      if (Constants.Values.CHAIN_MESSAGE_ACTION_NAME.equals(actionMap.get(Constants.Values.ACTION_ARG))) {
        return (String) actionMap.get(Constants.Values.CHAIN_MESSAGE_ARG);
      }
    }
    return null;
  }


  private boolean executeChainedMessage(String messageId, String name) {
    Map messages = VarCache.messages();
    if (messages == null) {
      return false;
    }
    Map message = CollectionUtil.uncheckedCast(messages.get(messageId));
    if (message != null) {
      Map messageArgs = CollectionUtil.uncheckedCast(
              message.get(Constants.Keys.VARS));
      Object messageAction = message.get("action");

      if (messageAction != null) {
        ActionContext chainContext = createChainedContext(
            messageAction.toString(),
            messageArgs,
            messageId,
            name);
        chainContext.isChainedMessage = true;
        ActionManagerTriggeringKt.trigger(
            ActionManager.getInstance(),
            chainContext,
            Priority.HIGH);
        return true;
      }
    }
    return false;
  }

  /**
   * Prefix given event with all parent actionContext names to while filtering out the string
   * "action" (used in ExperimentVariable names but filtered out from event names).
   *
   * @param eventName Current event.
   * @return Prefixed event name with all parent actions.
   */
  private String eventWithParentEventNames(String eventName) {
    StringBuilder fullEventName = new StringBuilder();
    ActionContext context = this;
    List parents = new ArrayList<>();
    while (context.parentContext != null) {
      parents.add(context);
      context = context.parentContext;
    }
    for (int i = parents.size() - 1; i >= -1; i--) {
      if (fullEventName.length() > 0) {
        fullEventName.append(' ');
      }
      String actionName;
      if (i > -1) {
        actionName = parents.get(i).key;
      } else {
        actionName = eventName;
      }
      if (actionName == null) {
        fullEventName = new StringBuilder("");
        break;
      }
      actionName = actionName.replace(" action", "");
      fullEventName.append(actionName);
    }

    return fullEventName.toString();
  }

  /**
   * Run the action with the given variable name, and track a message event with the name.
   *
   * @param name Action variable name to run.
   */
  public void runTrackedActionNamed(String name) {
    try {
      if (!Constants.isNoop() && messageId != null && isRooted) {
        if (TextUtils.isEmpty(name)) {
          Log.e("runTrackedActionNamed - Invalid name parameter provided.");
          return;
        }
        trackMessageEvent(name, 0.0, null, null);
      }
      runActionNamed(name);
    } catch (Throwable t) {
      Log.exception(t);
    }
  }

  /**
   * Track a message event with the given parameters. Any parent event names will be prepended to
   * given event name.
   *
   * @param event Name of event.
   * @param value Value for event.
   * @param info Info for event.
   * @param params Dictionary of params for event.
   */
  public void trackMessageEvent(String event, double value, String info,
      Map params) {
    try {
      if (!Constants.isNoop() && this.messageId != null) {
        if (TextUtils.isEmpty(event)) {
          Log.e("trackMessageEvent - Invalid event parameter provided.");
          return;
        }

        event = eventWithParentEventNames(event);
        if (TextUtils.isEmpty(event)) {
          Log.e("trackMessageEvent - Failed to generate parent action names.");
          return;
        }

        Map requestArgs = new HashMap<>();
        requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
        LeanplumInternal.track(event, value, info, params, requestArgs);
      }
    } catch (Throwable t) {
      Log.exception(t);
    }
  }

  public void track(String event, double value, Map params) {
    try {
      if (!Constants.isNoop() && this.messageId != null) {
        if (TextUtils.isEmpty(event)) {
          Log.e("track - Invalid event parameter provided.");
          return;
        }
        Map requestArgs = new HashMap<>();
        requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
        LeanplumInternal.track(event, value, null, params, requestArgs);
      }
    } catch (Throwable t) {
      Log.exception(t);
    }
  }

  public void muteFutureMessagesOfSameKind() {
    try {
      ActionManager.getInstance().muteFutureMessagesOfKind(messageId);
    } catch (Throwable t) {
      Log.exception(t);
    }
  }

  public int compareTo(@NonNull ActionContext other) {
    return priority - other.getPriority();
  }

  /**
   * Returns path to requested file.
   */
  public static String filePath(String stringValue) {
    return FileManager.fileValue(stringValue);
  }

  public static JSONObject mapToJsonObject(Map map) throws JSONException {
    return JsonConverter.mapToJsonObject(map);
  }

  public static  Map mapFromJson(JSONObject jsonObject) throws JSONException {
    return JsonConverter.mapFromJson(jsonObject);
  }

  public ActionContext getParentContext() {
    return parentContext;
  }

  public void setParentContext(ActionContext parentContext) {
    this.parentContext = parentContext;
  }

  public boolean isChainedMessage() {
    return isChainedMessage;
  }

  @Override
  @NonNull
  public String toString() {
    String parent = "";
    if (parentContext != null) {
      parent = "(parent=" + parentContext + ")";
    }
    return name + ":" + messageId + parent;
  }

  public void actionDismissed() {
    if (actionDidDismiss != null) {
      actionDidDismiss.onDismiss();
    }
  }

  public void setActionDidDismiss(ActionDidDismiss actionDidDismiss) {
    this.actionDidDismiss = actionDidDismiss;
  }

  public void setActionDidExecute(ActionDidExecute actionDidExecute) {
    this.actionDidExecute = actionDidExecute;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy