package com.emmaguy.todayilearned; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; import android.support.wearable.activity.ConfirmationActivity; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.emmaguy.todayilearned.comments.ActionReceiver; import com.emmaguy.todayilearned.comments.CommentsActivity; import com.emmaguy.todayilearned.sharedlib.Constants; import com.emmaguy.todayilearned.sharedlib.Post; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.data.FreezableUtils; import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataEvent; import com.google.android.gms.wearable.DataEventBuffer; import com.google.android.gms.wearable.DataMap; import com.google.android.gms.wearable.DataMapItem; import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.PutDataMapRequest; import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.WearableListenerService; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class NotificationListenerService extends WearableListenerService { private static final String GROUP_KEY_SUBREDDIT_POSTS = "group_key_subreddit_posts"; private static final String EXTRA_VOICE_REPLY = "extra_voice_reply"; private static final String ACTION_RESPONSE = "com.emmaguy.todayilearned.Reply"; private static final long TIMEOUT_MS = 30 * 1000; private static final int REQUEST_CODE_VOTE_UP = 0; private static final int REQUEST_CODE_VOTE_DOWN = 1; private static final int REQUEST_CODE_OPEN_ON_PHONE = 2; private static final int REQUEST_CODE_SAVE_TO_POCKET = 3; private static final int REQUEST_CODE_REPLY = 4; private static final int REQUEST_VIEW_COMMENTS = 5; private static final int REQUEST_VIEW_FULLSCREEN_IMAGE = 6; private static final int NOTIFICATION_ID_INCREMENT = 10; private static int sNotificationId = 0; private final Gson mGson = new Gson(); private GoogleApiClient mGoogleApiClient; private Handler mHandler; @Override public void onCreate() { super.onCreate(); mGoogleApiClient = new GoogleApiClient.Builder(this).addApi(Wearable.API).build(); mGoogleApiClient.connect(); mHandler = new Handler(); } @Override public void onMessageReceived(MessageEvent messageEvent) { final String path = messageEvent.getPath(); boolean finishActivity = false; String message = ""; Logger.log("onMessageReceived, path: " + path); if (path.equals(Constants.PATH_POST_REPLY_RESULT_SUCCESS)) { message = getString(R.string.reply_successful); } else if (path.equals(Constants.PATH_POST_REPLY_RESULT_FAILURE)) { message = getString(R.string.reply_failed_sad_face); } else if (path.equals(Constants.PATH_SAVE_TO_POCKET_RESULT_SUCCESS)) { message = getString(R.string.saving_to_pocket_succeeded); } else if (path.equals(Constants.PATH_SAVE_TO_POCKET_RESULT_FAILED)) { message = getString(R.string.saving_to_pocket_failed_sad_face); } else if (path.equals(Constants.PATH_VOTE_RESULT_FAILED)) { message = getString(R.string.voting_failed); } else if (path.equals(Constants.PATH_VOTE_RESULT_SUCCESS)) { message = getString(R.string.voting_succeded); } else if (path.equals(Constants.PATH_GET_COMMENTS_RESULT_FAILED)) { message = getString(R.string.retrieving_comments_failed); } else if (path.equals(Constants.PATH_NO_NEW_POSTS)) { message = getString(R.string.no_posts_to_retrieve); finishActivity = true; } logToPhone("Message received, path: " + path + " message: " + message + " finishActivity: " + finishActivity); updateUser(message, finishActivity); } private void logToPhone(@NonNull String message) { logErrorToPhone(message, null); } private void logErrorToPhone(@NonNull String message, @Nullable Exception e) { PutDataMapRequest mapRequest = PutDataMapRequest.create(Constants.PATH_LOGGING); mapRequest.getDataMap() .putString(Constants.PATH_KEY_MESSAGE, message + " " + getExceptionAsString(e)); mapRequest.getDataMap().putLong("timestamp", System.currentTimeMillis()); PutDataRequest request = mapRequest.asPutDataRequest(); Wearable.DataApi.putDataItem(mGoogleApiClient, request) .setResultCallback(new ResultCallback<DataApi.DataItemResult>() { @Override public void onResult(DataApi.DataItemResult dataItemResult) { Logger.log("Result from sending log to phone: " + dataItemResult.getStatus()); } }); } @NonNull private String getExceptionAsString(@Nullable Exception e) { if (e == null) { return ""; } return e.getMessage() + ", " + Log.getStackTraceString(e); } private void updateUser(String message, final boolean finishActivity) { if (!TextUtils.isEmpty(message)) { final String msg = message; mHandler.post(new Runnable() { @Override public void run() { if (finishActivity) { sendBroadcast(new Intent(getString(R.string.force_finish_main_activity))); } Toast.makeText(NotificationListenerService.this, msg, Toast.LENGTH_LONG).show(); } }); } } private Bitmap loadBitmapFromAsset(Asset asset) { Logger.log("loadBitmapFromAsset"); ConnectionResult result = mGoogleApiClient.blockingConnect(TIMEOUT_MS, TimeUnit.MILLISECONDS); if (!result.isSuccess()) { return null; } // convert asset into a file descriptor and block until it's ready InputStream assetInputStream = Wearable.DataApi.getFdForAsset(mGoogleApiClient, asset) .await() .getInputStream(); mGoogleApiClient.disconnect(); if (assetInputStream == null) { Logger.log("Requested an unknown Asset"); return null; } // decode the stream into a bitmap return BitmapFactory.decodeStream(assetInputStream); } @Override public void onDataChanged(DataEventBuffer dataEvents) { final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); dataEvents.close(); if (!mGoogleApiClient.isConnected()) { ConnectionResult connectionResult = mGoogleApiClient.blockingConnect(30, TimeUnit.SECONDS); if (!connectionResult.isSuccess()) { logToPhone("onDataChanged, service failed to connect: " + connectionResult); return; } } String msg = ""; for (DataEvent event : events) { msg += "Event type: " + event.getType(); if (event.getType() == DataEvent.TYPE_CHANGED) { String path = event.getDataItem().getUri().getPath(); msg += ", path: " + path; Logger.log("onDataChanged, path: " + path); if (path.equals(Constants.PATH_LOGGING)) { return; } else if (path.equals(Constants.PATH_REDDIT_POSTS)) { try { DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem()); DataMap dataMap = dataMapItem.getDataMap(); final String latestPosts = dataMap.getString(Constants.KEY_REDDIT_POSTS); final boolean openOnPhoneDismisses = dataMap.getBoolean(Constants.KEY_DISMISS_AFTER_ACTION); final ArrayList<Integer> actionOrder = dataMap.getIntegerArrayList(Constants.KEY_ACTION_ORDER); List<Post> posts = mGson.fromJson(latestPosts, new TypeToken<List<Post>>() { }.getType()); Bitmap themeBlueBitmap = Bitmap.createBitmap(new int[]{getResources().getColor( R.color.primary)}, 1, 1, Bitmap.Config.ARGB_8888); NotificationManager notificationManager = (NotificationManager) getSystemService( NOTIFICATION_SERVICE); msg += ", posts: " + posts.size(); for (int i = 0; i < posts.size(); i++) { Post post = posts.get(i); createNotificationForPost(dataMap, openOnPhoneDismisses, actionOrder, themeBlueBitmap, notificationManager, post); } } catch (Exception e) { logErrorToPhone("Failed to get reddit posts from data event", e); } } else if (path.equals(Constants.PATH_COMMENTS)) { DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem()); DataMap dataMap = dataMapItem.getDataMap(); final String comments = dataMap.getString(Constants.KEY_REDDIT_POSTS); msg += ", comments: " + (TextUtils.isEmpty(comments) ? "empty" : comments.length()); if (!TextUtils.isEmpty(comments)) { logToPhone("Comments received, starting activity"); Intent intent = new Intent(this, CommentsActivity.class); intent.putExtra(Constants.KEY_REDDIT_POSTS, comments); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } } } else if (event.getType() == DataEvent.TYPE_DELETED) { String path = event.getDataItem().getUri().getPath(); msg += ", path: " + path; } } logToPhone(msg); } private void createNotificationForPost(DataMap dataMap, boolean openOnPhoneDismisses, ArrayList<Integer> actionOrder, Bitmap themeBlueBitmap, NotificationManager notificationManager, Post post) { try { Bitmap backgroundBitmap = null; if (dataMap.containsKey(post.getId()) && dataMap.getAsset(post.getId()) != null) { backgroundBitmap = loadBitmapFromAsset(dataMap.getAsset(post.getId())); } NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setContentTitle( post.getTitle()) .setContentText(post.getPostContents()) .setSmallIcon(R.drawable.ic_launcher); boolean hasCachedImage; if (backgroundBitmap != null) { // If the post has a thumbnail, use it - this will filter out nfsw etc thumbnails // but will still allow the user to see the full image if they like builder.setLargeIcon(backgroundBitmap); hasCachedImage = cacheBackgroundToDisk(sNotificationId, backgroundBitmap); } else { hasCachedImage = false; setBlueBackground(themeBlueBitmap, builder); enableNotificationGrouping(builder); } addActions(actionOrder, openOnPhoneDismisses, hasCachedImage, post, sNotificationId, builder); if (hasCachedImage) { // When the notification is dismissed, we will remove this image from the file cache builder.setDeleteIntent(getDeletePendingIntent(sNotificationId)); } notificationManager.notify(sNotificationId, builder.build()); if (backgroundBitmap != null) { backgroundBitmap.recycle(); } sNotificationId += NOTIFICATION_ID_INCREMENT; sendBroadcast(new Intent(getString(R.string.force_finish_main_activity))); } catch (Exception e) { logErrorToPhone("Failed to create notification for post: " + post, e); } } private void enableNotificationGrouping(NotificationCompat.Builder builder) { // if it's not got an image we can group it with the other text based ones builder.setGroup(GROUP_KEY_SUBREDDIT_POSTS); } private void setBlueBackground(Bitmap themeBlueBitmap, NotificationCompat.Builder builder) { NotificationCompat.WearableExtender extender = new NotificationCompat.WearableExtender(); extender.setBackground(themeBlueBitmap); builder.extend(extender); } private boolean cacheBackgroundToDisk(int notificationId, Bitmap backgroundBitmap) { boolean isCached = false; File localCache = new File(getCacheDir(), getCachedImageName(notificationId)); FileOutputStream out = null; try { out = new FileOutputStream(localCache); backgroundBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); } catch (IOException e) { logErrorToPhone("Error writing local cache", e); } finally { try { if (out != null) { out.close(); } isCached = true; } catch (IOException e) { logErrorToPhone("Error closing local cache file", e); } } return isCached; } // Add the actions to the builder, based on the given actionOrder list private void addActions(ArrayList<Integer> actionOrder, boolean openOnPhoneDismisses, boolean hasCachedImage, Post post, int notificationId, NotificationCompat.Builder builder) { for (int i = 0; i < actionOrder.size(); i++) { int order = actionOrder.get(i); if (TextUtils.isEmpty(post.getPermalink())) { logToPhone("Open on phone permalink: " + post); } switch (order) { case Constants.ACTION_ORDER_VIEW_COMMENTS: builder.addAction(new NotificationCompat.Action.Builder(R.drawable.view_comments, getString(R.string.view_comments), getViewCommentsPendingIntent(post, notificationId)).build()); break; case Constants.ACTION_ORDER_REPLY: builder.addAction(new NotificationCompat.Action.Builder(R.drawable.reply, getString(R.string.reply_to_x, post.getShortTitle()), getReplyPendingIntent(post, notificationId)).addRemoteInput(new RemoteInput.Builder( EXTRA_VOICE_REPLY).build()).build()); break; case Constants.ACTION_ORDER_UPVOTE: builder.addAction(new NotificationCompat.Action.Builder(R.drawable.upvote, getString(R.string.upvote_x, post.getShortTitle()), getVotePendingIntent(post, 1, REQUEST_CODE_VOTE_UP + notificationId)).build()); break; case Constants.ACTION_ORDER_DOWNVOTE: builder.addAction(new NotificationCompat.Action.Builder(R.drawable.downvote, getString(R.string.downvote_x, post.getShortTitle()), getVotePendingIntent(post, -1, REQUEST_CODE_VOTE_DOWN + notificationId)) .build()); break; case Constants.ACTION_ORDER_SAVE_TO_POCKET: builder.addAction(new NotificationCompat.Action.Builder(R.drawable.pocket, getString(R.string.save_to_pocket), getSaveToPocketPendingIntent(post.getPermalink(), notificationId)).build()); break; case Constants.ACTION_ORDER_OPEN_ON_PHONE: builder.addAction(new NotificationCompat.Action.Builder(R.drawable.open_on_phone, getString(R.string.open_on_phone), getOpenOnPhonePendingIntent(post.getPermalink(), openOnPhoneDismisses, notificationId)).build()); break; case Constants.ACTION_ORDER_VIEW_IMAGE: if (hasCachedImage) { builder.addAction(new NotificationCompat.Action.Builder(R.drawable.view_image, getString(R.string.view_image), getViewImagePendingIntent(notificationId)).build()); } break; } } } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (null == intent || null == intent.getAction()) { return Service.START_STICKY; } String action = intent.getAction(); if (action.equals(ACTION_RESPONSE)) { Bundle remoteInputResults = RemoteInput.getResultsFromIntent(intent); CharSequence replyMessage = ""; if (remoteInputResults != null) { replyMessage = remoteInputResults.getCharSequence(EXTRA_VOICE_REPLY); } String subject = intent.getStringExtra(Constants.PATH_KEY_MESSAGE_SUBJECT); String toUser = intent.getStringExtra(Constants.PATH_KEY_MESSAGE_TO_USER); String fullname = intent.getStringExtra(Constants.PATH_KEY_POST_FULLNAME); boolean isDirectMessage = intent.getBooleanExtra(Constants.PATH_KEY_IS_DIRECT_MESSAGE, false); sendReplyToPhone(replyMessage.toString(), fullname, toUser, subject, isDirectMessage); } return Service.START_STICKY; } private void sendReplyToPhone(String text, String fullname, String toUser, String subject, boolean isDirectMessage) { PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(Constants.PATH_REPLY); putDataMapRequest.getDataMap().putLong("timestamp", System.currentTimeMillis()); putDataMapRequest.getDataMap().putString(Constants.PATH_KEY_MESSAGE_SUBJECT, subject); putDataMapRequest.getDataMap().putString(Constants.PATH_KEY_MESSAGE, text); putDataMapRequest.getDataMap().putString(Constants.PATH_KEY_POST_FULLNAME, fullname); putDataMapRequest.getDataMap().putString(Constants.PATH_KEY_MESSAGE_TO_USER, toUser); putDataMapRequest.getDataMap() .putBoolean(Constants.PATH_KEY_IS_DIRECT_MESSAGE, isDirectMessage); Wearable.DataApi.putDataItem(mGoogleApiClient, putDataMapRequest.asPutDataRequest()) .setResultCallback(new ResultCallback<DataApi.DataItemResult>() { @Override public void onResult(DataApi.DataItemResult dataItemResult) { logToPhone("sendReplyToPhone, putDataItem status: " + dataItemResult.getStatus() .toString()); } }); } private PendingIntent getVotePendingIntent(Post post, int voteDirection, int requestCode) { Intent vote = new Intent(this, ActionReceiver.class); vote.putExtra(Constants.KEY_PATH, Constants.PATH_VOTE); vote.putExtra(Constants.KEY_CONFIRMATION_MESSAGE, getString(R.string.vote)); vote.putExtra(Constants.KEY_CONFIRMATION_ANIMATION, ConfirmationActivity.SUCCESS_ANIMATION); vote.putExtra(Constants.PATH_KEY_POST_FULLNAME, post.getFullname()); vote.putExtra(Constants.KEY_POST_VOTE_DIRECTION, voteDirection); return PendingIntent.getBroadcast(this, requestCode, vote, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent getOpenOnPhonePendingIntent(String permalink, boolean openOnPhoneDismisses, int notificationId) { Intent openOnPhone = new Intent(this, ActionReceiver.class); openOnPhone.putExtra(Constants.KEY_PATH, Constants.PATH_OPEN_ON_PHONE); openOnPhone.putExtra(Constants.KEY_CONFIRMATION_MESSAGE, getString(R.string.open_on_phone)); openOnPhone.putExtra(Constants.KEY_CONFIRMATION_ANIMATION, ConfirmationActivity.SUCCESS_ANIMATION); openOnPhone.putExtra(Constants.KEY_POST_PERMALINK, permalink); openOnPhone.putExtra(Constants.KEY_DISMISS_AFTER_ACTION, openOnPhoneDismisses); openOnPhone.putExtra(Constants.KEY_NOTIFICATION_ID, notificationId); return PendingIntent.getBroadcast(this, REQUEST_CODE_OPEN_ON_PHONE + notificationId, openOnPhone, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent getSaveToPocketPendingIntent(String permalink, int notificationId) { Intent saveToPocket = new Intent(this, ActionReceiver.class); saveToPocket.putExtra(Constants.KEY_PATH, Constants.PATH_SAVE_TO_POCKET); saveToPocket.putExtra(Constants.KEY_CONFIRMATION_MESSAGE, getString(R.string.save_to_pocket)); saveToPocket.putExtra(Constants.KEY_CONFIRMATION_ANIMATION, ConfirmationActivity.SUCCESS_ANIMATION); saveToPocket.putExtra(Constants.KEY_POST_PERMALINK, permalink); return PendingIntent.getBroadcast(this, REQUEST_CODE_SAVE_TO_POCKET + notificationId, saveToPocket, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent getReplyPendingIntent(Post post, int notificationId) { Intent intent = new Intent(ACTION_RESPONSE); intent.putExtra(Constants.PATH_KEY_IS_DIRECT_MESSAGE, post.isDirectMessage()); intent.putExtra(Constants.PATH_KEY_MESSAGE_TO_USER, post.getAuthor()); intent.putExtra(Constants.PATH_KEY_MESSAGE_SUBJECT, post.getPostContents()); intent.putExtra(Constants.PATH_KEY_POST_FULLNAME, post.getFullname()); return PendingIntent.getService(this, REQUEST_CODE_REPLY + notificationId, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent getViewCommentsPendingIntent(Post post, int notificationId) { Intent intent = new Intent(this, ActionReceiver.class); intent.putExtra(Constants.KEY_PATH, Constants.PATH_COMMENTS); intent.putExtra(Constants.KEY_CONFIRMATION_MESSAGE, getString(R.string.getting_comments)); intent.putExtra(Constants.KEY_CONFIRMATION_ANIMATION, ConfirmationActivity.SUCCESS_ANIMATION); intent.putExtra(Constants.KEY_POST_PERMALINK, post.getPermalink()); return PendingIntent.getBroadcast(this, REQUEST_VIEW_COMMENTS + notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent getViewImagePendingIntent(int notificationId) { Intent intent = new Intent(this, ViewImageActivity.class); intent.putExtra(Constants.KEY_HIGHRES_IMAGE_NAME, getCachedImageName(notificationId)); return PendingIntent.getActivity(this, REQUEST_VIEW_FULLSCREEN_IMAGE + notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); } private PendingIntent getDeletePendingIntent(int notificationId) { Intent intent = new Intent(this, DeleteCachedImageReceiver.class); intent.putExtra(Constants.KEY_HIGHRES_IMAGE_NAME, getCachedImageName(notificationId)); return PendingIntent.getBroadcast(this, REQUEST_VIEW_COMMENTS + notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private String getCachedImageName(int notificationId) { return notificationId + ".png"; } }