com.mobicage.rogerthat.plugins.news.NewsChannel.java Source code

Java tutorial

Introduction

Here is the source code for com.mobicage.rogerthat.plugins.news.NewsChannel.java

Source

/*
 * Copyright 2018 GIG Technology NV
 *
 * 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.
 *
 * @@license_version:1.4@@
 */

package com.mobicage.rogerthat.plugins.news;

import com.mobicage.rogerthat.IdentityStore;
import com.mobicage.rogerthat.MainService;
import com.mobicage.rogerthat.MyIdentity;
import com.mobicage.rogerthat.config.Configuration;
import com.mobicage.rogerthat.config.ConfigurationProvider;
import com.mobicage.rogerthat.plugins.friends.FriendsPlugin;
import com.mobicage.rogerthat.util.logging.L;
import com.mobicage.rogerthat.util.net.NetworkConnectivityManager;
import com.mobicage.rogerthat.util.system.SafeRunnable;
import com.mobicage.rogerthat.util.system.T;
import com.mobicage.rogerthat.util.ui.TestUtils;
import com.mobicage.rpc.Credentials;
import com.mobicage.rpc.IncompleteMessageException;
import com.mobicage.rpc.config.AppConstants;
import com.mobicage.rpc.config.CloudConstants;
import com.mobicage.to.news.AppNewsItemTO;

import org.jivesoftware.smack.util.Base64;
import org.jivesoftware.smack.util.DNSUtil;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import java.io.IOException;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;

import javax.net.ssl.TrustManagerFactory;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

@ChannelHandler.Sharable
public class NewsChannel extends SimpleChannelInboundHandler<String> {
    private static final String CONFIGKEY = "com.mobicage.rogerthat.plugins.news.channel";
    private static final String CONFIG_TYPE_READ = "READ";
    private static final String CONFIG_TYPE_ROGER = "ROGER";
    private final int KEEPALIVE_DELAY = 30;

    private final MainService mService;
    private NewsChannelCallbackHandler mNewsChannelCallbackHandler;
    private String mHost;
    private int mPort;
    private Channel mChannel;
    private EventLoopGroup mEventLoopGroup;
    private volatile boolean mIsConnected;
    private ConfigurationProvider mConfigurationProvider;
    private boolean mIsRetryingToConnect = false;
    private volatile boolean mAuthenticated = false;
    private Timer mKeepAliveTimer;

    private IdentityStore mIdentityStore;

    private final Set<Long> mReadsToSend = new HashSet<>();
    private final Set<Long> mRogersToSend = new HashSet<>();
    private final List<String> mStashedCommands = new ArrayList<>();

    public boolean isConnected() {
        return mIsConnected;
    }

    public boolean hasValidConfiguration() {
        return this.mHost != null && this.mPort != -1;
    }

    private enum Command {
        AUTH("AUTH"), SET_INFO("SET INFO"), NEWS_READ("NEWS READ"), NEWS_ROGER("NEWS ROGER"), ACK_NEWS_ROGER(
                "ACK NEWS ROGER"), ACK_NEWS_READ("ACK NEWS READ"), NEWS_STATS("NEWS STATS"), NEWS_READ_UPDATE(
                        "NEWS READ UPDATE"), NEWS_ROGER_UPDATE(
                                "NEWS ROGER UPDATE"), NEWS_PUSH("NEWS PUSH"), PING("PING"), PONG("PONG");

        private final String name;

        Command(String s) {
            name = s;
        }

        public String toString() {
            return name;
        }

        private static Command fromValue(String value) {
            for (Command val : Command.values()) {
                if (String.valueOf(val).equals(value)) {
                    return val;
                }
            }
            return null;
        }
    }

    public NewsChannel(NewsChannelCallbackHandler handler, ConfigurationProvider configurationProvider) {
        mNewsChannelCallbackHandler = handler;
        mService = handler.getMainService();
        mConfigurationProvider = configurationProvider;

        mIdentityStore = mService.getIdentityStore();

        if (TestUtils.isRunningTest()) {
            return;
        }
        loadCallsFromDB();

        if (mService.getNetworkConnectivityManager().isConnected()) {
            mService.runOnNewsHandler(new SafeRunnable() {
                @Override
                protected void safeRun() throws Exception {
                    getConfiguration();
                }
            });
        }
    }

    public void internetConnected() {
        T.NEWS();
        if (mHost == null || mPort == -1) {
            getConfiguration();
        }
    }

    public void internetDisconnected() {
        T.NEWS();
        disconnect();
    }

    public void connect() {
        if (TestUtils.isRunningTest()) {
            return;
        }
        T.NEWS();
        if (mIsConnected) {
            L.d("Already connected to news channel");
            return;
        } else if (!mService.getNetworkConnectivityManager().isConnected()) {
            L.d("Cannot connect to news channel: no internet connection.");
            return;
        } else if (mHost == null) {
            L.d("Not connecting to news channel because no host was found");
            return;
        } else if (mPort == -1) {
            L.d("Not connecting to news channel because no port was found");
            return;
        }
        mIsRetryingToConnect = true;
        L.d("Attemping to connect to news channel...");
        final SslContext sslCtx;
        if (CloudConstants.NEWS_CHANNEL_SSL) {
            try {
                if (CloudConstants.NEWS_CHANNEL_MUST_VALIDATE_SSL_CERTIFICATE) {
                    TrustManagerFactory factory = TrustManagerFactory
                            .getInstance(TrustManagerFactory.getDefaultAlgorithm());
                    KeyStore keyStore = KeyStore.getInstance("AndroidCAStore"); // Gets the default system keystore
                    keyStore.load(null, null);
                    factory.init(keyStore);
                    sslCtx = SslContextBuilder.forClient().trustManager(factory).build();
                } else {
                    sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
                            .build();
                }
            } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException e) {
                L.bug(e);
                return;
            }
        } else {
            sslCtx = null;
        }
        if (mEventLoopGroup == null) {
            mEventLoopGroup = new NioEventLoopGroup();
        }
        Bootstrap b = new Bootstrap();
        b.group(mEventLoopGroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        if (sslCtx != null) {
                            SslHandler sslHandler = sslCtx.newHandler(ch.alloc(), mHost, mPort);
                            Future<Channel> handshakeDone = sslHandler.handshakeFuture();
                            handshakeDone.addListener(new GenericFutureListener<Future<? super Channel>>() {
                                @Override
                                public void operationComplete(Future<? super Channel> future) throws Exception {
                                    authenticate();
                                }
                            });
                            p.addLast(sslHandler);
                        }
                        // decoder
                        p.addLast(new DelimiterBasedFrameDecoder(102400, Delimiters.lineDelimiter()));
                        p.addLast(new StringDecoder(Charset.forName("UTF-8")));

                        //encoder
                        p.addLast(new StringEncoder(Charset.forName("UTF-8")));
                        p.addLast(NewsChannel.this);
                    }
                });
        // Bind and start to accept incoming connections.
        mChannel = b.connect(mHost, mPort).channel();
    }

    private void sendLine(String line) {
        L.d("[NEWS] >> " + line);
        if (mChannel == null || !mIsConnected)
            return;
        mChannel.writeAndFlush(line + "\r\n");
    }

    private void keepAlive() {
        if (mKeepAliveTimer != null) {
            mKeepAliveTimer.cancel();
        }
        mKeepAliveTimer = new Timer(true);
        mKeepAliveTimer.scheduleAtFixedRate(new java.util.TimerTask() {
            @Override
            public void run() {
                if (mIsConnected) {
                    sendLine(Command.PING.toString());
                }
            }
        }, 0, KEEPALIVE_DELAY * 1000);
    }

    private void sendCommand(Command command, String data) {
        T.dontCare(); //
        String line = String.format("%s: %s", command, data);
        if (CloudConstants.NEWS_CHANNEL_SSL && mAuthenticated && mIsConnected) {
            sendLine(line);
        } else if (!CloudConstants.NEWS_CHANNEL_SSL && mIsConnected) {
            sendLine(line);
        } else {
            // Stash commands when not connected/authenticated. Will be sent once authenticated.
            mStashedCommands.add(line);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        L.d("Connected to news channel.");
        mIsConnected = true;
        mIsRetryingToConnect = false;
        mAuthenticated = false;
        if (!CloudConstants.NEWS_CHANNEL_SSL)
            authenticate();
    }

    public void disconnect() {
        T.NEWS();
        if (mChannel == null || mEventLoopGroup == null) {
            return;
        }
        mChannel.closeFuture();
        mEventLoopGroup.shutdownGracefully();
        mChannel = null;
        mEventLoopGroup = null;
        mIsConnected = false;
        mIsRetryingToConnect = false;
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) {
        L.d("[NEWS] << " + msg);
        if (Command.PONG.toString().equals(msg)) {
            return;
        }
        String[] result = msg.split(": ", 2);
        if (result.length < 2) {
            L.d("Unknown command");
            return;
        }
        Command command = Command.fromValue(result[0]);
        String data = result[1];
        if (command == null) {
            L.bug("Received unknown command: " + result[0]);
            return;
        }
        switch (command) {
        case AUTH:
            if ("ERROR".equals(data)) {
                L.bug("Failed to authenticate user to kickserver.");
            } else if ("OK".equals(data)) {
                userAuthenticated();
            }
            break;
        case ACK_NEWS_READ:
            ackNewsRead(data);
            break;
        case ACK_NEWS_ROGER:
            ackNewsRoger(data);
            break;
        case NEWS_READ_UPDATE:
            newsReadUpdate(data);
            break;
        case NEWS_STATS:
            newsStatsReceived(data);
            break;
        case NEWS_ROGER_UPDATE:
            newsRogerUpdate(data);
            break;
        case NEWS_PUSH:
            newsPush(data);
            break;
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        L.d("News channel inactive");
        super.channelInactive(ctx);
        mIsConnected = false;
        mIsRetryingToConnect = false;
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        L.d("News channel unregistered");
        super.channelUnregistered(ctx);
        mIsConnected = false;
        mIsRetryingToConnect = false;
    }

    private void getConfiguration() {
        NetworkConnectivityManager networkConnectivityManager = mService.getNetworkConnectivityManager();
        NewsConfigurationFactory configurationFactory = new NewsConfigurationFactory(mConfigurationProvider,
                networkConnectivityManager, null);
        try {
            DNSUtil.HostAddress hostAddress = configurationFactory.getSafeNewsConnectionHost(false);
            mHost = hostAddress.getHost();
            mPort = hostAddress.getPort();
        } catch (NewsConfigurationConnectionException ignored) {
        } catch (NewsConfigurationException e) {
            L.bug(e);
        }
    }

    public boolean isTryingToReconnect() {
        return mIsRetryingToConnect;
    }

    private void userAuthenticated() {
        mAuthenticated = true;
        MyIdentity identity = mIdentityStore.getIdentity();
        sendCommand(Command.SET_INFO, String.format("APP %s", AppConstants.APP_ID));
        sendCommand(Command.SET_INFO, String.format("ACCOUNT %s", identity.getEmail()));
        List<String> friendSet = mService.getPlugin(FriendsPlugin.class).getStore().getFriendSet();
        sendCommand(Command.SET_INFO, String.format("FRIENDS %s", JSONValue.toJSONString(friendSet)));

        Map<String, Long> profile = new HashMap();
        profile.put("birthdate", identity.getBirthdate());
        profile.put("gender", (long) identity.getGender());
        sendCommand(Command.SET_INFO, String.format("PROFILE %s", JSONValue.toJSONString(profile)));

        keepAlive();
        L.d(String.format("Sending %d stashed commands", mStashedCommands.size()));
        for (String line : mStashedCommands) {
            sendLine(line);
        }
        mStashedCommands.clear();
        resendUnsentItems();
    }

    private void newsPush(String data) {
        JSONObject json = (JSONObject) JSONValue.parse(data);
        try {
            //noinspection unchecked
            final AppNewsItemTO newsItem = new AppNewsItemTO(json);
            mService.postOnBIZZHandler(new SafeRunnable() {
                @Override
                protected void safeRun() throws Exception {
                    mNewsChannelCallbackHandler.newsPush(newsItem);
                }
            });
        } catch (IncompleteMessageException e) {
            L.bug(String.format("Invalid news item received from update server (%s)\n: %s", e.getMessage(), data));
        }
    }

    private void newsRogerUpdate(String data) {
        String[] splitData = data.split(" ");
        final long newsId = Long.parseLong(splitData[0]);
        final String friendEmail = splitData[1];
        mNewsChannelCallbackHandler.newsRogerUpdate(newsId, friendEmail);
    }

    private void newsReadUpdate(String data) {
        String[] stats = data.split(" "); // news_id1 read_count1 news_id2 read_count2
        final Map<Long, Long> statsMap = new HashMap<>();
        for (int i = 0; i < stats.length; i += 2) {
            statsMap.put(Long.parseLong(stats[i]), Long.parseLong(stats[i + 1]));
        }
        mNewsChannelCallbackHandler.newsReadUpdate(statsMap);
    }

    private void newsStatsReceived(String data) {
        mNewsChannelCallbackHandler.newsStatsReceived(data);
    }

    public void readNews(Long newsId) {
        addCallToDB(CONFIG_TYPE_READ, newsId);
        sendCommand(Command.NEWS_READ, newsId.toString());
    }

    public void rogerNews(Long newsId) {
        addCallToDB(CONFIG_TYPE_ROGER, newsId);
        sendCommand(Command.NEWS_ROGER, newsId.toString());
    }

    public void statsNews(List<Long> newsIds) {
        T.dontCare();
        if (newsIds.size() == 0)
            return;
        sendCommand(Command.NEWS_STATS, android.text.TextUtils.join(" ", newsIds));
    }

    private void authenticate() {
        Credentials credentials = mService.getCredentials();
        String username = Base64.encodeBytes(credentials.getUsername().getBytes(Charset.forName("utf-8")),
                Base64.DONT_BREAK_LINES);
        String password = Base64.encodeBytes(credentials.getPassword().getBytes(Charset.forName("utf-8")),
                Base64.DONT_BREAK_LINES);
        sendLine(String.format("AUTH: %s %s", username, password));
    }

    private void addCallToDB(String type, Long newsId) {
        if (CONFIG_TYPE_READ.equals(type)) {
            mReadsToSend.add(newsId);
        } else if (CONFIG_TYPE_ROGER.equals(type)) {
            mRogersToSend.add(newsId);
        } else {
            L.e("addCallToDB with unknown type: " + type);
            return;
        }

        saveCallInDB(type);
    }

    private void ackNewsRead(String newsId) {
        L.d(String.format("News %s marked as read", newsId));
        removeCallFromDB(CONFIG_TYPE_READ, Long.parseLong(newsId));
    }

    private void ackNewsRoger(String newsId) {
        L.d(String.format("News %s marked as rogered", newsId));
        removeCallFromDB(CONFIG_TYPE_ROGER, Long.parseLong(newsId));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        L.e(cause);
        ctx.close();
        mIsConnected = false; // Not sure if necessary
        mIsRetryingToConnect = false;
    }

    private void removeCallFromDB(String type, Long newsId) {
        if (CONFIG_TYPE_READ.equals(type)) {
            mReadsToSend.remove(newsId);
        } else if (CONFIG_TYPE_ROGER.equals(type)) {
            mRogersToSend.remove(newsId);
        } else {
            L.e("removeCallFromDB with unknown type: " + type);
            return;
        }

        saveCallInDB(type);
    }

    private void saveCallInDB(String type) {
        JSONArray jsonNewsIds = new JSONArray();
        if (CONFIG_TYPE_READ.equals(type)) {
            for (Long readNewsId : mReadsToSend) {
                jsonNewsIds.add(readNewsId);
            }
        } else if (CONFIG_TYPE_ROGER.equals(type)) {
            for (Long rogerNewsId : mRogersToSend) {
                jsonNewsIds.add(rogerNewsId);
            }
        } else {
            L.e("saveCallInDB with unkown type: " + type);
            return;
        }
        Configuration cfg = mConfigurationProvider.getConfiguration(CONFIGKEY);
        cfg.put(type, JSONValue.toJSONString(jsonNewsIds));
        mConfigurationProvider.updateConfigurationNow(CONFIGKEY, cfg);
    }

    private void loadCallsFromDB() {
        Configuration cfg = mConfigurationProvider.getConfiguration(CONFIGKEY);
        final String readNewsIdsJSON = cfg.get(CONFIG_TYPE_READ, null);
        if (readNewsIdsJSON != null) {
            JSONArray jsonNewsIds = (JSONArray) JSONValue.parse(readNewsIdsJSON);
            for (Object jsonNewsId : jsonNewsIds) {
                mReadsToSend.add((Long) jsonNewsId);
            }
        }

        final String rogerNewsIdsJSON = cfg.get(CONFIG_TYPE_ROGER, null);
        if (rogerNewsIdsJSON != null) {
            JSONArray jsonNewsIds = (JSONArray) JSONValue.parse(rogerNewsIdsJSON);
            for (Object jsonNewsId : jsonNewsIds) {
                mRogersToSend.add((Long) jsonNewsId);
            }
        }
    }

    private void resendUnsentItems() {
        for (Long newsId : mReadsToSend) {
            sendCommand(Command.NEWS_READ, newsId.toString());
        }

        for (Long newsId : mRogersToSend) {
            sendCommand(Command.NEWS_ROGER, newsId.toString());
        }
    }
}