aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java540
1 files changed, 540 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java
new file mode 100644
index 000000000..56d6caa19
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java
@@ -0,0 +1,540 @@
+package org.wordpress.android.ui.notifications.utils;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.design.widget.Snackbar;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.AlignmentSpan;
+import android.text.style.ImageSpan;
+import android.text.style.StyleSpan;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.comments.CommentActionResult;
+import org.wordpress.android.ui.comments.CommentActions;
+import org.wordpress.android.ui.notifications.NotificationEvents.NoteModerationFailed;
+import org.wordpress.android.ui.notifications.NotificationEvents.NoteModerationStatusChanged;
+import org.wordpress.android.ui.notifications.NotificationEvents.NoteVisibilityChanged;
+import org.wordpress.android.ui.notifications.NotificationsDetailActivity;
+import org.wordpress.android.ui.notifications.blocks.NoteBlock;
+import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DeviceUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.helpers.WPImageGetter;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class NotificationsUtils {
+ public static final String ARG_PUSH_AUTH_TOKEN = "arg_push_auth_token";
+ public static final String ARG_PUSH_AUTH_TITLE = "arg_push_auth_title";
+ public static final String ARG_PUSH_AUTH_MESSAGE = "arg_push_auth_message";
+ public static final String ARG_PUSH_AUTH_EXPIRES = "arg_push_auth_expires";
+
+ public static final String WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS = "wp_pref_notification_settings";
+ public static final String WPCOM_PUSH_DEVICE_UUID = "wp_pref_notifications_uuid";
+ public static final String WPCOM_PUSH_DEVICE_TOKEN = "wp_pref_notifications_token";
+
+ public static final String WPCOM_PUSH_DEVICE_SERVER_ID = "wp_pref_notifications_server_id";
+ private static final String PUSH_AUTH_ENDPOINT = "me/two-step/push-authentication";
+
+ private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";
+ private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
+
+ private static final String WPCOM_SETTINGS_ENDPOINT = "/me/notifications/settings/";
+
+ private static boolean mSnackbarDidUndo;
+
+ public static void getPushNotificationSettings(Context context, RestRequest.Listener listener,
+ RestRequest.ErrorListener errorListener) {
+ if (!AccountHelper.isSignedInWordPressDotCom()) {
+ return;
+ }
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ String deviceID = settings.getString(WPCOM_PUSH_DEVICE_SERVER_ID, null);
+ String settingsEndpoint = WPCOM_SETTINGS_ENDPOINT;
+ if (!TextUtils.isEmpty(deviceID)) {
+ settingsEndpoint += "?device_id=" + deviceID;
+ }
+ WordPress.getRestClientUtilsV1_1().get(settingsEndpoint, listener, errorListener);
+ }
+
+ public static void registerDeviceForPushNotifications(final Context ctx, String token) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx);
+ String uuid = settings.getString(WPCOM_PUSH_DEVICE_UUID, null);
+ if (uuid == null)
+ return;
+
+ String deviceName = DeviceUtils.getInstance().getDeviceName(ctx);
+ Map<String, String> contentStruct = new HashMap<>();
+ contentStruct.put("device_token", token);
+ contentStruct.put("device_family", "android");
+ contentStruct.put("device_name", deviceName);
+ contentStruct.put("device_model", Build.MANUFACTURER + " " + Build.MODEL);
+ contentStruct.put("app_version", WordPress.versionName);
+ contentStruct.put("os_version", Build.VERSION.RELEASE);
+ contentStruct.put("device_uuid", uuid);
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.d(T.NOTIFS, "Register token action succeeded");
+ try {
+ String deviceID = jsonObject.getString("ID");
+ if (deviceID==null) {
+ AppLog.e(T.NOTIFS, "Server response is missing of the device_id. Registration skipped!!");
+ return;
+ }
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx);
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putString(WPCOM_PUSH_DEVICE_SERVER_ID, deviceID);
+ editor.apply();
+ AppLog.d(T.NOTIFS, "Server response OK. The device_id: " + deviceID);
+ } catch (JSONException e1) {
+ AppLog.e(T.NOTIFS, "Server response is NOT ok, registration skipped.", e1);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.NOTIFS, "Register token action failed", volleyError);
+ }
+ };
+
+ WordPress.getRestClientUtils().post("/devices/new", contentStruct, null, listener, errorListener);
+ }
+
+ public static void unregisterDevicePushNotifications(final Context ctx) {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.d(T.NOTIFS, "Unregister token action succeeded");
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(ctx).edit();
+ editor.remove(WPCOM_PUSH_DEVICE_SERVER_ID);
+ editor.remove(WPCOM_PUSH_DEVICE_UUID);
+ editor.remove(WPCOM_PUSH_DEVICE_TOKEN);
+ editor.apply();
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.NOTIFS, "Unregister token action failed", volleyError);
+ }
+ };
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx);
+ String deviceID = settings.getString(WPCOM_PUSH_DEVICE_SERVER_ID, null );
+ if (TextUtils.isEmpty(deviceID)) {
+ return;
+ }
+ WordPress.getRestClientUtils().post("/devices/" + deviceID + "/delete", listener, errorListener);
+ }
+
+ public static Spannable getSpannableContentForRanges(JSONObject subject) {
+ return getSpannableContentForRanges(subject, null, null, false);
+ }
+
+ /**
+ * Returns a spannable with formatted content based on WP.com note content 'range' data
+ * @param blockObject the JSON data
+ * @param textView the TextView that will display the spannnable
+ * @param onNoteBlockTextClickListener - click listener for ClickableSpans in the spannable
+ * @param isFooter - Set if spannable should apply special formatting
+ * @return Spannable string with formatted content
+ */
+ public static Spannable getSpannableContentForRanges(JSONObject blockObject, TextView textView,
+ final NoteBlock.OnNoteBlockTextClickListener onNoteBlockTextClickListener,
+ boolean isFooter) {
+ if (blockObject == null) {
+ return new SpannableStringBuilder();
+ }
+
+ String text = blockObject.optString("text", "");
+ SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
+
+ boolean shouldLink = onNoteBlockTextClickListener != null;
+
+ // Add ImageSpans for note media
+ addImageSpansForBlockMedia(textView, blockObject, spannableStringBuilder);
+
+ // Process Ranges to add links and text formatting
+ JSONArray rangesArray = blockObject.optJSONArray("ranges");
+ if (rangesArray != null) {
+ for (int i = 0; i < rangesArray.length(); i++) {
+ JSONObject rangeObject = rangesArray.optJSONObject(i);
+ if (rangeObject == null) {
+ continue;
+ }
+
+ NoteBlockClickableSpan clickableSpan = new NoteBlockClickableSpan(WordPress.getContext(), rangeObject,
+ shouldLink, isFooter) {
+ @Override
+ public void onClick(View widget) {
+ if (onNoteBlockTextClickListener != null) {
+ onNoteBlockTextClickListener.onNoteBlockTextClicked(this);
+ }
+ }
+ };
+
+ int[] indices = clickableSpan.getIndices();
+ if (indices.length == 2 && indices[0] <= spannableStringBuilder.length() &&
+ indices[1] <= spannableStringBuilder.length()) {
+ spannableStringBuilder.setSpan(clickableSpan, indices[0], indices[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ // Add additional styling if the range wants it
+ if (clickableSpan.getSpanStyle() != Typeface.NORMAL) {
+ StyleSpan styleSpan = new StyleSpan(clickableSpan.getSpanStyle());
+ spannableStringBuilder.setSpan(styleSpan, indices[0], indices[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+ }
+ }
+
+ return spannableStringBuilder;
+ }
+
+ public static int[] getIndicesForRange(JSONObject rangeObject) {
+ int[] indices = new int[]{0,0};
+ if (rangeObject == null) {
+ return indices;
+ }
+
+ JSONArray indicesArray = rangeObject.optJSONArray("indices");
+ if (indicesArray != null && indicesArray.length() >= 2) {
+ indices[0] = indicesArray.optInt(0);
+ indices[1] = indicesArray.optInt(1);
+ }
+
+ return indices;
+ }
+
+ /**
+ * Adds ImageSpans to the passed SpannableStringBuilder
+ */
+ private static void addImageSpansForBlockMedia(TextView textView, JSONObject subject, SpannableStringBuilder spannableStringBuilder) {
+ if (textView == null || subject == null || spannableStringBuilder == null) return;
+
+ Context context = textView.getContext();
+ JSONArray mediaArray = subject.optJSONArray("media");
+ if (context == null || mediaArray == null) {
+ return;
+ }
+
+ Drawable loading = context.getResources().getDrawable(
+ org.wordpress.android.editor.R.drawable.legacy_dashicon_format_image_big_grey);
+ Drawable failed = context.getResources().getDrawable(R.drawable.noticon_warning_big_grey);
+ // Note: notifications_max_image_size seems to be the max size an ImageSpan can handle,
+ // otherwise it would load blank white
+ WPImageGetter imageGetter = new WPImageGetter(
+ textView,
+ context.getResources().getDimensionPixelSize(R.dimen.notifications_max_image_size),
+ WordPress.imageLoader,
+ loading,
+ failed
+ );
+
+ int indexAdjustment = 0;
+ String imagePlaceholder;
+ for (int i = 0; i < mediaArray.length(); i++) {
+ JSONObject mediaObject = mediaArray.optJSONObject(i);
+ if (mediaObject == null) {
+ continue;
+ }
+
+ final Drawable remoteDrawable = imageGetter.getDrawable(mediaObject.optString("url", ""));
+ ImageSpan noteImageSpan = new ImageSpan(remoteDrawable, mediaObject.optString("url", ""));
+ int startIndex = JSONUtils.queryJSON(mediaObject, "indices[0]", -1);
+ int endIndex = JSONUtils.queryJSON(mediaObject, "indices[1]", -1);
+ if (startIndex >= 0) {
+ startIndex += indexAdjustment;
+ endIndex += indexAdjustment;
+
+ if (startIndex > spannableStringBuilder.length()) {
+ continue;
+ }
+
+ // If we have a range, it means there is alt text that should be removed
+ if (endIndex > startIndex && endIndex <= spannableStringBuilder.length()) {
+ spannableStringBuilder.replace(startIndex, endIndex, "");
+ }
+
+ // We need an empty space to insert the ImageSpan into
+ imagePlaceholder = " ";
+
+ // Move the image to a new line if needed
+ int previousCharIndex = (startIndex > 0) ? startIndex - 1 : 0;
+ if (!spannableHasCharacterAtIndex(spannableStringBuilder, '\n', previousCharIndex)
+ || spannableStringBuilder.getSpans(startIndex, startIndex, ImageSpan.class).length > 0) {
+ imagePlaceholder = "\n ";
+ }
+
+ int spanIndex = startIndex + imagePlaceholder.length() - 1;
+
+ // Add a newline after the image if needed
+ if (!spannableHasCharacterAtIndex(spannableStringBuilder, '\n', startIndex)
+ && !spannableHasCharacterAtIndex(spannableStringBuilder, '\r', startIndex)) {
+ imagePlaceholder += "\n";
+ }
+
+ spannableStringBuilder.insert(startIndex, imagePlaceholder);
+
+ // Add the image span
+ spannableStringBuilder.setSpan(
+ noteImageSpan,
+ spanIndex,
+ spanIndex + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ // Add an AlignmentSpan to center the image
+ spannableStringBuilder.setSpan(
+ new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
+ spanIndex,
+ spanIndex + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ indexAdjustment += imagePlaceholder.length();
+ }
+ }
+ }
+
+ public static void handleNoteBlockSpanClick(NotificationsDetailActivity activity, NoteBlockClickableSpan clickedSpan) {
+ switch (clickedSpan.getRangeType()) {
+ case SITE:
+ // Show blog preview
+ activity.showBlogPreviewActivity(clickedSpan.getId());
+ break;
+ case USER:
+ // Show blog preview
+ activity.showBlogPreviewActivity(clickedSpan.getSiteId());
+ break;
+ case POST:
+ // Show post detail
+ activity.showPostActivity(clickedSpan.getSiteId(), clickedSpan.getId());
+ break;
+ case COMMENT:
+ // Load the comment in the reader list if it exists, otherwise show a webview
+ if (ReaderUtils.postAndCommentExists(clickedSpan.getSiteId(), clickedSpan.getPostId(), clickedSpan.getId())) {
+ activity.showReaderCommentsList(clickedSpan.getSiteId(), clickedSpan.getPostId(), clickedSpan.getId());
+ } else {
+ activity.showWebViewActivityForUrl(clickedSpan.getUrl());
+ }
+ break;
+ case STAT:
+ case FOLLOW:
+ // We can open native stats if the site is a wpcom or Jetpack + stored in the app locally.
+ // Note that for Jetpack sites we need the options already synced. That happens when the user
+ // selects the site in the sites picker. So adding it to the app doesn't always populate options.
+
+ // Do not load Jetpack shadow sites here. They've empty options and Stats can't be loaded for them.
+ Blog blog = WordPress.wpDB.getBlogForDotComBlogId(
+ String.valueOf(clickedSpan.getSiteId())
+ );
+ // Make sure blog is not null, and it's either JP or dotcom. Better safe than sorry.
+ if (blog == null || blog.getLocalTableBlogId() <= 0 || (!blog.isDotcomFlag() && !blog.isJetpackPowered())) {
+ activity.showWebViewActivityForUrl(clickedSpan.getUrl());
+ break;
+ }
+ activity.showStatsActivityForSite(blog.getLocalTableBlogId(), clickedSpan.getRangeType());
+ break;
+ case LIKE:
+ if (ReaderPostTable.postExists(clickedSpan.getSiteId(), clickedSpan.getId())) {
+ activity.showReaderPostLikeUsers(clickedSpan.getSiteId(), clickedSpan.getId());
+ } else {
+ activity.showPostActivity(clickedSpan.getSiteId(), clickedSpan.getId());
+ }
+ break;
+ default:
+ // We don't know what type of id this is, let's see if it has a URL and push a webview
+ if (!TextUtils.isEmpty(clickedSpan.getUrl())) {
+ activity.showWebViewActivityForUrl(clickedSpan.getUrl());
+ }
+ }
+ }
+
+ public static boolean spannableHasCharacterAtIndex(Spannable spannable, char character, int index) {
+ return spannable != null && index < spannable.length() && spannable.charAt(index) == character;
+ }
+
+
+ public static void showPushAuthAlert(Context context, final String token, String title, String message) {
+ if (context == null ||
+ TextUtils.isEmpty(token) ||
+ TextUtils.isEmpty(title) ||
+ TextUtils.isEmpty(message)) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(title).setMessage(message);
+
+ builder.setPositiveButton(R.string.mnu_comment_approve, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // ping the push auth endpoint with the token, wp.com will take care of the rest!
+ Map<String, String> tokenMap = new HashMap<>();
+ tokenMap.put("action", "authorize_login");
+ tokenMap.put("push_token", token);
+ WordPress.getRestClientUtilsV1_1().post(PUSH_AUTH_ENDPOINT, tokenMap, null, null,
+ new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_FAILED);
+ }
+ });
+
+ AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_APPROVED);
+ }
+ });
+
+ builder.setNegativeButton(R.string.ignore, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_IGNORED);
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private static void showUndoBarForNote(final Note note,
+ final CommentStatus status,
+ final View parentView) {
+ Resources resources = parentView.getContext().getResources();
+ String message = (status == CommentStatus.TRASH ? resources.getString(R.string.comment_trashed) : resources.getString(R.string.comment_spammed));
+ View.OnClickListener undoListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mSnackbarDidUndo = true;
+ EventBus.getDefault().postSticky(new NoteVisibilityChanged(note.getId(), false));
+ }
+ };
+
+ mSnackbarDidUndo = false;
+ Snackbar snackbar = Snackbar.make(parentView, message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, undoListener);
+
+ // Deleted notifications in Simperium never come back, so we won't
+ // make the request until the undo bar fades away
+ snackbar.setCallback(new Snackbar.Callback() {
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ super.onDismissed(snackbar, event);
+ if (mSnackbarDidUndo) {
+ return;
+ }
+ CommentActions.moderateCommentForNote(note, status,
+ new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ if (!result.isSuccess()) {
+ EventBus.getDefault().postSticky(new NoteVisibilityChanged(note.getId(), false));
+ EventBus.getDefault().postSticky(new NoteModerationFailed());
+ }
+ }
+ });
+ }
+ });
+
+ snackbar.show();
+ }
+
+ /**
+ * Moderate a comment from a WPCOM notification.
+ * Broadcast EventBus events on update/success/failure and show an undo bar if new status is Trash or Spam
+ */
+ public static void moderateCommentForNote(final Note note, final CommentStatus newStatus, final View parentView) {
+ if (newStatus == CommentStatus.APPROVED || newStatus == CommentStatus.UNAPPROVED) {
+ note.setLocalStatus(CommentStatus.toRESTString(newStatus));
+ note.save();
+ EventBus.getDefault().postSticky(new NoteModerationStatusChanged(note.getId(), true));
+ CommentActions.moderateCommentForNote(note, newStatus,
+ new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ EventBus.getDefault().postSticky(new NoteModerationStatusChanged(note.getId(), false));
+ if (!result.isSuccess()) {
+ note.setLocalStatus(null);
+ note.save();
+ EventBus.getDefault().postSticky(new NoteModerationFailed());
+ }
+ }
+ });
+ } else if (newStatus == CommentStatus.TRASH || newStatus == CommentStatus.SPAM) {
+ // Post as sticky, so that NotificationsListFragment can pick it up after it's created
+ EventBus.getDefault().postSticky(new NoteVisibilityChanged(note.getId(), true));
+ // Show undo bar for trash or spam actions
+ showUndoBarForNote(note, newStatus, parentView);
+ }
+ }
+
+ // Checks if global notifications toggle is enabled in the Android app settings
+ // See: https://code.google.com/p/android/issues/detail?id=38482#c15
+ @SuppressWarnings("unchecked")
+ @TargetApi(19)
+ public static boolean isNotificationsEnabled(Context context) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ ApplicationInfo appInfo = context.getApplicationInfo();
+ String pkg = context.getApplicationContext().getPackageName();
+ int uid = appInfo.uid;
+
+ Class appOpsClass;
+ try {
+ appOpsClass = Class.forName(AppOpsManager.class.getName());
+
+ Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);
+
+ Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
+ int value = (int) opPostNotificationValue.get(Integer.class);
+
+ return ((int) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED);
+ } catch (ClassNotFoundException | NoSuchFieldException | NoSuchMethodException |
+ IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Default to assuming notifications are enabled
+ return true;
+ }
+}