io.teak.sdk.TeakNotification.java Source code

Java tutorial

Introduction

Here is the source code for io.teak.sdk.TeakNotification.java

Source

/* Teak -- Copyright (C) 2016 GoCarrot Inc.
 *
 * 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 io.teak.sdk;

import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.os.Bundle;

import android.content.Intent;
import android.content.Context;

import android.app.Notification;

import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.app.NotificationCompat;

import android.util.Log;
import android.util.SparseArray;

import java.util.HashMap;
import java.util.Locale;
import java.util.Random;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Callable;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;

import java.net.URL;
import java.net.URLEncoder;

import javax.net.ssl.HttpsURLConnection;

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

/**
 * An app-to-user notification received from Teak via GCM.
 * <p/>
 * The following parameters from the GCM payload are used to create a <code>TeakNotification</code>.
 * <pre>
 * {@code
 * {
 *   [noAutolaunch] : boolean - automatically launch the app when a push notification is 'opened',
 *   [teakRewardId] : string  - associated Teak Reward Id,
 *   [deepLink]     : string  - a deep link to navigate to on launch,
 *   teakNotifId    : string  - associated Teak Notification Id,
 *   message        : string  - the body text of the notification,
 *   longText       : string  - text displayed when the notification is expanded,
 *   imageAssetA    : string  - URI of an image asset to use for a banner image,
 *   [extras]       : string  - JSON encoded extra data
 * }
 * }
 * </pre>
 */
public class TeakNotification {
    private static final String LOG_TAG = "Teak:Notification";

    /**
     * The 'tag' specified by Teak to the {@link NotificationCompat}
     */
    public static final String NOTIFICATION_TAG = "io.teak.sdk.TeakNotification";

    /**
     * The {@link Intent} action sent by Teak when a notification has been opened by the user.
     * <p/>
     * This allows you to take special actions, it is not required that you listen for it.
     */
    public static final String TEAK_NOTIFICATION_OPENED_INTENT_ACTION_SUFFIX = ".intent.TEAK_NOTIFICATION_OPENED";

    /**
     * The {@link Intent} action sent by Teak when a notification has been cleared by the user.
     * <p/>
     * This allows you to take special actions, it is not required that you listen for it.
     */
    public static final String TEAK_NOTIFICATION_CLEARED_INTENT_ACTION_SUFFIX = ".intent.TEAK_NOTIFICATION_CLEARED";

    /**
     * Intent action used by Teak to notify you that the app was launched from a notification.
     * <p/>
     * You can listen for this using a {@link BroadcastReceiver} and the {@link LocalBroadcastManager}.
     * <pre>
     * {@code
     *     IntentFilter filter = new IntentFilter();
     *     filter.addAction(TeakNotification.LAUNCHED_FROM_NOTIFICATION_INTENT);
     *     LocalBroadcastManager.getInstance(context).registerReceiver(yourBroadcastListener, filter);
     * }
     * </pre>
     */
    public static final String LAUNCHED_FROM_NOTIFICATION_INTENT = "io.teak.sdk.TeakNotification.intent.LAUNCHED_FROM_NOTIFICATION";

    /**
     * Get optional extra data associated with this notification.
     *
     * @return {@link JSONObject} containing extra data sent by the server.
     */
    @SuppressWarnings("unused")
    public JSONObject getExtras() {
        return this.extras;
    }

    public static class Reward {

        /**
         * An unknown error occured while processing the reward.
         */
        public static final int UNKNOWN = 1;

        /**
         * Valid reward claim, grant the user the reward.
         */
        public static final int GRANT_REWARD = 0;

        /**
         * The user has attempted to claim a reward from their own social post.
         */
        public static final int SELF_CLICK = -1;

        /**
         * The user has already been issued this reward.
         */
        public static final int ALREADY_CLICKED = -2;

        /**
         * The reward has already been claimed its maximum number of times globally.
         */
        public static final int TOO_MANY_CLICKS = -3;

        /**
         * The user has already claimed their maximum number of rewards of this type for the day.
         */
        public static final int EXCEED_MAX_CLICKS_FOR_DAY = -4;

        /**
         * This reward has expired and is no longer valid.
         */
        public static final int EXPIRED = -5;

        /**
         * Teak does not recognize this reward id.
         */
        public static final int INVALID_POST = -6;

        /**
         * Status of this reward.
         * <p/>
         * One of the following status codes:
         * {@link Reward#UNKNOWN}
         * {@link Reward#GRANT_REWARD}
         * {@link Reward#SELF_CLICK}
         * {@link Reward#ALREADY_CLICKED}
         * {@link Reward#TOO_MANY_CLICKS}
         * {@link Reward#EXCEED_MAX_CLICKS_FOR_DAY}
         * {@link Reward#EXPIRED}
         * {@link Reward#INVALID_POST}
         * <p/>
         * If status is {@link Reward#GRANT_REWARD}, the 'reward' field will contain the reward that should be granted.
         */
        public int status;

        /**
         * The reward(s) to grant, or <code>null</code>.
         * <p/>
         * <p>The reward(s) contained are in the format:
         * <code>{
         * internal_id? : quantity,
         * another_internal_id? : anotherQuantity
         * }</code></p>
         */
        public JSONObject reward;

        public JSONObject originalJson;

        Reward(JSONObject json) {
            String statusString = "";
            // Try/catch is unneeded practically, but needed to compile
            try {
                statusString = json.isNull("status") ? "" : json.getString("status");
            } catch (Exception ignored) {
            }
            this.originalJson = json;

            if (GRANT_REWARD_STRING.equals(statusString)) {
                status = GRANT_REWARD;
                try {
                    reward = new JSONObject(json.getString("reward"));
                } catch (Exception e) {
                    // TODO: Raven log this
                    Log.e(LOG_TAG, Log.getStackTraceString(e));
                }
            } else if (SELF_CLICK_STRING.equals(statusString)) {
                status = SELF_CLICK;
            } else if (ALREADY_CLICKED_STRING.equals(statusString)) {
                status = ALREADY_CLICKED;
            } else if (TOO_MANY_CLICKS_STRING.equals(statusString)) {
                status = TOO_MANY_CLICKS;
            } else if (EXCEED_MAX_CLICKS_FOR_DAY_STRING.equals(statusString)) {
                status = EXCEED_MAX_CLICKS_FOR_DAY;
            } else if (EXPIRED_STRING.equals(statusString)) {
                status = EXPIRED;
            } else if (INVALID_POST_STRING.equals(statusString)) {
                status = INVALID_POST;
            } else {
                status = UNKNOWN;
            }
        }

        @Override
        public String toString() {
            return String.format(Locale.US, "%s{status: %d, reward: %s}", super.toString(), status,
                    reward == null ? "null" : reward.toString());
        }

        private static final String GRANT_REWARD_STRING = "grant_reward";
        private static final String SELF_CLICK_STRING = "self_click";
        private static final String ALREADY_CLICKED_STRING = "already_clicked";
        private static final String TOO_MANY_CLICKS_STRING = "too_many_clicks";
        private static final String EXCEED_MAX_CLICKS_FOR_DAY_STRING = "exceed_max_clicks_for_day";
        private static final String EXPIRED_STRING = "expired";
        private static final String INVALID_POST_STRING = "invalid_post";

        /**
         * @return A {@link Future} which will contain the reward that should be granted, or <code>null</code> if there is no associated reward.
         */
        @SuppressWarnings("unused")
        public static Future<Reward> rewardFromRewardId(final String teakRewardId) {
            if (!Teak.isEnabled()) {
                Log.e(LOG_TAG, "Teak is disabled, ignoring rewardFromRewardId().");
                return null;
            }

            if (teakRewardId == null || teakRewardId.isEmpty()) {
                Log.e(LOG_TAG, "teakRewardId cannot be null or empty");
                return null;
            }

            final ArrayBlockingQueue<Reward> q = new ArrayBlockingQueue<>(1);
            final FutureTask<Reward> ret = new FutureTask<>(new Callable<Reward>() {
                public Reward call() {
                    try {
                        return q.take();
                    } catch (InterruptedException e) {
                        Log.e(LOG_TAG, Log.getStackTraceString(e));
                    }
                    return null;
                }
            });
            new Thread(ret).start();

            Session.whenUserIdIsReadyRun(new Session.SessionRunnable() {
                @Override
                public void run(Session session) {
                    HttpsURLConnection connection = null;

                    if (Teak.isDebug) {
                        Log.d(LOG_TAG, "Claiming reward id: " + teakRewardId);
                    }

                    try {
                        // https://rewards.gocarrot.com/<<teak_reward_id>>/clicks?clicking_user_id=<<your_user_id>>
                        String requestBody = "clicking_user_id=" + URLEncoder.encode(session.userId(), "UTF-8");

                        URL url = new URL("https://rewards.gocarrot.com/" + teakRewardId + "/clicks");
                        connection = (HttpsURLConnection) url.openConnection();

                        connection.setRequestProperty("Accept-Charset", "UTF-8");
                        connection.setUseCaches(false);
                        connection.setDoOutput(true);
                        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                        connection.setRequestProperty("Content-Length",
                                "" + Integer.toString(requestBody.getBytes().length));

                        // Send request
                        DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
                        wr.writeBytes(requestBody);
                        wr.flush();
                        wr.close();

                        // Get Response
                        InputStream is;
                        if (connection.getResponseCode() < 400) {
                            is = connection.getInputStream();
                        } else {
                            is = connection.getErrorStream();
                        }
                        BufferedReader rd = new BufferedReader(new InputStreamReader(is));
                        String line;
                        StringBuilder response = new StringBuilder();
                        while ((line = rd.readLine()) != null) {
                            response.append(line);
                            response.append('\r');
                        }
                        rd.close();

                        JSONObject responseJson = new JSONObject(response.toString());
                        JSONObject rewardResponse = responseJson.optJSONObject("response");
                        Reward reward = new Reward(rewardResponse);

                        if (Teak.isDebug) {
                            Log.d(LOG_TAG, "Reward claim response: " + responseJson.toString(2));
                        }

                        q.offer(reward);
                    } catch (Exception e) {
                        Log.e(LOG_TAG, Log.getStackTraceString(e));
                        q.offer(null);
                    } finally {
                        if (connection != null) {
                            connection.disconnect();
                        }
                    }
                }
            });

            return ret;
        }
    }

    /**
     * Schedules a push notification for some time in the future.
     *
     * @param creativeId     The identifier of the notification in the Teak dashboard (will create if not found).
     * @param defaultMessage The default message to send, may be over-ridden in the dashboard.
     * @param delayInSeconds The delay in seconds from now to send the notification.
     * @return The identifier of the scheduled notification (see {@link TeakNotification#cancelNotification(String)} or null.
     */
    @SuppressWarnings("unused")
    public static FutureTask<String> scheduleNotification(final String creativeId, final String defaultMessage,
            final long delayInSeconds) {
        if (!Teak.isEnabled()) {
            Log.e(LOG_TAG, "Teak is disabled, ignoring scheduleNotification().");
            return null;
        }

        if (creativeId == null || creativeId.isEmpty()) {
            Log.e(LOG_TAG, "creativeId cannot be null or empty");
            return null;
        }

        if (defaultMessage == null || defaultMessage.isEmpty()) {
            Log.e(LOG_TAG, "defaultMessage cannot be null or empty");
            return null;
        }

        final ArrayBlockingQueue<String> q = new ArrayBlockingQueue<>(1);
        final FutureTask<String> ret = new FutureTask<>(new Callable<String>() {
            public String call() {
                try {
                    return q.take();
                } catch (InterruptedException e) {
                    Log.e(LOG_TAG, Log.getStackTraceString(e));
                }
                return null;
            }
        });

        Session.whenUserIdIsReadyRun(new Session.SessionRunnable() {
            @Override
            public void run(Session session) {
                HashMap<String, Object> payload = new HashMap<>();
                payload.put("identifier", creativeId);
                payload.put("message", defaultMessage);
                payload.put("offset", delayInSeconds);

                new Request("/me/local_notify.json", payload, session) {
                    @Override
                    protected void done(int responseCode, String responseBody) {
                        try {
                            JSONObject response = new JSONObject(responseBody);
                            if (response.getString("status").equals("ok")) {
                                q.offer(response.getJSONObject("event").getString("id"));
                            } else {
                                q.offer("");
                            }
                        } catch (Exception ignored) {
                            q.offer("");
                        }

                        ret.run();
                    }
                }.run();
            }
        });
        return ret;
    }

    /**
     * Cancel a push notification that was scheduled with {@link TeakNotification#scheduleNotification(String, String, long)}
     *
     * @param scheduleId
     * @return
     */
    @SuppressWarnings("unused")
    public static FutureTask<String> cancelNotification(final String scheduleId) {
        if (!Teak.isEnabled()) {
            Log.e(LOG_TAG, "Teak is disabled, ignoring cancelNotification().");
            return null;
        }

        if (scheduleId == null || scheduleId.isEmpty()) {
            Log.e(LOG_TAG, "scheduleId cannot be null or empty");
            return null;
        }

        final ArrayBlockingQueue<String> q = new ArrayBlockingQueue<>(1);
        final FutureTask<String> ret = new FutureTask<>(new Callable<String>() {
            public String call() {
                try {
                    return q.take();
                } catch (InterruptedException e) {
                    Log.e(LOG_TAG, Log.getStackTraceString(e));
                }
                return null;
            }
        });

        Session.whenUserIdIsReadyRun(new Session.SessionRunnable() {
            @Override
            public void run(Session session) {
                HashMap<String, Object> payload = new HashMap<>();
                payload.put("id", scheduleId);

                new Request("/me/cancel_local_notify.json", payload, session) {
                    @Override
                    protected void done(int responseCode, String responseBody) {
                        try {
                            JSONObject response = new JSONObject(responseBody);
                            if (response.getString("status").equals("ok")) {
                                q.offer(response.getJSONObject("event").getString("id"));
                            } else {
                                q.offer("");
                            }
                        } catch (Exception ignored) {
                            q.offer("");
                        }
                        ret.run();
                    }
                }.run();
            }
        });

        return ret;
    }

    /**************************************************************************/

    static final String INBOX_CACHE_CREATE_SQL = "CREATE TABLE IF NOT EXISTS inbox(teak_notification_id INTEGER, android_id INTEGER, notification_payload TEXT)";

    String message;
    String longText;
    String teakRewardId;
    String imageAssetA;
    String deepLink;
    int platformId;
    long teakNotifId;
    JSONObject extras;

    static SparseArray<Thread> notificationUpdateThread = new SparseArray<>();

    private TeakNotification(Bundle bundle) {
        this.message = bundle.getString("message");
        this.longText = bundle.getString("longText");
        this.teakRewardId = bundle.getString("teakRewardId");
        this.imageAssetA = bundle.getString("imageAssetA");
        this.deepLink = bundle.getString("deepLink");
        try {
            this.extras = bundle.getString("extras") == null ? null : new JSONObject(bundle.getString("extras"));
        } catch (JSONException e) {
            this.extras = null;
        }

        try {
            this.teakNotifId = Long.parseLong(bundle.getString("teakNotifId"));
        } catch (Exception e) {
            this.teakNotifId = 0;
        }

        this.platformId = new Random().nextInt();
    }

    static NotificationManager notificationManager;

    static TeakNotification remoteNotificationFromIntent(final Context context, Intent intent) {
        final Bundle bundle = intent.getExtras();

        if (!bundle.containsKey("teakNotifId")) {
            return null;
        }

        final TeakNotification ret = new TeakNotification(bundle);

        new Thread(new Runnable() {
            @Override
            public void run() {
                // Add platformId to bundle
                bundle.putInt("platformId", ret.platformId);
                // Create native notification
                Notification nativeNotification = NotificationBuilder.createNativeNotification(context, bundle,
                        ret);
                if (nativeNotification != null) {
                    displayNotification(context, ret, nativeNotification);
                }
            }
        }).start();

        return ret;
    }

    static void displayNotification(Context context, TeakNotification teakNotif, Notification nativeNotification) {
        if (notificationManager == null) {
            try {
                notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            } catch (Exception e) {
                Log.e(LOG_TAG, Log.getStackTraceString(e));
                return;
            }
        }

        // Send it out
        if (Teak.isDebug) {
            Log.d(LOG_TAG, "Showing Notification");
            Log.d(LOG_TAG, "       Teak id: " + teakNotif.teakNotifId);
            Log.d(LOG_TAG, "   Platform id: " + teakNotif.platformId);
        }
        notificationManager.notify(NOTIFICATION_TAG, teakNotif.platformId, nativeNotification);

        // TODO: Here is where any kind of thread/update logic will live
    }

    static void cancel(Context context, int platformId) {
        if (notificationManager == null) {
            try {
                notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            } catch (Exception e) {
                Log.e(LOG_TAG, Log.getStackTraceString(e));
                return;
            }
        }

        if (Teak.isDebug) {
            Log.d(LOG_TAG, "Canceling notification id: " + platformId);
        }

        notificationManager.cancel(NOTIFICATION_TAG, platformId);
        context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));

        Thread updateThread = TeakNotification.notificationUpdateThread.get(platformId);
        if (updateThread != null) {
            updateThread.interrupt();
        }
    }
}