com.morlunk.mumbleclient.service.PlumbleService.java Source code

Java tutorial

Introduction

Here is the source code for com.morlunk.mumbleclient.service.PlumbleService.java

Source

/*
 * Copyright (C) 2014 Andrew Comminos
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.morlunk.mumbleclient.service;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.speech.tts.TextToSpeech;
import android.support.v4.app.NotificationCompat;

import com.morlunk.jumble.Constants;
import com.morlunk.jumble.JumbleService;
import com.morlunk.jumble.model.Message;
import com.morlunk.jumble.model.User;
import com.morlunk.jumble.util.JumbleObserver;
import com.morlunk.mumbleclient.R;
import com.morlunk.mumbleclient.Settings;
import com.morlunk.mumbleclient.app.PlumbleActivity;

import java.util.ArrayList;
import java.util.List;

/**
 * An extension of the Jumble service with some added Plumble-exclusive non-standard Mumble features.
 * Created by andrew on 28/07/13.
 */
public class PlumbleService extends JumbleService implements SharedPreferences.OnSharedPreferenceChangeListener {
    public static final String BROADCAST_TALK = "com.morlunk.mumbleclient.action.TALK";
    public static final String EXTRA_TALK_STATUS = "status";
    public static final String TALK_STATUS_ON = "on";
    public static final String TALK_STATUS_OFF = "off";
    public static final String TALK_STATUS_TOGGLE = "toggle";

    /** Undocumented constant that permits a proximity-sensing wake lock. */
    public static final int PROXIMITY_SCREEN_OFF_WAKE_LOCK = 32;
    public static final int TTS_THRESHOLD = 250; // Maximum number of characters to read

    public static final String BROADCAST_MUTE = "broadcast_mute";
    public static final String BROADCAST_DEAFEN = "broadcast_deafen";
    public static final String BROADCAST_TOGGLE_OVERLAY = "broadcast_toggle_overlay";

    public static final int STATUS_NOTIFICATION_ID = 1;

    private PlumbleBinder mBinder = new PlumbleBinder();
    private NotificationCompat.Builder mStatusNotificationBuilder;
    /** A list of messages to be displayed in the chat notification. Should be cleared when notification pressed. */
    private List<String> mUnreadMessages = new ArrayList<String>();

    private Settings mSettings;
    /** Channel view overlay. */
    private PlumbleOverlay mChannelOverlay;
    /** Proximity lock for handset mode. */
    private PowerManager.WakeLock mProximityLock;
    /** Play sound when push to talk key is pressed */
    private boolean mPTTSoundEnabled;

    private TextToSpeech mTTS;
    private TextToSpeech.OnInitListener mTTSInitListener = new TextToSpeech.OnInitListener() {
        @Override
        public void onInit(int status) {
            if (status == TextToSpeech.ERROR)
                log(Message.Type.WARNING, getString(R.string.tts_failed));
        }
    };

    /** The view representing the hot corner. */
    private PlumbleHotCorner mHotCorner;
    private PlumbleHotCorner.PlumbleHotCornerListener mHotCornerListener = new PlumbleHotCorner.PlumbleHotCornerListener() {
        @Override
        public void onHotCornerDown() {
            try {
                mBinder.setTalkingState(!mSettings.isPushToTalkToggle() || !mBinder.isTalking());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onHotCornerUp() {
            if (!mSettings.isPushToTalkToggle()) {
                try {
                    mBinder.setTalkingState(false);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    private BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (!isConnected())
                return;
            try {
                if (BROADCAST_MUTE.equals(intent.getAction())) {
                    User user = getBinder().getSessionUser();
                    boolean muted = !user.isSelfMuted();
                    boolean deafened = user.isSelfDeafened();
                    deafened &= muted;
                    getBinder().setSelfMuteDeafState(muted, deafened);
                } else if (BROADCAST_DEAFEN.equals(intent.getAction())) {
                    User user = getBinder().getSessionUser();
                    getBinder().setSelfMuteDeafState(!user.isSelfDeafened(), !user.isSelfDeafened());
                } else if (BROADCAST_TOGGLE_OVERLAY.equals(intent.getAction())) {
                    if (!mChannelOverlay.isShown())
                        mChannelOverlay.show();
                    else
                        mChannelOverlay.hide();
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };

    private BroadcastReceiver mTalkReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (!isConnected())
                return;
            try {
                if (BROADCAST_TALK.equals(intent.getAction())) {
                    String status = intent.getStringExtra(EXTRA_TALK_STATUS);
                    if (status == null)
                        status = TALK_STATUS_TOGGLE;
                    if (TALK_STATUS_ON.equals(status)) {
                        mBinder.setTalkingState(true);
                    } else if (TALK_STATUS_OFF.equals(status)) {
                        mBinder.setTalkingState(false);
                    } else if (TALK_STATUS_TOGGLE.equals(status)) {
                        mBinder.setTalkingState(!mBinder.isTalking());
                    }
                } else {
                    throw new UnsupportedOperationException();
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };

    private JumbleObserver mObserver = new JumbleObserver() {

        @Override
        public void onConnectionError(String message, boolean reconnecting) throws RemoteException {
            if (reconnecting) {
                String tickerMessage = getString(R.string.reconnecting, PlumbleActivity.RECONNECT_DELAY / 1000);
                if (mNotificationReceiver == null)
                    createNotification();
                updateNotificationTicker(tickerMessage);
                updateNotificationState();
            }
        }

        @Override
        public void onUserStateUpdated(User user) throws RemoteException {
            if (user.getSession() == mBinder.getSession()) {
                mSettings.setMutedAndDeafened(user.isSelfMuted(), user.isSelfDeafened()); // Update settings mute/deafen state
                updateNotificationState();
            }
        }

        @Override
        public void onMessageLogged(Message message) throws RemoteException {
            // Strip all HTML tags.
            String strippedMessage = message.getMessage().replaceAll("<[^>]*>", "");

            // Only read text messages. TODO: make this an option.
            if (message.getType() == Message.Type.TEXT_MESSAGE) {
                User actor = getBinder().getUser(message.getActor());
                String actorName = actor != null ? actor.getName() : getString(R.string.server);

                String formattedMessage = actorName + ": " + strippedMessage;
                mUnreadMessages.add(formattedMessage);

                if (mSettings.isChatNotifyEnabled() && mStatusNotificationBuilder != null) {
                    // Set the ticker to be the last message received
                    mStatusNotificationBuilder.setTicker(mUnreadMessages.get(mUnreadMessages.size() - 1));
                    updateNotificationState();
                }

                // Read if TTS is enabled, the message is less than threshold, is a text message, and not deafened
                if (mSettings.isTextToSpeechEnabled() && mTTS != null
                        && message.getType() == Message.Type.TEXT_MESSAGE
                        && strippedMessage.length() <= TTS_THRESHOLD && getBinder().getSessionUser() != null
                        && !getBinder().getSessionUser().isSelfDeafened()) {
                    mTTS.speak(formattedMessage, TextToSpeech.QUEUE_ADD, null);
                }
            }
        }

        @Override
        public void onPermissionDenied(String reason) throws RemoteException {
            if (!mSettings.isChatNotifyEnabled())
                return;
            updateNotificationTicker(reason);
        }

        @Override
        public void onUserTalkStateUpdated(User user) throws RemoteException {
            if (isConnected() && mBinder.getSession() == user.getSession()
                    && mBinder.getTransmitMode() == Constants.TRANSMIT_PUSH_TO_TALK
                    && user.getTalkState() == User.TalkState.TALKING && mPTTSoundEnabled) {
                AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
                audioManager.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, -1);
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        // Register for notification actions
        IntentFilter notificationIntentFilter = new IntentFilter();
        notificationIntentFilter.addAction(BROADCAST_MUTE);
        notificationIntentFilter.addAction(BROADCAST_DEAFEN);
        notificationIntentFilter.addAction(BROADCAST_TOGGLE_OVERLAY);
        registerReceiver(mNotificationReceiver, notificationIntentFilter);
        registerReceiver(mTalkReceiver, new IntentFilter(BROADCAST_TALK));

        try {
            getBinder().registerObserver(mObserver);
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        // Register for preference changes
        mSettings = Settings.getInstance(this);
        mPTTSoundEnabled = mSettings.isPttSoundEnabled();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        preferences.registerOnSharedPreferenceChangeListener(this);

        // Instantiate overlay view
        mChannelOverlay = new PlumbleOverlay(this);

        mHotCorner = new PlumbleHotCorner(this, mSettings.getHotCornerGravity(), mHotCornerListener);

        // Set up TTS
        if (mSettings.isTextToSpeechEnabled())
            mTTS = new TextToSpeech(this, mTTSInitListener);
    }

    @Override
    public void onDestroy() {
        stopForeground(true);
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        preferences.unregisterOnSharedPreferenceChangeListener(this);
        unregisterReceiver(mNotificationReceiver);
        unregisterReceiver(mTalkReceiver);
        try {
            getBinder().unregisterObserver(mObserver);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        if (mTTS != null)
            mTTS.shutdown();
        super.onDestroy();
    }

    @Override
    public void onConnectionEstablished() {
        super.onConnectionEstablished();
        // Restore mute/deafen state
        if (mSettings.isMuted() || mSettings.isDeafened()) {
            try {
                getBinder().setSelfMuteDeafState(mSettings.isMuted(), mSettings.isDeafened());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

    }

    @Override
    public void onConnectionSynchronized() {
        super.onConnectionSynchronized();
        createNotification();

        if (mSettings.isHotCornerEnabled()) {
            mHotCorner.setShown(true);
        }
        // Configure proximity sensor
        if (mSettings.isHandsetMode()) {
            setProximitySensorOn(true);
        }
    }

    @Override
    public void onConnectionDisconnected() {
        super.onConnectionDisconnected();
        // Remove overlay if present.
        mChannelOverlay.hide();

        mHotCorner.setShown(false);

        setProximitySensorOn(false);

        try {
            if (!getBinder().isReconnecting()) {
                hideNotification();
                //                stopSelf(); // Stop manual control of the service's lifecycle.
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * Called when the user makes a change to their preferences. Should update all preferences relevant to the service.
     */
    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (!isConnected())
            return; // These properties should all be set on connect regardless.

        if (Settings.PREF_INPUT_METHOD.equals(key)) {
            /* Convert input method defined in settings to an integer format used by Jumble. */
            int inputMethod = mSettings.getJumbleInputMethod();
            try {
                getBinder().setTransmitMode(inputMethod);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            mChannelOverlay.setPushToTalkShown(inputMethod == Constants.TRANSMIT_PUSH_TO_TALK);
        } else if (Settings.PREF_HANDSET_MODE.equals(key)) {
            setProximitySensorOn(mSettings.isHandsetMode());
        } else if (Settings.PREF_THRESHOLD.equals(key)) {
            try {
                getBinder().setVADThreshold(mSettings.getDetectionThreshold());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        } else if (Settings.PREF_HOT_CORNER_KEY.equals(key)) {
            mHotCorner.setGravity(mSettings.getHotCornerGravity());
            mHotCorner.setShown(mSettings.isHotCornerEnabled());
        } else if (Settings.PREF_USE_TTS.equals(key)) {
            if (mTTS == null && mSettings.isTextToSpeechEnabled())
                mTTS = new TextToSpeech(this, mTTSInitListener);
            else if (mTTS != null && !mSettings.isTextToSpeechEnabled()) {
                mTTS.shutdown();
                mTTS = null;
            }
        } else if (Settings.PREF_AMPLITUDE_BOOST.equals(key)) {
            try {
                getBinder().setAmplitudeBoost(mSettings.getAmplitudeBoostMultiplier());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        } else if (Settings.PREF_HALF_DUPLEX.equals(key)) {
            try {
                getBinder().setHalfDuplex(mSettings.isHalfDuplex());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        } else if (Settings.PREF_PTT_SOUND.equals(key)) {
            mPTTSoundEnabled = mSettings.isPttSoundEnabled();
        }
    }

    /**
     * Creates a new NotificationCompat.Builder configured for the service, and shows it.
     */
    public void createNotification() {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.drawable.ic_stat_notify);
        builder.setTicker(getResources().getString(R.string.plumbleConnected));
        builder.setContentTitle(getResources().getString(R.string.app_name));
        builder.setContentText(getResources().getString(R.string.connected));
        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
        builder.setOngoing(true);

        // Add notification triggers
        Intent muteIntent = new Intent(BROADCAST_MUTE);
        Intent deafenIntent = new Intent(BROADCAST_DEAFEN);
        Intent overlayIntent = new Intent(BROADCAST_TOGGLE_OVERLAY);

        if (isConnected()) {
            builder.addAction(R.drawable.ic_action_microphone, getString(R.string.mute),
                    PendingIntent.getBroadcast(this, 1, muteIntent, PendingIntent.FLAG_CANCEL_CURRENT));
            builder.addAction(R.drawable.ic_action_audio, getString(R.string.deafen),
                    PendingIntent.getBroadcast(this, 1, deafenIntent, PendingIntent.FLAG_CANCEL_CURRENT));
            builder.addAction(R.drawable.ic_action_channels, getString(R.string.overlay),
                    PendingIntent.getBroadcast(this, 2, overlayIntent, PendingIntent.FLAG_CANCEL_CURRENT));
        }

        Intent channelListIntent = new Intent(this, PlumbleActivity.class);

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, channelListIntent, 0);

        builder.setContentIntent(pendingIntent);

        mStatusNotificationBuilder = builder;
        startForeground(STATUS_NOTIFICATION_ID, builder.build());
    }

    /**
     * Updates the status notification with unread messages (if applicable), or status updates.
     */
    public void updateNotificationState() throws RemoteException {
        if (mStatusNotificationBuilder == null)
            return;
        String contentText;
        if (isConnected()) {
            User currentUser = mBinder.getSessionUser();
            if (currentUser.isSelfMuted() && currentUser.isSelfDeafened())
                contentText = getString(R.string.status_notify_muted_and_deafened);
            else if (currentUser.isSelfMuted())
                contentText = getString(R.string.status_notify_muted);
            else
                contentText = getString(R.string.connected);
        } else {
            contentText = getString(R.string.disconnected);
        }

        if (getBinder().isReconnecting()) {
            contentText = getString(R.string.reconnecting, PlumbleActivity.RECONNECT_DELAY / 1000);
        }

        if (!mUnreadMessages.isEmpty() && isConnected()) {
            NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
            for (String message : mUnreadMessages)
                inboxStyle.addLine(message);
            mStatusNotificationBuilder.setStyle(inboxStyle);
        } else {
            mStatusNotificationBuilder.setStyle(null);
        }

        mStatusNotificationBuilder.setContentText(contentText);
        startForeground(STATUS_NOTIFICATION_ID, mStatusNotificationBuilder.build());
    }

    public void updateNotificationTicker(String message) {
        if (mStatusNotificationBuilder == null)
            return;
        mStatusNotificationBuilder.setTicker(message);
        startForeground(STATUS_NOTIFICATION_ID, mStatusNotificationBuilder.build());
    }

    public void hideNotification() {
        stopForeground(true);
    }

    private void setProximitySensorOn(boolean on) {
        if (on) {
            PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
            mProximityLock = pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, "plumble_proximity");
            mProximityLock.acquire();
        } else {
            if (mProximityLock != null)
                mProximityLock.release();
            mProximityLock = null;
        }
    }

    @Override
    public PlumbleBinder getBinder() {
        return mBinder;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    /**
     * An extension of JumbleBinder to add Plumble-specific functionality.
     */
    public class PlumbleBinder extends JumbleBinder {
        public void setOverlayShown(boolean showOverlay) {
            if (!mChannelOverlay.isShown()) {
                mChannelOverlay.show();
            } else {
                mChannelOverlay.hide();
            }
        }

        public boolean isOverlayShown() {
            return mChannelOverlay.isShown();
        }

        public void clearChatNotifications() throws RemoteException {
            if (mUnreadMessages.size() > 0) {
                mUnreadMessages.clear();
                if (isConnected())
                    updateNotificationState();
            }
        }
    }
}