Java tutorial
/* * ChatService.java * * Copyright (C) 2011 IBR, TU Braunschweig * * Written-by: Johannes Morgenroth <morgenroth@ibr.cs.tu-bs.de> * * 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.socialdisasters.other.service; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Date; import java.util.Locale; import java.util.StringTokenizer; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; import android.media.AudioManager; import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.WearableExtender; import android.support.v4.app.RemoteInput; import android.support.v4.app.TaskStackBuilder; import android.util.Log; import de.tubs.ibr.dtn.api.Block; import de.tubs.ibr.dtn.api.Bundle; import de.tubs.ibr.dtn.api.Bundle.ProcFlags; import de.tubs.ibr.dtn.api.BundleID; import de.tubs.ibr.dtn.api.DTNClient; import de.tubs.ibr.dtn.api.DTNClient.Session; import de.tubs.ibr.dtn.api.DTNIntentService; import de.tubs.ibr.dtn.api.DataHandler; import de.tubs.ibr.dtn.api.GroupEndpoint; import de.tubs.ibr.dtn.api.Registration; import de.tubs.ibr.dtn.api.ServiceNotAvailableException; import de.tubs.ibr.dtn.api.SessionDestroyedException; import de.tubs.ibr.dtn.api.SingletonEndpoint; import de.tubs.ibr.dtn.api.TransferMode; import com.socialdisasters.other.MainActivity; import com.socialdisasters.R; import com.socialdisasters.other.ReplyActivity; import com.socialdisasters.other.RosterAdapter; import com.socialdisasters.other.core.Buddy; import com.socialdisasters.other.core.Message; import com.socialdisasters.other.core.Roster; public class ChatService extends DTNIntentService { public enum Debug { NOTIFICATION, BUDDY_ADD, SEND_PRESENCE, UNREGISTER } private static final String TAG = "ChatService"; public static final String EXTRA_BUDDY_ID = "com.socialdisasters.other.BUDDY_ID"; public static final String EXTRA_TEXT_BODY = "com.socialdisasters.other.TEXT_BODY"; public static final String EXTRA_DISPLAY_NAME = "com.socialdisasters.other.DISPLAY_NAME"; public static final String EXTRA_PRESENCE = "com.socialdisasters.other.EXTRA_PRESENCE"; public static final String EXTRA_STATUS = "com.socialdisasters.other.EXTRA_STATUS"; public static final String EXTRA_VOICE_REPLY = "extra_voice_reply"; // mark a specific bundle as delivered public static final String MARK_DELIVERED_INTENT = "com.socialdisasters.other.MARK_DELIVERED"; public static final String REPORT_DELIVERED_INTENT = "com.socialdisasters.other.REPORT_DELIVERED"; public static final String ACTION_NEW_MESSAGE = "com.socialdisasters.other.ACTION_NEW_MESSAGE"; public static final String ACTION_PRESENCE_ALARM = "com.socialdisasters.other.PRESENCE_ALARM"; public static final String ACTION_SEND_MESSAGE = "com.socialdisasters.other.SEND_MESSAGE"; public static final String ACTION_REFRESH_PRESENCE = "com.socialdisasters.other.REFRESH_PRESENCE"; public static final String ACTION_USERINFO_UPDATED = "com.socialdisasters.other.USERINFO_UPDATED"; private static final int MESSAGE_NOTIFICATION = 1; private static final int REFRESH_BUDDY_DATA = 2; public static final String ACTION_OPENCHAT = "com.socialdisasters.other.OPENCHAT"; public static final GroupEndpoint PRESENCE_GROUP_EID = new GroupEndpoint("dtn://chat.dtn/presence"); private ServiceError _service_error = ServiceError.NO_ERROR; private boolean _unregister_on_destroy = false; private final static String FALLBACK_NICKNAME = "Nobody"; // This is the object that receives interactions from clients. See // RemoteService for a more complete example. private final IBinder mBinder = new LocalBinder(); // local roster with the connection to the database private Roster roster = null; // DTN client to talk with the DTN service private DTNClient.Session _session = null; // handler for scheduled refreshes private UpdateHandler mUpdateHandler = null; private HandlerThread mUpdateThread = null; public ChatService() { super(TAG); } public final static class UpdateHandler extends Handler { private Roster mRoster = null; public UpdateHandler(Looper looper, Roster roster) { super(looper); mRoster = roster; } @Override public void handleMessage(android.os.Message msg) { super.handleMessage(msg); if (msg.what == REFRESH_BUDDY_DATA) { String endpoint = (String) msg.obj; Long buddyId = mRoster.getBuddyId(endpoint); mRoster.notifyBuddyChanged(buddyId); } } } private DataHandler _data_handler = new DataHandler() { ByteArrayOutputStream stream = null; Bundle current; Long flags = 0L; public void startBundle(Bundle bundle) { this.current = bundle; this.flags = 0L; if (bundle.get(Bundle.ProcFlags.DTNSEC_STATUS_CONFIDENTIAL)) { this.flags |= Message.FLAG_ENCRYPTED; } if (bundle.get(Bundle.ProcFlags.DTNSEC_STATUS_VERIFIED)) { this.flags |= Message.FLAG_SIGNED; } } public void endBundle() { de.tubs.ibr.dtn.api.BundleID received = new de.tubs.ibr.dtn.api.BundleID(this.current); // run the queue and delivered process asynchronously Intent i = new Intent(ChatService.this, ChatService.class); i.setAction(MARK_DELIVERED_INTENT); i.putExtra("bundleid", received); startService(i); this.current = null; } public TransferMode startBlock(Block block) { // ignore messages with a size larger than 8k if ((block.length > 8196) || (block.type != 1)) return TransferMode.NULL; // create a new bytearray output stream stream = new ByteArrayOutputStream(); return TransferMode.SIMPLE; } public void endBlock() { if (stream != null) { String msg = new String(stream.toByteArray()); stream = null; if (current.getDestination().equals(PRESENCE_GROUP_EID)) { eventNewPresence(current.getSource(), current.getTimestamp().getDate(), msg, flags); } else { eventNewMessage(current.getSource(), current.getTimestamp().getDate(), msg, flags); } } } public void payload(byte[] data) { if (stream == null) return; // write data to the stream try { stream.write(data); } catch (IOException e) { Log.e(TAG, "error on writing payload", e); } } public ParcelFileDescriptor fd() { return null; } public void progress(long current, long length) { } private void eventNewPresence(SingletonEndpoint source, Date created, String payload, Long flags) { Log.i(TAG, "Presence received from " + source); // buddy info String nickname = null; String presence = null; String status = null; String voiceeid = null; String language = null; String country = null; StringTokenizer tokenizer = new StringTokenizer(payload, "\n"); while (tokenizer.hasMoreTokens()) { String data = tokenizer.nextToken(); // search for the delimiter int delimiter = data.indexOf(':'); // if the is no delimiter, ignore the line if (delimiter == -1) return; // split the keyword and data pair String keyword = data.substring(0, delimiter); String value = data.substring(delimiter + 1, data.length()).trim(); if (keyword.equalsIgnoreCase("Presence")) { presence = value; } else if (keyword.equalsIgnoreCase("Nickname")) { nickname = value; } else if (keyword.equalsIgnoreCase("Status")) { status = value; } else if (keyword.equalsIgnoreCase("Voice")) { voiceeid = value; } else if (keyword.equalsIgnoreCase("Language")) { language = value; } else if (keyword.equalsIgnoreCase("Country")) { country = value; } } if (nickname != null) { getRoster().updatePresence(source.toString(), created, presence, nickname, status, voiceeid, language, country, flags); // set timer to update buddy status if presence is not 'unavailable' if (!"unavailable".equals(presence)) { // obtain a new refresh message android.os.Message msg = mUpdateHandler.obtainMessage(REFRESH_BUDDY_DATA, source.toString()); // schedule the update in 61 minutes mUpdateHandler.sendMessageDelayed(msg, 61 * 60 * 1000); Log.d(TAG, "scheduled update for " + source.toString()); } } } private void eventNewMessage(SingletonEndpoint source, Date created, String payload, Long flags) { if (source == null) { Log.e(TAG, "message source is null!"); } // create a new message Long msgId = getRoster().createMessage(source.toString(), created, new Date(), true, payload, flags); // retrieve message object Message msg = getRoster().getMessage(msgId); // get buddy object Buddy b = getRoster().getBuddy(msg.getBuddyId()); // create a notification createNotification(b, msg); // create a status bar notification Log.i(TAG, "New message received!"); } }; /** * Class for clients to access. Because we know this service always * runs in the same process as its clients, we don't need to deal with * IPC. */ public class LocalBinder extends Binder { public ChatService getService() { return ChatService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onCreate() { // call onCreate of the super-class super.onCreate(); // create a roster object this.roster = new Roster(); this.roster.open(this); // create a new background looper mUpdateThread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND); mUpdateThread.start(); Looper looper = mUpdateThread.getLooper(); mUpdateHandler = new UpdateHandler(looper, roster); // create registration Registration registration = new Registration("chat"); registration.add(PRESENCE_GROUP_EID); try { initialize(registration); _service_error = ServiceError.NO_ERROR; } catch (ServiceNotAvailableException e) { _service_error = ServiceError.SERVICE_NOT_FOUND; } catch (SecurityException ex) { _service_error = ServiceError.PERMISSION_NOT_GRANTED; } // register to presence changes SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.registerOnSharedPreferenceChangeListener(mPrefListener); // schedule update actions for all online buddies Cursor c = getRoster().queryOnlineBuddies(); RosterAdapter.ColumnsMap cmap = new RosterAdapter.ColumnsMap(); while (c.moveToNext()) { Buddy b = new Buddy(this, c, cmap); // obtain a new refresh message android.os.Message msg = mUpdateHandler.obtainMessage(REFRESH_BUDDY_DATA, b.getEndpoint()); // schedule the update in 61 minutes mUpdateHandler.sendMessageDelayed(msg, 61 * 60 * 1000); Log.d(TAG, "scheduled update for " + b.getEndpoint()); } Log.i(TAG, "service created."); } private SharedPreferences.OnSharedPreferenceChangeListener mPrefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if ("presencetag".equals(key)) { String announced = prefs.getString("announced-presence", "unavailable"); String desired = prefs.getString(key, "unavailable"); // if desired presence different to the announced if (!announced.equals(desired)) { if (desired.equals("unavailable")) { // deactivate presence generator PresenceGenerator.deactivate(ChatService.this); } else { // activate presence generator PresenceGenerator.activate(ChatService.this); } // set new presence as announced prefs.edit().putString("announced-presence", desired).commit(); // broadcast presence Intent intent = new Intent(ChatService.this, ChatService.class); intent.setAction(ChatService.ACTION_PRESENCE_ALARM); startService(intent); } } } }; public ServiceError getServiceError() { return _service_error; } @Override public void onDestroy() { // unregister preference listener SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.unregisterOnSharedPreferenceChangeListener(mPrefListener); try { // stop looper mUpdateThread.quit(); mUpdateThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Wait for looper thread was interrupted.", e); } // close the roster (plus db connection) this.roster.close(); // destroy the session if the unregister option is enabled if (_unregister_on_destroy && _session != null) _session.destroy(); // clear all variables this.roster = null; super.onDestroy(); Log.i(TAG, "service destroyed."); } public synchronized Roster getRoster() { return this.roster; } public synchronized String getLocalNickname() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ChatService.this); String presence_nick = preferences.getString("editNickname", ""); if (presence_nick.length() == 0) { String endpoint = getClient().getEndpoint(); if (endpoint == null) return FALLBACK_NICKNAME; return Roster.generateNickname(endpoint); } return presence_nick; } public static void cancelNotification(Context context, Long buddyId) { if (buddyId == null) return; NotificationManager mNotificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.cancel(buddyId.toString(), ChatService.MESSAGE_NOTIFICATION); } @SuppressWarnings("deprecation") private void showNotification(Intent intent) { int defaults = 0; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (prefs.getBoolean("vibrateOnMessage", true)) { defaults |= Notification.DEFAULT_VIBRATE; } Long buddyId = intent.getLongExtra(EXTRA_BUDDY_ID, -1L); String displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME); String textBody = intent.getStringExtra(EXTRA_TEXT_BODY); CharSequence tickerText = getString(R.string.new_message_from) + " " + displayName; CharSequence contentTitle = getString(R.string.new_message); CharSequence contentText = displayName + ":\n" + textBody; TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); // forward intent to the activity intent.setClass(this, MainActivity.class); // Adds the intent to the main view stackBuilder.addNextIntent(intent); // Gets a PendingIntent containing the entire back stack PendingIntent contentIntent = stackBuilder.getPendingIntent(buddyId.intValue(), PendingIntent.FLAG_UPDATE_CURRENT); // create a reply intent String replyLabel = getResources().getString(R.string.reply_label); RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_VOICE_REPLY).setLabel(replyLabel).build(); Intent replyIntent = new Intent(this, ReplyActivity.class); replyIntent.putExtra(EXTRA_BUDDY_ID, buddyId); PendingIntent replyPendingIntent = PendingIntent.getActivity(this, buddyId.intValue(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action action = new NotificationCompat.Action.Builder(R.drawable.ic_action_reply, getString(R.string.reply_label), replyPendingIntent).addRemoteInput(remoteInput).build(); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setContentTitle(contentTitle); builder.setContentText(contentText); builder.setSmallIcon(R.drawable.ic_stat_message); builder.setTicker(tickerText); builder.setDefaults(defaults); builder.setWhen(System.currentTimeMillis()); builder.setContentIntent(contentIntent); builder.setLights(0xffff0000, 300, 1000); builder.setSound( Uri.parse(prefs.getString("ringtoneOnMessage", "content://settings/system/notification_sound"))); builder.extend(new WearableExtender().addAction(action)); builder.setAutoCancel(false); Notification notification = builder.getNotification(); NotificationManager mNotificationManager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE); mNotificationManager.notify(buddyId.toString(), MESSAGE_NOTIFICATION, notification); if (prefs.getBoolean("ttsWhenOnHeadset", false)) { AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); if (am.isBluetoothA2dpOn() || am.isWiredHeadsetOn()) { // speak the notification Intent tts_intent = new Intent(this, TTSService.class); tts_intent.setAction(TTSService.INTENT_SPEAK); tts_intent.putExtra("speechText", tickerText + ": " + textBody); startService(tts_intent); } } } private void createNotification(Buddy b, Message msg) { Intent intent = new Intent(ACTION_NEW_MESSAGE); intent.putExtra(EXTRA_BUDDY_ID, b.getId()); intent.putExtra(EXTRA_DISPLAY_NAME, b.getNickname()); intent.putExtra(EXTRA_TEXT_BODY, msg.getPayload()); // send intent to activity or broadcast receiver for notification sendOrderedBroadcast(intent, null); } @Override protected void onHandleIntent(Intent intent) { // stop processing if the session is not assigned if (_session == null) return; String action = intent.getAction(); // create a task to process concurrently if (ACTION_PRESENCE_ALARM.equals(action)) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ChatService.this); // check if the screen is active PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); Boolean screenOn = pm.isScreenOn(); String presence_tag = preferences.getString("presencetag", "unavailable"); String presence_nick = getLocalNickname(); String presence_text = preferences.getString("statustext", ""); if (presence_tag.equals("auto")) { if (screenOn) { presence_tag = "chat"; } else { presence_tag = "away"; } } Log.i(TAG, "push out presence; " + presence_tag); actionRefreshPresence(presence_tag, presence_nick, presence_text); Editor edit = preferences.edit(); edit.putLong("lastpresenceupdate", (new Date().getTime())); edit.commit(); } // create a task to check for messages else if (de.tubs.ibr.dtn.Intent.RECEIVE.equals(action)) { // wait until connected try { while (_session.queryNext()) ; } catch (SessionDestroyedException e) { Log.e(TAG, "Can not query for bundle", e); } } else if (MARK_DELIVERED_INTENT.equals(action)) { BundleID bundleid = intent.getParcelableExtra("bundleid"); if (bundleid == null) { Log.e(TAG, "Intent to mark a bundle as delivered, but no bundle ID given"); return; } try { _session.delivered(bundleid); } catch (Exception e) { Log.e(TAG, "Can not mark bundle as delivered.", e); } } else if (REPORT_DELIVERED_INTENT.equals(action)) { SingletonEndpoint source = intent.getParcelableExtra("source"); BundleID bundleid = intent.getParcelableExtra("bundleid"); if (bundleid == null) { Log.e(TAG, "Intent to mark a bundle as delivered, but no bundle ID given"); return; } synchronized (this.roster) { // report delivery to the roster getRoster().reportDelivery(source, bundleid); } } else if (ACTION_SEND_MESSAGE.equals(action)) { Long buddyId = intent.getLongExtra(ChatService.EXTRA_BUDDY_ID, -1); String text = intent.getStringExtra(ChatService.EXTRA_TEXT_BODY); // abort if there is no buddyId if (buddyId < 0) return; actionSendMessage(buddyId, text); } else if (ACTION_REFRESH_PRESENCE.equals(action)) { String presence = intent.getStringExtra(ChatService.EXTRA_PRESENCE); String nickname = intent.getStringExtra(ChatService.EXTRA_DISPLAY_NAME); String status = intent.getStringExtra(ChatService.EXTRA_STATUS); actionRefreshPresence(presence, nickname, status); } else if (ACTION_NEW_MESSAGE.equals(action)) { showNotification(intent); } } private void actionSendMessage(Long buddyId, String text) { try { Long msgId = getRoster().createMessage(buddyId, new Date(), new Date(), false, text, 0L); // load buddy from roster Buddy buddy = getRoster().getBuddy(buddyId); // create a new bundle Bundle b = new Bundle(); b.setDestination(new SingletonEndpoint(buddy.getEndpoint())); String lifetime = PreferenceManager.getDefaultSharedPreferences(this).getString("messageduration", "259200"); b.setLifetime(Long.parseLong(lifetime)); // set status report requests b.set(ProcFlags.REQUEST_REPORT_OF_BUNDLE_DELIVERY, true); b.setReportto(SingletonEndpoint.ME); // request encryption for this message b.set(ProcFlags.DTNSEC_REQUEST_ENCRYPT, true); // request signing of the message b.set(ProcFlags.DTNSEC_REQUEST_SIGN, true); synchronized (this.roster) { // send out the message BundleID ret = _session.send(b, text.getBytes()); if (ret == null) { Log.e(TAG, "could not send the message"); } else { Log.d(TAG, "Bundle sent, BundleID: " + ret.toString()); } // update message into the database getRoster().reportSent(msgId, ret.toString()); } } catch (SessionDestroyedException e) { Log.e(TAG, "Can not send message", e); } } public void actionRefreshPresence(String presence, String nickname, String status) { try { String presence_message = "Presence: " + presence + "\n" + "Nickname: " + nickname + "\n" + "Status: " + status + "\n" + "Language: " + Locale.getDefault().getLanguage() + "\n" + "Country: " + Locale.getDefault().getCountry(); if (Utils.isVoiceRecordingSupported(this)) { String endpoint = getClient().getEndpoint(); if (endpoint != null) { presence_message += "\n" + "Voice: " + endpoint + "/dtalkie"; } } // create a new bundle Bundle b = new Bundle(); // set destination to group endpoint b.setDestination(ChatService.PRESENCE_GROUP_EID); // set lifetime to one hour b.setLifetime(3600L); // request signing of the message b.set(ProcFlags.DTNSEC_REQUEST_SIGN, true); BundleID ret = _session.send(b, presence_message.getBytes()); if (ret == null) { Log.e(TAG, "could not send the message"); } else { Log.d(TAG, "Presence sent, BundleID: " + ret.toString()); } } catch (SessionDestroyedException e) { Log.e(TAG, "Can not send presence", e); } } public void startDebug(Debug d) { String debug_source = "dtn://debug/chat"; switch (d) { case NOTIFICATION: // create a new message Long msgId = getRoster().createMessage(debug_source, new Date(), new Date(), true, "Hello World", 0L); // retrieve message object Message msg = getRoster().getMessage(msgId); // get buddy object Buddy b = getRoster().getBuddy(msg.getBuddyId()); // create a notification createNotification(b, msg); break; case BUDDY_ADD: getRoster().updatePresence(debug_source + "/" + String.valueOf((new Date()).getTime()), new Date(), "online", "Debug Buddy", "Hello World", "dtn://test/dtalkie", "en", "gb", 0L); break; case SEND_PRESENCE: // wake-up the chat service and queue a send presence task Intent i = new Intent(this, ChatService.class); i.setAction(ACTION_PRESENCE_ALARM); startService(i); break; case UNREGISTER: _unregister_on_destroy = true; break; } } @Override protected void onSessionConnected(Session session) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); /** * Upgrade from "checkBroadcastPresence" option */ if (prefs.contains("checkBroadcastPresence")) { if (!prefs.getBoolean("checkBroadcastPresence", false)) { prefs.edit().putString("presencetag", "unavailable").remove("checkBroadcastPresence").commit(); } else { prefs.edit().remove("checkBroadcastPresence").commit(); } } /** * Activate presence by default */ if (!prefs.contains("presencetag")) prefs.edit().putString("presencetag", "auto").commit(); mPrefListener.onSharedPreferenceChanged(prefs, "presencetag"); // register own data handler for incoming bundles session.setDataHandler(_data_handler); // store session locally _session = session; // signal updated user info Intent i = new Intent(ACTION_USERINFO_UPDATED); sendBroadcast(i); } @Override protected void onSessionDisconnected() { _session = null; } }