com.deltadna.android.sdk.ImageMessage.java Source code

Java tutorial

Introduction

Here is the source code for com.deltadna.android.sdk.ImageMessage.java

Source

/*
 * Copyright (c) 2016 deltaDNA Ltd. All rights reserved.
 * 
 * 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 com.deltadna.android.sdk;

import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.support.annotation.Nullable;
import android.util.Log;

import com.deltadna.android.sdk.listeners.RequestListener;
import com.deltadna.android.sdk.net.CancelableRequest;
import com.deltadna.android.sdk.net.Response;

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

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Locale;
import java.util.Vector;

public final class ImageMessage implements Serializable {

    private static final String TAG = BuildConfig.LOG_TAG + ' ' + ImageMessage.class.getSimpleName();

    static final String ACTION_DISMISS = "dismiss";
    static final String ACTION_ACTION = "action";

    private static final String ALIGN_CENTER = "center";
    private static final String ALIGN_RIGHT = "right";
    private static final String ALIGN_BOTTOM = "bottom";

    static final String MASK_DIMMED = "dimmed";

    private static final int METRICTYPE_PIXELS = 0;
    private static final int METRICTYPE_PERCENTAGE = 1;

    private final String transactionId;
    private final String parameters;

    private final String imageUrl;
    private String imageFormat;

    final Background background;
    private final Vector<Button> buttons;
    final Shim shim;

    private boolean prepared;

    @Nullable
    private CancelableRequest request;

    /**
     * Creates an instance from a JSON response.
     *
     * @throws JSONException if the JSON is invalid
     */
    public ImageMessage(JSONObject json) throws JSONException {
        transactionId = json.getString("transactionID");
        parameters = json.getJSONObject("parameters").toString();

        final JSONObject image = json.getJSONObject("image");
        imageUrl = image.getString("url");
        imageFormat = image.getString("format");

        final JSONObject layout = image.getJSONObject("layout");
        final JSONObject spritemap = image.getJSONObject("spritemap");
        final JSONObject layoutLandscape = layout.optJSONObject("landscape");
        final JSONObject layoutPortrait = layout.optJSONObject("portrait");

        background = new Background(spritemap.getJSONObject("background"),
                (layoutLandscape == null) ? null : layoutLandscape.getJSONObject("background"),
                (layoutPortrait == null) ? null : layoutPortrait.getJSONObject("background"));

        buttons = new Vector<>();
        final JSONArray buttons = spritemap.has("buttons") ? spritemap.getJSONArray("buttons") : new JSONArray();
        final JSONArray buttonLayoutLandscape = (layoutLandscape == null || buttons.length() == 0) ? null
                : layoutLandscape.getJSONArray("buttons");
        final JSONArray buttonLayoutPortrait = (layoutPortrait == null || buttons.length() == 0) ? null
                : layoutPortrait.getJSONArray("buttons");
        for (int i = 0; i < buttons.length(); i++) {
            this.buttons.add(new Button(buttons.getJSONObject(i),
                    (buttonLayoutLandscape == null) ? null : buttonLayoutLandscape.getJSONObject(i),
                    (buttonLayoutPortrait == null) ? null : buttonLayoutPortrait.getJSONObject(i)));
        }

        shim = new Shim(image.getJSONObject("shim"));
    }

    /**
     * Gets the prepared state.
     *
     * @return {@code true} if ready to use, else {@code false}
     */
    public boolean prepared() {
        return prepared;
    }

    /**
     * Prepares the Image Message for use, by downloading the image.
     *
     * @param listener  the listener for receiving prepared state
     */
    public void prepare(final PrepareListener listener) {
        if (prepared) {
            listener.onPrepared(this);
        } else {
            // do we have an image?
            final File file = new File(getImageFilepath());
            if (!file.exists() && request == null) {
                if (!file.getParentFile().exists()) {
                    if (!file.getParentFile().mkdirs()) {
                        Log.w(TAG, "Failed to create path for " + file);
                        listener.onError(new IOException("Failed to create path for " + file));
                        return;
                    }
                }

                request = DDNA.instance().getNetworkManager().fetch(imageUrl, file, new RequestListener<File>() {
                    @Override
                    public void onCompleted(Response<File> result) {
                        prepared = true;
                        request = null;

                        listener.onPrepared(ImageMessage.this);
                    }

                    @Override
                    public void onError(Throwable t) {
                        prepared = false;
                        request = null;

                        listener.onError(t);
                    }
                });
            } else {
                prepared = true;
                listener.onPrepared(this);
            }
        }
    }

    /**
     * Opens the {@link ImageMessageActivity} for showing this Image Message.
     *
     * @param activity      the {@link Activity} from which the request is
     *                      being started
     * @param requestCode   the request code that will be used in
     *                      {@link Activity#onActivityResult(int, int, Intent)}
     *                      for the result
     *
     * @throws IllegalStateException if the Image Message is not prepared
     */
    public void show(Activity activity, int requestCode) {
        if (!prepared)
            throw new IllegalStateException("image message has not been prepared yet");

        activity.startActivityForResult(ImageMessageActivity.createIntent(activity, this), requestCode);
    }

    /**
     * Cleans up associated resources.
     */
    public void cleanUp() {
        if (request != null)
            request.cancel();

        final File file = new File(getImageFilepath());
        if (file.exists() && !file.delete()) {
            Log.w(TAG, "Failed to cleanup " + file);
        }
    }

    /**
     * Recalculates the layouts, assumes that the larger dimension will be
     * portrait vertical.
     */
    void init(int orientation, int screenWidth, int screenHeight) {
        // calculate landscape/portrait based on given widths and heights
        final int realWidth = screenWidth < screenHeight ? screenWidth : screenHeight;
        final int realHeight = screenHeight > screenWidth ? screenHeight : screenWidth;

        // pass screen width and height to background
        background.init(orientation, realWidth, realHeight);

        for (int i = 0; i < buttons.size(); i++) {
            buttons.get(i).init(orientation, background.layout(Configuration.ORIENTATION_PORTRAIT),
                    background.layout(Configuration.ORIENTATION_LANDSCAPE));
        }
    }

    /**
     * Gets the user defined parameters of the message.
     *
     * @return the user parameters
     */
    JSONObject parameters() {
        try {
            return new JSONObject(parameters);
        } catch (JSONException e) {
            // cannot happen as parameters came from JSON
            throw new IllegalStateException(e);
        }
    }

    Iterator<Button> buttons() {
        return buttons.iterator();
    }

    /**
     * Gets the image filepath.
     *
     * @return the local image filepath.
     */
    String getImageFilepath() {
        return DDNA.instance().getEngageStoragePath() + "/engageimg_" + transactionId + '.' + imageFormat;
    }

    /**
     * Creates an Image Message from an Engagement once it has been populated
     * with response data after a successful request.
     * <p>
     * {@code null} may be returned in case the Engagement was not set-up to
     * display an Image Message.
     *
     * @param engagement the Engagement with response data
     *
     * @return  the Image Message created from {@code engagement}, else
     *          {@code null}
     */
    @Nullable
    public static ImageMessage create(Engagement engagement) {
        //noinspection ConstantConditions
        if (engagement.isSuccessful() && engagement.getJson().has("image")) {
            try {
                return new ImageMessage(engagement.getJson());
            } catch (JSONException e) {
                Log.w(TAG, "Failed creating image message", e);
                return null;
            }
        }

        return null;
    }

    public interface PrepareListener {

        /**
         * Notifies the listener that the Image Message has been prepared.
         * <p>
         * In most implementations {@link #show(Activity, int)} should be
         * called, if the application is still in an appropriate state to do
         * so.
         *
         * @param src the prepared Image Message
         */
        void onPrepared(ImageMessage src);

        /**
         * Notifies the listener that an error has happened during the
         * preparation request.
         * <p>
         * If this method is called {@link #onPrepared(ImageMessage)} will not
         * be called.
         *
         * @param cause the cause of the error
         */
        void onError(Throwable cause);
    }

    /**
     * Description of an image popup background.
     *
     * TODO legacy code
     */
    class Background extends ImageBase {

        /**
         * Layout data for a background.
         */
        class Layout implements Serializable {

            private String mType = "cover";

            private String mHAlign = ALIGN_CENTER;
            private String mVAlign = ALIGN_CENTER;

            private int mPadLeft = 0;
            private int mPadLeftUnits = METRICTYPE_PIXELS;
            private int mPadRight = 0;
            private int mPadRightUnits = METRICTYPE_PIXELS;
            private int mPadTop = 0;
            private int mPadTopUnits = METRICTYPE_PIXELS;
            private int mPadBottom = 0;
            private int mPadBottomUnits = METRICTYPE_PIXELS;

            private float mScale = 1.0f;

            private Rect mFrame = null;

            /**
             * The overall background scale factor.
             *
             * @return The scale factor normalised to 1.0f
             */
            public float scale() {
                return mScale;
            }

            /**
             * The screen frame of the background calculated for a previous call
             * to init() with screen height and width.
             *
             * @return The screen rect, null if it has not been initialised.
             */
            public Rect frame() {
                return mFrame;
            }

            /**
             * Initialises the screen rect using the specified screen width and height
             * and applying the layout metrics.
             *
             * @param screenWidth The screen width to use.
             * @param screenHeight The screen height to use.
             */
            public void init(int screenWidth, int screenHeight) {
                if (mFrame == null) {
                    mFrame = new Rect();

                    int tp = 0;
                    int lp = 0;
                    int bp = 0;
                    int rp = 0;

                    // if "contain" calculate padding
                    if (mType.equalsIgnoreCase("contain")) {
                        if (mPadTopUnits == METRICTYPE_PIXELS) {
                            tp = mPadTop;
                        } else {
                            tp = (int) ((mPadTop / 100.0) * screenHeight);
                        }
                        if (mPadLeftUnits == METRICTYPE_PIXELS) {
                            lp = mPadLeft;
                        } else {
                            lp = (int) ((mPadLeft / 100.0) * screenWidth);
                        }
                        if (mPadBottomUnits == METRICTYPE_PIXELS) {
                            bp = mPadBottom;
                        } else {
                            bp = (int) ((mPadBottom / 100.0) * screenHeight);
                        }
                        if (mPadRightUnits == METRICTYPE_PIXELS) {
                            rp = mPadRight;
                        } else {
                            rp = (int) ((mPadRight / 100.0) * screenWidth);
                        }
                    }

                    // calculate scales
                    float sw = (screenWidth - (lp + rp)) / (float) imageW;
                    float sh = (screenHeight - (tp + bp)) / (float) imageH;

                    mScale = (sw < sh && mType.equalsIgnoreCase("contain")) ? sw : sh;

                    // calculate the width and height
                    int pWidth = (int) (imageW * mScale);
                    int pHeight = (int) (imageH * mScale);

                    // calculate alignment
                    int x = lp;
                    int y = tp;

                    if (mHAlign.equalsIgnoreCase(ImageMessage.ALIGN_CENTER)) {
                        x = lp + ((screenWidth - (pWidth + lp + rp)) / 2);
                    } else if (mHAlign.equalsIgnoreCase(ImageMessage.ALIGN_RIGHT)) {
                        x = screenWidth - (pWidth + rp);
                    }
                    if (mVAlign.equalsIgnoreCase(ImageMessage.ALIGN_CENTER)) {
                        y = tp + ((screenHeight - (pHeight + tp + bp)) / 2);
                    } else if (mVAlign.equalsIgnoreCase(ImageMessage.ALIGN_BOTTOM)) {
                        y = screenHeight - (pHeight + bp);
                    }

                    mFrame.left = x;
                    mFrame.top = y;
                    mFrame.right = x + pWidth;
                    mFrame.bottom = y + pHeight;
                }
            }
        }

        private Layout mLandscape = null;
        private Layout mPortrait = null;

        protected Background(JSONObject sprite, JSONObject layoutLandscape, JSONObject layoutPortrait)
                throws JSONException {

            super(sprite, layoutLandscape, layoutPortrait);

            JSONObject tempObj = null;
            String tempStr = null;

            if (layoutLandscape != null) {
                mLandscape = new Layout();
                try {
                    tempObj = layoutLandscape.getJSONObject("contain");
                    mLandscape.mType = "contain";
                } catch (JSONException e) {
                    try {
                        tempObj = layoutLandscape.getJSONObject("cover");
                        mLandscape.mType = "cover";
                    } catch (JSONException e2) {
                    }
                }

                if (tempObj != null) {
                    try {
                        mLandscape.mHAlign = tempObj.getString("halign");
                    } catch (JSONException e) {
                    }
                    try {
                        mLandscape.mVAlign = tempObj.getString("valign");
                    } catch (JSONException e) {
                    }

                    try {
                        tempStr = tempObj.getString("left");
                        mLandscape.mPadLeft = getInteger(tempStr);
                        mLandscape.mPadLeftUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                    try {
                        tempStr = tempObj.getString("right");
                        mLandscape.mPadRight = getInteger(tempStr);
                        mLandscape.mPadRightUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                    try {
                        tempStr = tempObj.getString("top");
                        mLandscape.mPadTop = getInteger(tempStr);
                        mLandscape.mPadTopUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                    try {
                        tempStr = tempObj.getString("bottom");
                        mLandscape.mPadBottom = getInteger(tempStr);
                        mLandscape.mPadBottomUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                }
            }

            if (layoutPortrait != null) {
                mPortrait = new Layout();
                try {
                    tempObj = layoutPortrait.getJSONObject("contain");
                    mLandscape.mType = "contain";
                } catch (JSONException e) {
                    try {
                        tempObj = layoutPortrait.getJSONObject("cover");
                        mLandscape.mType = "cover";
                    } catch (JSONException e2) {
                    }
                }

                if (tempObj != null) {
                    try {
                        mPortrait.mHAlign = tempObj.getString("halign");
                    } catch (JSONException e) {
                    }
                    try {
                        mPortrait.mVAlign = tempObj.getString("valign");
                    } catch (JSONException e) {
                    }

                    try {
                        tempStr = tempObj.getString("left");
                        mPortrait.mPadLeft = getInteger(tempStr);
                        mPortrait.mPadLeftUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                    try {
                        tempStr = tempObj.getString("right");
                        mPortrait.mPadRight = getInteger(tempStr);
                        mPortrait.mPadRightUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                    try {
                        tempStr = tempObj.getString("top");
                        mPortrait.mPadTop = getInteger(tempStr);
                        mPortrait.mPadTopUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                    try {
                        tempStr = tempObj.getString("bottom");
                        mPortrait.mPadBottom = getInteger(tempStr);
                        mPortrait.mPadBottomUnits = getMetricUnit(tempStr);
                    } catch (JSONException e) {
                    }
                }
            }
        }

        /**
         * Returns the layout for the specified orientation. If a layout for the
         * given orientation is not present any existing layout is returned.
         *
         * @param orientation The requested orientation.
         *
         * @return The layout, null on error.
         */
        public Layout layout(int orientation) {
            if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                return mLandscape != null ? mLandscape : mPortrait;
            } else {
                return mPortrait != null ? mPortrait : mLandscape;
            }
        }

        /**
         * The units defined by the given layout value.
         *
         * May be one of METRICTYPE_PIXELS or METRICTYPE_PERCENTAGE
         *
         * @param s The data string to parse.
         *
         * @return The unit type.
         */
        private int getMetricUnit(String s) {
            int result = METRICTYPE_PIXELS;

            if (s != null) {
                if (s.contains("%")) {
                    result = METRICTYPE_PERCENTAGE;
                } else if (s.toUpperCase(Locale.getDefault()).contains("px")) {
                    result = METRICTYPE_PIXELS;
                }
            }

            return result;
        }

        /**
         * Parses the integer from a layout metric string.
         *
         * @param s The metric string.
         *
         * @return The integer value represented by the string.
         */
        private int getInteger(String s) {
            int result = 0;

            if (s != null) {
                String intStr = null;
                int idx = s.indexOf("%");
                if (idx < 0) {
                    idx = s.indexOf("px");
                }

                if (idx > -1) {
                    intStr = s.substring(0, idx);
                    result = Integer.parseInt(intStr);
                }
            }

            return result;
        }

        /**
         * Initialises this Background object using the orientation and width/height.
         *
         * @param orientation The current device orientation.
         * @param screenWidth The screen width.
         * @param screenHeight The screen height.
         */
        public void init(int orientation, int screenWidth, int screenHeight) {
            if (mPortrait != null) {
                if ((mLandscape != null) || (orientation == Configuration.ORIENTATION_PORTRAIT)) {
                    mPortrait.init(screenWidth, screenHeight);
                } else {
                    mPortrait.init(screenHeight, screenWidth);
                }
            }
            if (mLandscape != null) {
                if ((mPortrait != null) || (orientation == Configuration.ORIENTATION_LANDSCAPE)) {
                    mLandscape.init(screenHeight, screenWidth);
                } else {
                    mLandscape.init(screenWidth, screenHeight);
                }
            }
        }
    }

    /**
     * Description of an image message button.
     *
     * TODO legacy code
     */
    static public class Button extends ImageBase {
        /**
         * Layout data for a button.
         */
        public class Layout implements Serializable {
            private int mX = -1;
            private int mY = -1;

            private Rect mFrame = null;

            /**
             * The popup relative x position of the button.
             *
             * @return The popup relative x position.
             */
            public int x() {
                return mX;
            }

            /**
             * The popup relative y position of the button.
             *
             * @return The popup relative y position.
             */
            public int y() {
                return mY;
            }

            /**
             * The popup relative button frame calculated by init().
             *
             * @return A popup relative frame, null if not calculated yet.
             */
            public Rect frame() {
                return mFrame;
            }

            /**
             * Initialises the button frame to the given popup frame and scale.
             *
             * @param frame The popup frame.
             * @param scale The popup scale.
             */
            public void init(Rect frame, float scale) {
                if (mFrame == null) {
                    mFrame = new Rect();

                    final int btnX = frame.left + (int) (mX * scale);
                    final int btnY = frame.top + (int) (mY * scale);

                    mFrame.left = btnX;
                    mFrame.top = btnY;
                    mFrame.right = btnX + (int) (imageW * scale);
                    mFrame.bottom = btnY + (int) (imageH * scale);
                }
            }

        }

        private Layout mLandscape = null;
        private Layout mPortrait = null;

        protected Button(JSONObject sprite, JSONObject layoutLandscape, JSONObject layoutPortrait)
                throws JSONException {

            super(sprite, layoutLandscape, layoutPortrait);

            if (layoutLandscape != null) {
                mLandscape = new Layout();
                try {
                    mLandscape.mX = layoutLandscape.getInt("x");
                } catch (JSONException e) {
                }
                try {
                    mLandscape.mY = layoutLandscape.getInt("y");
                } catch (JSONException e) {
                }
            }

            if (layoutPortrait != null) {
                mPortrait = new Layout();
                try {
                    mPortrait.mX = layoutPortrait.getInt("x");
                } catch (JSONException e) {
                }
                try {
                    mPortrait.mY = layoutPortrait.getInt("y");
                } catch (JSONException e) {
                }
            }
        }

        /**
         * Returns the layout for the specified orientation. If a layout for the
         * given orientation is not present any existing layout is returned.
         *
         * @param orientation The requested orientation.
         *
         * @return The layout, null on error.
         */
        public Layout layout(int orientation) {
            if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                return mLandscape != null ? mLandscape : mPortrait;
            } else {
                return mPortrait != null ? mPortrait : mLandscape;
            }
        }

        /**
         * Initialises the button to the given orientation and landscape/portrait layouts.
         *
         * @param orientation The current device orientation.
         * @param portraitBg The portrait popup layout.
         * @param landscapeBg The landscape popup layout.
         */
        public void init(int orientation, Background.Layout portraitBg, Background.Layout landscapeBg) {
            if (mPortrait != null) {
                if ((mLandscape != null) || (orientation == Configuration.ORIENTATION_PORTRAIT)) {
                    mPortrait.init(portraitBg.frame(), portraitBg.scale());
                } else {
                    mPortrait.init(portraitBg.frame(), portraitBg.scale());
                }
            }
            if (mLandscape != null) {
                if ((mPortrait != null) || (orientation == Configuration.ORIENTATION_LANDSCAPE)) {
                    mLandscape.init(landscapeBg.frame(), landscapeBg.scale());
                } else {
                    mLandscape.init(landscapeBg.frame(), landscapeBg.scale());
                }
            }
        }
    }

    /**
     * The image sprite base for popup render.
     */
    private static class ImageBase implements Serializable {

        final int imageX;
        final int imageY;
        final int imageW;
        final int imageH;

        final Rect imageRect;

        @Nullable
        private final Action landscapeAction;
        @Nullable
        private final Action portraitAction;

        ImageBase(JSONObject sprite, @Nullable JSONObject layoutLandscape, @Nullable JSONObject layoutPortrait)
                throws JSONException {

            imageX = sprite.getInt("x");
            imageY = sprite.getInt("y");
            imageW = sprite.getInt("width");
            imageH = sprite.getInt("height");

            imageRect = new Rect(imageX, imageY, imageX + imageW, imageY + imageH);

            landscapeAction = (layoutLandscape != null) ? new Action(layoutLandscape.getJSONObject("action"))
                    : null;
            portraitAction = (layoutPortrait != null) ? new Action(layoutPortrait.getJSONObject("action")) : null;
        }

        /**
         * The action for the given orientation.
         *
         * @param orientation The device orientation.
         *
         * @return The action.
         */
        Action action(int orientation) {
            if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                return landscapeAction != null ? landscapeAction : portraitAction;
            } else {
                return portraitAction != null ? portraitAction : landscapeAction;
            }
        }
    }

    /**
     * Description of the screen outside the popup.
     */
    static class Shim implements Serializable {

        /**
         * Fill mask type.
         */
        final String mask;
        /**
         * Touch action.
         */
        final Action action;

        Shim(JSONObject json) throws JSONException {
            mask = json.getString("mask");
            action = new Action(json.getJSONObject("action"));
        }
    }

    /**
     * Encapsulates an Action.
     */
    static class Action implements Serializable {

        /**
         * Action type.
         */
        final String type;
        /**
         * Action value.
         */
        @Nullable
        final String value;

        Action(JSONObject json) throws JSONException {
            type = json.getString("type");
            value = json.optString("value");
        }
    }

    static class Rect implements Serializable {

        int left;
        int top;
        int right;
        int bottom;

        Rect() {
        }

        Rect(int left, int top, int right, int bottom) {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }

        boolean contains(int x, int y) {
            return asRect().contains(x, y);
        }

        android.graphics.Rect asRect() {
            return new android.graphics.Rect(left, top, right, bottom);
        }
    }
}