diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index d77e3398cf1f..6f03bd9a9ed7 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,15 @@ +13.8 +----- + 13.7 ----- +* Block editor: Include block title in Unsupported block's UI +* Block editor: Show new-block-indicator when no blocks at all and when at the last block +* Block editor: Use existing links in the clipboard to prefill url field when inserting new link +* Block editor: Media & Text block alignment options +* Block editor: Images clickable for fullscreen preview +* Fixed time displayed on Post Conflict Detected and Unpublished Revision dialogs +* Block editor: Fix issue when removing image/page break block crashes the app 13.6 ----- diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 7d816d27308e..da1cb5d7655a 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -55,9 +55,9 @@ android { if (project.hasProperty("versionName")) { versionName project.property("versionName") } else { - versionName "alpha-196" + versionName "alpha-197" } - versionCode 795 + versionCode 798 minSdkVersion 21 targetSdkVersion 28 @@ -83,9 +83,9 @@ android { dimension "buildType" // Only set the release version if one isn't provided if (!project.hasProperty("versionName")) { - versionName "13.6" + versionName "13.7-rc-1" } - versionCode 796 + versionCode 797 buildConfigField "boolean", "ME_ACTIVITY_AVAILABLE", "false" } @@ -297,6 +297,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0-alpha' + + implementation 'com.github.joshjdevl.libsodiumjni:libsodium-jni-aar:2.0.1' } configurations.all { diff --git a/WordPress/proguard.cfg b/WordPress/proguard.cfg index 79eb756b4a4d..1fc5b4fa8926 100644 --- a/WordPress/proguard.cfg +++ b/WordPress/proguard.cfg @@ -70,3 +70,12 @@ -dontwarn com.github.godness84.RNRecyclerViewList.** ###### React Native - end + +###### Main resource class - begin +-keepattributes InnerClasses + +-keep class org.wordpress.android.R +-keep class org.wordpress.android.R$* { + ; +} +###### Main resource class - end diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/EncryptionUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/EncryptionUtilsTest.java new file mode 100644 index 000000000000..b844ceb4ba0d --- /dev/null +++ b/WordPress/src/androidTest/java/org/wordpress/android/util/EncryptionUtilsTest.java @@ -0,0 +1,209 @@ +package org.wordpress.android.util; + +import android.util.Base64; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.json.JSONException; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.libsodium.jni.NaCl; + +import static junit.framework.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(AndroidJUnit4.class) +public class EncryptionUtilsTest { + byte[] mPublicKey; + byte[] mSecretKey; + + static final int BOX_PUBLIC_KEY_BYTES = NaCl.sodium().crypto_box_publickeybytes(); + static final int BOX_SECRET_KEY_BYTES = NaCl.sodium().crypto_box_secretkeybytes(); + + static final int BASE64_DECODE_FLAGS = Base64.DEFAULT; + + // test data + static final String TEST_EMPTY_STRING = ""; + static final String TEST_LOG_STRING = "WordPress - 13.5 - Version code: 789\n" + + "Android device name: Google Android SDK built for x86\n\n" + + "01 - [Nov-11 03:04 UTILS] WordPress.onCreate\n" + + "02 - [Nov-11 03:04 API] Dispatching action: ListAction-REMOVE_EXPIRED_LISTS\n" + + "03 - [Nov-11 03:04 API] QuickStartStore onRegister\n" + + "04 - [Nov-11 03:04 STATS] 🔵 Tracked: deep_link_not_default_handler, " + + "Properties: {\"interceptor_classname\":\"com.google.android.setupwizard.util.WebDialogActivity\"}\n" + + "05 - [Nov-11 03:04 UTILS] App comes from background\n" + + "06 - [Nov-11 03:04 STATS] 🔵 Tracked: application_opened\n" + + "07 - [Nov-11 03:04 READER] notifications update job service > job scheduled\n" + + "08 - [Nov-11 03:04 API] Dispatching action: SiteAction-FETCH_SITES\n" + + "09 - [Nov-11 03:04 API] StackTrace: com.android.volley.AuthFailureError\n" + + " at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:195)\n" + + " at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131)\n" + + " at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111)\n" + + " at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)\n"; + static final String TEST_CHAR_SAMPLE = "!\"#$%&' ()*+,- ./{|}~[\\]^_`: ;<=>?Ⓟ @︼︽︾⑳₡\n" + + "¢£¤¥¦§¨©ª«¬®¯ °±²ɇɈɉɊɋɌɎɏɐɑɒɓɔ ɕɖɗɘəɚ⤚▓⤜⤝⤞⤟ⰙⰚⰛⰜ⭑⬤⭒‰ ꕢ ꕣꕤ ꕥ¥₩ \n" + + "❌ ⛱⛲⛳⛰⛴⛵ ⚡⏰⏱⏲⭐ ✋☕⛩⛺⛪✨ ⚽ ⛄⏳\n" + + " ḛḜḝḞṶṷṸẂ ẃ ẄẅẆ ᾃᾄᾅ ᾆ Ṥṥ ȊȋȌ ȍ Ȏȏ ȐṦṧåæçèéêë ì í ΔƟΘ\n" + + "㥯㥰㥱㥲㥳㥴㥵 㥶㥷㥸㥹㥺 俋 俌 俍 俎 俏 俐 俑 俒 俓㞢㞣㞤㞥㞦㞧㞨쨜 쨝쨠쨦걵걷 걸걹걺モヤユ ヨラリル\n" + + " ﵑﵓﵔ ﵕﵗ ﵘ ﯿ ﰀﰁﰂ ﰃ ﮁﮂﮃﮄﮅᎹᏪ Ⴥჭᡴᠦᡀ\n"; + + @Before + public void setup() { + mPublicKey = new byte[BOX_PUBLIC_KEY_BYTES]; + mSecretKey = new byte[BOX_SECRET_KEY_BYTES]; + NaCl.sodium().crypto_box_keypair(mPublicKey, mSecretKey); + } + + @Test + public void testEmptyStringEncryptionResultIsValid() { + testEncryption(TEST_EMPTY_STRING); + } + + @Test + public void testLogStringEncryptionResultIsValid() { + testEncryption(TEST_LOG_STRING); + } + + @Test + public void testCharacterSampleEncryptionResultIsValid() { + testEncryption(TEST_CHAR_SAMPLE); + } + + private void testEncryption(final String testString) { + final JSONObject encryptionDataJson = getEncryptionDataJson(mPublicKey, testString); + assertNotNull(encryptionDataJson); + + /* + Expected Contents for JSON: + { + "keyedWith": "v1", + "encryptedKey": "$key_as_base_64", // The encrypted AES key + "header": "base_64_encoded_header", // The xchacha20poly1305 stream header + "messages": [] // the stream elements, base-64 encoded + } + */ + + final byte[] dataSpecificKey = getDataSpecificKey(encryptionDataJson); + assertNotNull(dataSpecificKey); + + final byte[] header = getHeader(encryptionDataJson); + assertNotNull(header); + + final byte[] state = new byte[EncryptionUtils.XCHACHA20POLY1305_STATEBYTES]; + final int initPullReturnCode = NaCl.sodium().crypto_secretstream_xchacha20poly1305_init_pull( + state, + header, + dataSpecificKey); + assertEquals(initPullReturnCode, 0); + + String decryptedDataString = ""; + final byte[][] encryptedLines = getEncryptedLines(encryptionDataJson); + assertNotNull(encryptedLines); + for (int i = 0; i < encryptedLines.length; ++i) { + final String decryptedLine = getDecryptedString(state, encryptedLines[i]); + if (decryptedLine == null) { + // expecting null for the final line in the encryption data + assertEquals(encryptedLines.length - 1, i); + break; + } + + decryptedDataString = decryptedDataString + decryptedLine; + } + + assertEquals(testString, decryptedDataString); + } + private JSONObject getEncryptionDataJson(final byte[] publicKey, final String data) { + try { + final String encryptionDataJsonString = EncryptionUtils.encryptStringData( + Base64.encodeToString(publicKey, Base64.DEFAULT), + data); + + return new JSONObject(encryptionDataJsonString); + } catch (JSONException e) { + fail("encryptStringData failed with JSONException: " + e.toString()); + } + return null; + } + + private byte[] getDataSpecificKey(final JSONObject encryptionDataJson) { + try { + final byte[] decryptedKey = new byte[EncryptionUtils.XCHACHA20POLY1305_KEYBYTES]; + final String encryptedKeyBase64 = encryptionDataJson.getString("encryptedKey"); + final byte[] encryptedKey = Base64.decode(encryptedKeyBase64, BASE64_DECODE_FLAGS); + final int returnCode = NaCl.sodium().crypto_box_seal_open( + decryptedKey, + encryptedKey, + EncryptionUtils.XCHACHA20POLY1305_KEYBYTES + EncryptionUtils.BOX_SEALBYTES, + mPublicKey, + mSecretKey); + assertEquals(returnCode, 0); + + return decryptedKey; + } catch (JSONException e) { + fail("failed to get encryptedKey from encrypted data JSON"); + } + + return null; + } + + private byte[] getHeader(final JSONObject encryptionDataJson) { + try { + final String headerBase64 = encryptionDataJson.getString("header"); + return Base64.decode(headerBase64, BASE64_DECODE_FLAGS); + } catch (JSONException e) { + fail("failed to get header from encrypted data JSON"); + } + return null; + } + + private byte[][] getEncryptedLines(final JSONObject encryptionDataJson) { + try { + final JSONArray messages = encryptionDataJson.getJSONArray("messages"); + + final int messagesLength = messages.length(); + final byte[][] encryptedLines = new byte[messagesLength][]; + for (int i = 0; i < messagesLength; ++i) { + final String messageBase64 = messages.getString(i); + encryptedLines[i] = Base64.decode(messageBase64, BASE64_DECODE_FLAGS); + } + return encryptedLines; + } catch (JSONException e) { + fail("failed to get messages from encrypted data JSON"); + } + + return null; + } + + private String getDecryptedString(final byte[] state, final byte[] encryptedLine) { + final byte[] tag = new byte[1]; + final int decryptedLineLength = encryptedLine.length - EncryptionUtils.XCHACHA20POLY1305_ABYTES; + final byte[] decryptedLine = new byte[decryptedLineLength]; + final byte[] additionalData = new byte[0]; // opting not to use this value + final int additionalDataLength = 0; + final int[] decryptedLineLengthOutput = new int[0]; // opting not to get this value + final int returnCode = NaCl.sodium().crypto_secretstream_xchacha20poly1305_pull( + state, + decryptedLine, + decryptedLineLengthOutput, + tag, + encryptedLine, + encryptedLine.length, + additionalData, + additionalDataLength); + assertEquals(returnCode, 0); + + final int encryptionTag = tag[0]; + if (encryptionTag == EncryptionUtils.XCHACHA20POLY1305_TAG_MESSAGE) { + return new String(decryptedLine); + } else if (encryptionTag == EncryptionUtils.XCHACHA20POLY1305_TAG_FINAL) { + return null; + } + + fail("message decryption failed, unexpected tag."); + return null; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/WordPress.java b/WordPress/src/main/java/org/wordpress/android/WordPress.java index 992c8f9919b9..a258b9069110 100644 --- a/WordPress/src/main/java/org/wordpress/android/WordPress.java +++ b/WordPress/src/main/java/org/wordpress/android/WordPress.java @@ -895,7 +895,7 @@ public void onAppComesFromBackground() { } // Let's migrate the old editor preference if available in AppPrefs to the remote backend - SiteUtils.migrateAppWideMobileEditorPreferenceToRemote(mContext, mDispatcher); + SiteUtils.migrateAppWideMobileEditorPreferenceToRemote(mAccountStore, mSiteStore, mDispatcher); if (mFirstActivityResumed) { deferredInit(); diff --git a/WordPress/src/main/java/org/wordpress/android/push/GCMMessageService.java b/WordPress/src/main/java/org/wordpress/android/push/GCMMessageService.java index db580f3e6d25..78feddf734df 100644 --- a/WordPress/src/main/java/org/wordpress/android/push/GCMMessageService.java +++ b/WordPress/src/main/java/org/wordpress/android/push/GCMMessageService.java @@ -56,6 +56,12 @@ import javax.inject.Inject; +import static org.wordpress.android.push.NotificationPushIds.AUTH_PUSH_NOTIFICATION_ID; +import static org.wordpress.android.push.NotificationPushIds.GROUP_NOTIFICATION_ID; +import static org.wordpress.android.push.NotificationPushIds.PUSH_NOTIFICATION_ID; +import static org.wordpress.android.push.NotificationType.UNKNOWN_NOTE; +import static org.wordpress.android.push.NotificationPushIds.ZENDESK_PUSH_NOTIFICATION_ID; +import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; import static org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter.IS_TAPPED_ON_NOTIFICATION; public class GCMMessageService extends FirebaseMessagingService { @@ -63,12 +69,6 @@ public class GCMMessageService extends FirebaseMessagingService { private static final NotificationHelper NOTIFICATION_HELPER = new NotificationHelper(); private static final String NOTIFICATION_GROUP_KEY = "notification_group_key"; - private static final int PUSH_NOTIFICATION_ID = 10000; - public static final int AUTH_PUSH_NOTIFICATION_ID = 20000; - public static final int GROUP_NOTIFICATION_ID = 30000; - public static final int ACTIONS_RESULT_NOTIFICATION_ID = 40000; - public static final int ACTIONS_PROGRESS_NOTIFICATION_ID = 50000; - public static final int GENERIC_LOCAL_NOTIFICATION_ID = 60000; private static final int AUTH_PUSH_REQUEST_CODE_APPROVE = 0; private static final int AUTH_PUSH_REQUEST_CODE_IGNORE = 1; private static final int AUTH_PUSH_REQUEST_CODE_OPEN_DIALOG = 2; @@ -95,9 +95,6 @@ public class GCMMessageService extends FirebaseMessagingService { private static final String PUSH_TYPE_TEST_NOTE = "push_test"; private static final String PUSH_TYPE_ZENDESK = "zendesk"; - // All Zendesk push notifications will show the same notification, so hopefully this will be a unique ID - private static final int ZENDESK_PUSH_NOTIFICATION_ID = 1999999999; - @Inject AccountStore mAccountStore; @Inject SiteStore mSiteStore; @Inject ZendeskHelper mZendeskHelper; @@ -246,7 +243,7 @@ public static synchronized void removeNotificationWithNoteIdFromSystemBar(Contex } if (ACTIVE_NOTIFICATIONS_MAP.size() == 0) { - notificationManager.cancel(GCMMessageService.GROUP_NOTIFICATION_ID); + notificationManager.cancel(GROUP_NOTIFICATION_ID); } } @@ -266,7 +263,7 @@ public static synchronized void removeAllNotifications(Context context) { it.remove(); } } - notificationManager.cancel(GCMMessageService.GROUP_NOTIFICATION_ID); + notificationManager.cancel(GROUP_NOTIFICATION_ID); } public static synchronized void remove2FANotification(Context context) { @@ -381,7 +378,7 @@ private void buildAndShowNotificationFromTestPushData(Context context, Bundle da ACTIVE_NOTIFICATIONS_MAP.put(pushId, data); Intent resultIntent = new Intent(context, WPMainActivity.class); resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - showSimpleNotification(context, title, message, resultIntent, pushId); + showSimpleNotification(context, title, message, resultIntent, pushId, NotificationType.TEST_NOTE); } private void buildAndShowNotificationFromNoteData(Context context, Bundle data) { @@ -487,9 +484,9 @@ private void buildAndShowNotificationFromNoteData(Context context, Bundle data) } private void showSimpleNotification(Context context, String title, String message, Intent resultIntent, - int pushId) { + int pushId, NotificationType notificationType) { NotificationCompat.Builder builder = getNotificationBuilder(context, title, message); - showNotificationForBuilder(builder, context, resultIntent, pushId, true); + showNotificationForBuilder(builder, context, resultIntent, pushId, true, notificationType); } private void addActionsForCommentNotification(Context context, NotificationCompat.Builder builder, @@ -722,12 +719,14 @@ private void showGroupNotificationForBuilder(Context context, NotificationCompat .setContentText(subject) .setStyle(inboxStyle); - showWPComNotificationForBuilder(groupBuilder, context, wpcomNoteID, GROUP_NOTIFICATION_ID, false); + showWPComNotificationForBuilder(groupBuilder, context, wpcomNoteID, GROUP_NOTIFICATION_ID, false, + NotificationType.GROUP_NOTIFICATION); } else { // Set the individual notification we've already built as the group summary builder.setGroupSummary(true) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - showWPComNotificationForBuilder(builder, context, wpcomNoteID, GROUP_NOTIFICATION_ID, false); + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + showWPComNotificationForBuilder(builder, context, wpcomNoteID, GROUP_NOTIFICATION_ID, false, + NotificationType.GROUP_NOTIFICATION); } } @@ -742,11 +741,41 @@ private void showSingleNotificationForBuilder(Context context, NotificationCompa addActionsForCommentNotification(context, builder, wpcomNoteID); } - showWPComNotificationForBuilder(builder, context, wpcomNoteID, pushId, notifyUser); + showWPComNotificationForBuilder(builder, context, wpcomNoteID, pushId, notifyUser, fromNoteType(noteType)); + } + + private NotificationType fromNoteType(String noteType) { + switch (noteType) { + case PUSH_TYPE_COMMENT: + return NotificationType.COMMENT; + case PUSH_TYPE_LIKE: + return NotificationType.LIKE; + case PUSH_TYPE_COMMENT_LIKE: + return NotificationType.COMMENT_LIKE; + case PUSH_TYPE_AUTOMATTCHER: + return NotificationType.AUTOMATTCHER; + case PUSH_TYPE_FOLLOW: + return NotificationType.FOLLOW; + case PUSH_TYPE_REBLOG: + return NotificationType.REBLOG; + case PUSH_TYPE_PUSH_AUTH: + return NotificationType.AUTHENTICATION; + case PUSH_TYPE_BADGE_RESET: + return NotificationType.BADGE_RESET; + case PUSH_TYPE_NOTE_DELETE: + return NotificationType.NOTE_DELETE; + case PUSH_TYPE_TEST_NOTE: + return NotificationType.TEST_NOTE; + case PUSH_TYPE_ZENDESK: + return NotificationType.ZENDESK; + default: + return UNKNOWN_NOTE; + } } private void showWPComNotificationForBuilder(NotificationCompat.Builder builder, Context context, - String wpcomNoteID, int pushId, boolean notifyUser) { + String wpcomNoteID, int pushId, boolean notifyUser, + NotificationType notificationType) { Intent resultIntent = new Intent(context, WPMainActivity.class); resultIntent.putExtra(WPMainActivity.ARG_OPENED_FROM_PUSH, true); resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK @@ -756,12 +785,13 @@ private void showWPComNotificationForBuilder(NotificationCompat.Builder builder, resultIntent.putExtra(NotificationsListFragment.NOTE_ID_EXTRA, wpcomNoteID); resultIntent.putExtra(IS_TAPPED_ON_NOTIFICATION, true); - showNotificationForBuilder(builder, context, resultIntent, pushId, notifyUser); + showNotificationForBuilder(builder, context, resultIntent, pushId, notifyUser, notificationType); } // Displays a notification to the user private void showNotificationForBuilder(NotificationCompat.Builder builder, Context context, - Intent resultIntent, int pushId, boolean notifyUser) { + Intent resultIntent, int pushId, boolean notifyUser, + NotificationType notificationType) { if (builder == null || context == null || resultIntent == null) { return; } @@ -800,11 +830,16 @@ private void showNotificationForBuilder(NotificationCompat.Builder builder, Cont // Call processing service when notification is dismissed PendingIntent pendingDeleteIntent = - NotificationsProcessingService.getPendingIntentForNotificationDismiss(context, pushId); + NotificationsProcessingService.getPendingIntentForNotificationDismiss( + context, + pushId, + notificationType + ); builder.setDeleteIntent(pendingDeleteIntent); builder.setCategory(NotificationCompat.CATEGORY_SOCIAL); + resultIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType); PendingIntent pendingIntent = PendingIntent.getActivity(context, pushId, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_UPDATE_CURRENT); @@ -985,6 +1020,7 @@ private void handlePushAuth(Context context, Bundle data) { | Intent.FLAG_ACTIVITY_CLEAR_TASK); pushAuthIntent.setAction("android.intent.action.MAIN"); pushAuthIntent.addCategory("android.intent.category.LAUNCHER"); + pushAuthIntent.putExtra(ARG_NOTIFICATION_TYPE, NotificationType.AUTHENTICATION); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, context.getString(R.string.notification_channel_important_id)) @@ -1040,7 +1076,7 @@ private void handlePushAuth(Context context, Bundle data) { // Call processing service when notification is dismissed PendingIntent pendingDeleteIntent = NotificationsProcessingService.getPendingIntentForNotificationDismiss( - context, AUTH_PUSH_NOTIFICATION_ID); + context, AUTH_PUSH_NOTIFICATION_ID, NotificationType.AUTHENTICATION); builder.setDeleteIntent(pendingDeleteIntent); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); @@ -1080,7 +1116,8 @@ private void handleZendeskNotification(Context context) { Intent resultIntent = new Intent(context, WPMainActivity.class); resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); resultIntent.putExtra(WPMainActivity.ARG_SHOW_ZENDESK_NOTIFICATIONS, true); - showSimpleNotification(context, title, message, resultIntent, ZENDESK_PUSH_NOTIFICATION_ID); + showSimpleNotification(context, title, message, resultIntent, ZENDESK_PUSH_NOTIFICATION_ID, + NotificationType.ZENDESK); } } } diff --git a/WordPress/src/main/java/org/wordpress/android/push/NativeNotificationsUtils.java b/WordPress/src/main/java/org/wordpress/android/push/NativeNotificationsUtils.java index 4cd5cc459c3e..2e416bbe6e92 100644 --- a/WordPress/src/main/java/org/wordpress/android/push/NativeNotificationsUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/push/NativeNotificationsUtils.java @@ -8,22 +8,34 @@ import org.wordpress.android.R; -import static org.wordpress.android.push.GCMMessageService.ACTIONS_PROGRESS_NOTIFICATION_ID; +import static org.wordpress.android.push.NotificationPushIds.ACTIONS_PROGRESS_NOTIFICATION_ID; public class NativeNotificationsUtils { - public static void showIntermediateMessageToUser(String message, Context context) { - showMessageToUser(message, true, ACTIONS_PROGRESS_NOTIFICATION_ID, context); + public static void showIntermediateMessageToUser(String message, Context context, + NotificationType notificationType) { + showMessageToUser(message, true, ACTIONS_PROGRESS_NOTIFICATION_ID, context, notificationType); } - public static void showFinalMessageToUser(String message, int pushId, Context context) { - showMessageToUser(message, false, pushId, context); + public static void showFinalMessageToUser(String message, int pushId, Context context, + NotificationType notificationType) { + showMessageToUser(message, false, pushId, context, notificationType); } - private static void showMessageToUser(String message, boolean intermediateMessage, int pushId, Context context) { + private static void showMessageToUser(String message, boolean intermediateMessage, int pushId, + Context context, NotificationType notificationType) { NotificationCompat.Builder builder = getBuilder(context, context.getString(R.string.notification_channel_transient_id)) .setContentText(message).setTicker(message) .setOnlyAlertOnce(true); + if (notificationType != null) { + builder = builder.setDeleteIntent( + NotificationsProcessingService.getPendingIntentForNotificationDismiss( + context, + pushId, + notificationType + ) + ); + } showMessageToUserWithBuilder(builder, message, intermediateMessage, pushId, context); } diff --git a/WordPress/src/main/java/org/wordpress/android/push/NotificationPushIds.kt b/WordPress/src/main/java/org/wordpress/android/push/NotificationPushIds.kt new file mode 100644 index 000000000000..72b81ed4bcd2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/push/NotificationPushIds.kt @@ -0,0 +1,12 @@ +package org.wordpress.android.push + +object NotificationPushIds { + const val PUSH_NOTIFICATION_ID = 10000 + const val AUTH_PUSH_NOTIFICATION_ID = 20000 + const val GROUP_NOTIFICATION_ID = 30000 + const val ACTIONS_RESULT_NOTIFICATION_ID = 40000 + const val ACTIONS_PROGRESS_NOTIFICATION_ID = 50000 + const val PENDING_DRAFTS_NOTIFICATION_ID = 600001 + const val QUICK_START_REMINDER_NOTIFICATION_ID = 4001 + const val ZENDESK_PUSH_NOTIFICATION_ID = 1999999999 +} diff --git a/WordPress/src/main/java/org/wordpress/android/push/NotificationType.kt b/WordPress/src/main/java/org/wordpress/android/push/NotificationType.kt new file mode 100644 index 000000000000..b40b8e496cde --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/push/NotificationType.kt @@ -0,0 +1,26 @@ +package org.wordpress.android.push + +enum class NotificationType { + COMMENT, + LIKE, + COMMENT_LIKE, + AUTOMATTCHER, + FOLLOW, + REBLOG, + BADGE_RESET, + NOTE_DELETE, + TEST_NOTE, + ZENDESK, + UNKNOWN_NOTE, + AUTHENTICATION, + GROUP_NOTIFICATION, + ACTIONS_RESULT, + ACTIONS_PROGRESS, + PENDING_DRAFTS, + QUICK_START_REMINDER, + POST_UPLOAD_SUCCESS, + POST_UPLOAD_ERROR, + MEDIA_UPLOAD_SUCCESS, + MEDIA_UPLOAD_ERROR, + POST_PUBLISHED; +} diff --git a/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java b/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java index 7021a53c6d59..905481ce04a0 100644 --- a/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java +++ b/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java @@ -40,6 +40,7 @@ import org.wordpress.android.models.Note; import org.wordpress.android.ui.main.WPMainActivity; import org.wordpress.android.ui.notifications.NotificationsListFragment; +import org.wordpress.android.ui.notifications.SystemNotificationsTracker; import org.wordpress.android.ui.notifications.receivers.NotificationsPendingDraftsReceiver; import org.wordpress.android.ui.notifications.utils.NotificationsActions; import org.wordpress.android.ui.notifications.utils.NotificationsUtils; @@ -56,7 +57,8 @@ import javax.inject.Inject; -import static org.wordpress.android.ui.RequestCodes.QUICK_START_REMINDER_NOTIFICATION; +import static org.wordpress.android.push.NotificationPushIds.GROUP_NOTIFICATION_ID; +import static org.wordpress.android.push.NotificationPushIds.QUICK_START_REMINDER_NOTIFICATION_ID; /** * service which makes it possible to process Notifications quick actions in the background, @@ -81,6 +83,7 @@ public class NotificationsProcessingService extends Service { public static final String ARG_ACTION_NOTIFICATION_DISMISS = "action_dismiss"; public static final String ARG_NOTE_ID = "note_id"; public static final String ARG_PUSH_ID = "notificationId"; + public static final String ARG_NOTIFICATION_TYPE = "notificationType"; // bundle and push ID, as they are held in the system dashboard public static final String ARG_NOTE_BUNDLE = "note_bundle"; @@ -91,6 +94,7 @@ public class NotificationsProcessingService extends Service { @Inject Dispatcher mDispatcher; @Inject SiteStore mSiteStore; @Inject CommentStore mCommentStore; + @Inject SystemNotificationsTracker mSystemNotificationsTracker; /* * Use this if you want the service to handle a background note Like. @@ -123,11 +127,14 @@ public static void startServiceForReply(Context context, String noteId, String r context.startService(intent); } - public static PendingIntent getPendingIntentForNotificationDismiss(Context context, int pushId) { + public static PendingIntent getPendingIntentForNotificationDismiss(Context context, int pushId, + NotificationType notificationType) { Intent intent = new Intent(context, NotificationsProcessingService.class); intent.putExtra(ARG_ACTION_TYPE, ARG_ACTION_NOTIFICATION_DISMISS); intent.putExtra(ARG_PUSH_ID, pushId); + intent.putExtra(ARG_NOTIFICATION_TYPE, notificationType); intent.addCategory(ARG_ACTION_NOTIFICATION_DISMISS); + return PendingIntent.getService(context, pushId, intent, PendingIntent.FLAG_CANCEL_CURRENT); } @@ -163,7 +170,8 @@ public void onDestroy() { @Override public int onStartCommand(Intent intent, int flags, int startId) { // Offload to a separate thread. - mQuickActionProcessor = new QuickActionProcessor(this, intent, startId); + + mQuickActionProcessor = new QuickActionProcessor(this, mSystemNotificationsTracker, intent, startId); new Thread(new Runnable() { public void run() { mQuickActionProcessor.process(); @@ -174,17 +182,20 @@ public void run() { } private class QuickActionProcessor { + private SystemNotificationsTracker mSystemNotificationsTracker; private String mNoteId; private String mReplyText; private String mActionType; + private NotificationType mNotificationType; private int mPushId; private Note mNote; private final int mTaskId; private final Context mContext; private final Intent mIntent; - QuickActionProcessor(Context ctx, Intent intent, int taskId) { + QuickActionProcessor(Context ctx, SystemNotificationsTracker notificationsTracker, Intent intent, int taskId) { mContext = ctx; + mSystemNotificationsTracker = notificationsTracker; mIntent = intent; mTaskId = taskId; } @@ -198,12 +209,12 @@ public void process() { if (mActionType.equals(ARG_ACTION_AUTH_IGNORE)) { // dismiss notifs NativeNotificationsUtils.dismissNotification( - GCMMessageService.ACTIONS_RESULT_NOTIFICATION_ID, mContext); + NotificationPushIds.ACTIONS_RESULT_NOTIFICATION_ID, mContext); NativeNotificationsUtils.dismissNotification( - GCMMessageService.AUTH_PUSH_NOTIFICATION_ID, mContext); + NotificationPushIds.AUTH_PUSH_NOTIFICATION_ID, mContext); NativeNotificationsUtils.dismissNotification( - GCMMessageService.ACTIONS_PROGRESS_NOTIFICATION_ID, mContext); - GCMMessageService.removeNotification(GCMMessageService.AUTH_PUSH_NOTIFICATION_ID); + NotificationPushIds.ACTIONS_PROGRESS_NOTIFICATION_ID, mContext); + GCMMessageService.removeNotification(NotificationPushIds.AUTH_PUSH_NOTIFICATION_ID); AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_IGNORED); return; @@ -211,17 +222,20 @@ public void process() { // check notification dismissed pending intent if (mActionType.equals(ARG_ACTION_NOTIFICATION_DISMISS)) { + if (mNotificationType != null) { + mSystemNotificationsTracker.trackDismissedNotification(mNotificationType); + } int notificationId = mIntent.getIntExtra(ARG_PUSH_ID, 0); - if (notificationId == GCMMessageService.GROUP_NOTIFICATION_ID) { + if (notificationId == GROUP_NOTIFICATION_ID) { GCMMessageService.clearNotifications(); - } else if (notificationId == QUICK_START_REMINDER_NOTIFICATION) { + } else if (notificationId == QUICK_START_REMINDER_NOTIFICATION_ID) { AnalyticsTracker.track(Stat.QUICK_START_NOTIFICATION_DISMISSED); } else { GCMMessageService.removeNotification(notificationId); // Dismiss the grouped notification if a user dismisses all notifications from a wear device if (!GCMMessageService.hasNotifications()) { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mContext); - notificationManager.cancel(GCMMessageService.GROUP_NOTIFICATION_ID); + notificationManager.cancel(GROUP_NOTIFICATION_ID); } } return; @@ -252,8 +266,9 @@ public void process() { // because we've got inline-reply there with its own spinner to show progress // no op } else { - NativeNotificationsUtils.showIntermediateMessageToUser( - getProcessingTitleForAction(mActionType), mContext); + NativeNotificationsUtils + .showIntermediateMessageToUser(getProcessingTitleForAction(mActionType), mContext, + mNotificationType); } // if we still don't have a Note, go get it from the REST API @@ -304,10 +319,13 @@ private void getDataFromIntent() { mActionType = mIntent.getStringExtra(ARG_ACTION_TYPE); // default value for push notification ID is likely GROUP_NOTIFICATION_ID for the only // notif in active notifs map (there is only one notif if quick actions are available) - mPushId = GCMMessageService.GROUP_NOTIFICATION_ID; + mPushId = GROUP_NOTIFICATION_ID; if (mIntent.hasExtra(ARG_ACTION_REPLY_TEXT)) { mReplyText = mIntent.getStringExtra(ARG_ACTION_REPLY_TEXT); } + if (mIntent.hasExtra(ARG_NOTIFICATION_TYPE)) { + mNotificationType = (NotificationType) mIntent.getSerializableExtra(ARG_NOTIFICATION_TYPE); + } if (TextUtils.isEmpty(mReplyText)) { // if voice reply is enabled in a wearable, it will come through the remoteInput @@ -417,11 +435,12 @@ private void requestCompleted(String actionType) { // dismiss any other pending result notification NativeNotificationsUtils.dismissNotification( - GCMMessageService.ACTIONS_RESULT_NOTIFICATION_ID, mContext); + NotificationPushIds.ACTIONS_RESULT_NOTIFICATION_ID, mContext); // update notification indicating the operation succeeded NativeNotificationsUtils.showFinalMessageToUser(successMessage, - GCMMessageService.ACTIONS_PROGRESS_NOTIFICATION_ID, - mContext); + NotificationPushIds.ACTIONS_PROGRESS_NOTIFICATION_ID, + mContext, + NotificationType.ACTIONS_PROGRESS); // remove the original notification from the system bar GCMMessageService.removeNotificationWithNoteIdFromSystemBar(mContext, mNoteId); @@ -430,7 +449,7 @@ private void requestCompleted(String actionType) { handler.postDelayed(new Runnable() { public void run() { NativeNotificationsUtils.dismissNotification( - GCMMessageService.ACTIONS_PROGRESS_NOTIFICATION_ID, mContext); + NotificationPushIds.ACTIONS_PROGRESS_NOTIFICATION_ID, mContext); } }, 3000); // show the success message for 3 seconds, then dismiss @@ -456,9 +475,10 @@ private void requestFailed(String actionType) { } resetOriginalNotification(); NativeNotificationsUtils.dismissNotification( - GCMMessageService.ACTIONS_PROGRESS_NOTIFICATION_ID, mContext); - NativeNotificationsUtils.showFinalMessageToUser(errorMessage, - GCMMessageService.ACTIONS_RESULT_NOTIFICATION_ID, mContext); + NotificationPushIds.ACTIONS_PROGRESS_NOTIFICATION_ID, mContext); + NativeNotificationsUtils + .showFinalMessageToUser(errorMessage, NotificationPushIds.ACTIONS_RESULT_NOTIFICATION_ID, mContext, + NotificationType.ACTIONS_RESULT); // after 3 seconds, dismiss the error message notification Handler handler = new Handler(getMainLooper()); @@ -466,7 +486,7 @@ private void requestFailed(String actionType) { public void run() { // remove the error notification from the system bar NativeNotificationsUtils.dismissNotification( - GCMMessageService.ACTIONS_RESULT_NOTIFICATION_ID, mContext); + NotificationPushIds.ACTIONS_RESULT_NOTIFICATION_ID, mContext); } }, 3000); // show the success message for 3 seconds, then dismiss @@ -479,8 +499,9 @@ private void requestFailedWithMessage(String errorMessage, boolean autoDismiss) errorMessage = getString(R.string.error_generic); } resetOriginalNotification(); - NativeNotificationsUtils.showFinalMessageToUser(errorMessage, - GCMMessageService.ACTIONS_RESULT_NOTIFICATION_ID, mContext); + NativeNotificationsUtils + .showFinalMessageToUser(errorMessage, NotificationPushIds.ACTIONS_RESULT_NOTIFICATION_ID, mContext, + NotificationType.ACTIONS_RESULT); if (autoDismiss) { // after 3 seconds, dismiss the error message notification @@ -489,7 +510,7 @@ private void requestFailedWithMessage(String errorMessage, boolean autoDismiss) public void run() { // remove the error notification from the system bar NativeNotificationsUtils.dismissNotification( - GCMMessageService.ACTIONS_RESULT_NOTIFICATION_ID, mContext); + NotificationPushIds.ACTIONS_RESULT_NOTIFICATION_ID, mContext); } }, 3000); // show the success message for 3 seconds, then dismiss } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActionableEmptyView.kt b/WordPress/src/main/java/org/wordpress/android/ui/ActionableEmptyView.kt index 1f5a7a79d643..9297946e6324 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActionableEmptyView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActionableEmptyView.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui import android.content.Context +import android.text.TextUtils import android.util.AttributeSet import android.view.Gravity import android.view.View @@ -36,7 +37,11 @@ class ActionableEmptyView : LinearLayout { initView(context, attrs) } - constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) { initView(context, attrs) } @@ -55,9 +60,17 @@ class ActionableEmptyView : LinearLayout { bottomImage = layout.findViewById(R.id.bottom_image) attrs.let { - val typedArray = context.obtainStyledAttributes(it, R.styleable.ActionableEmptyView, 0, 0) - - val imageResource = typedArray.getResourceId(R.styleable.ActionableEmptyView_aevImage, 0) + val typedArray = context.obtainStyledAttributes( + it, + R.styleable.ActionableEmptyView, + 0, + 0 + ) + + val imageResource = typedArray.getResourceId( + R.styleable.ActionableEmptyView_aevImage, + 0 + ) val titleAttribute = typedArray.getString(R.styleable.ActionableEmptyView_aevTitle) val subtitleAttribute = typedArray.getString(R.styleable.ActionableEmptyView_aevSubtitle) val buttonAttribute = typedArray.getString(R.styleable.ActionableEmptyView_aevButton) @@ -99,17 +112,38 @@ class ActionableEmptyView : LinearLayout { val params: RelativeLayout.LayoutParams if (isSearching) { - params = RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) - layout.setPadding(0, context.resources.getDimensionPixelSize(R.dimen.margin_extra_extra_large), 0, 0) + params = RelativeLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + ) + layout.setPadding( + 0, + context.resources.getDimensionPixelSize(R.dimen.margin_extra_extra_large), + 0, + 0 + ) image.visibility = View.GONE button.visibility = View.GONE } else { - params = RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + params = RelativeLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ) layout.setPadding(0, 0, 0, 0) } params.topMargin = topMargin layout.layoutParams = params } + + fun announceEmptyStateForAccessibility() { + val subTitle = if (!TextUtils.isEmpty(subtitle.contentDescription)) { + subtitle.contentDescription + } else { + subtitle.text + } + + announceForAccessibility("${title.text}.$subTitle") + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java index 357f0d70ecb3..0d996b6d397f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java @@ -45,7 +45,6 @@ public class RequestCodes { // QuickStart public static final int QUICK_START_REMINDER_RECEIVER = 4000; - public static final int QUICK_START_REMINDER_NOTIFICATION = 4001; public static final int GIPHY_PICKER = 3200; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index a68abb3d4e0d..b763bd328604 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -60,6 +60,7 @@ import org.wordpress.android.push.GCMMessageService; import org.wordpress.android.push.GCMRegistrationIntentService; import org.wordpress.android.push.NativeNotificationsUtils; +import org.wordpress.android.push.NotificationType; import org.wordpress.android.push.NotificationsProcessingService; import org.wordpress.android.ui.ActivityId; import org.wordpress.android.ui.ActivityLauncher; @@ -74,6 +75,7 @@ import org.wordpress.android.ui.news.NewsManager; import org.wordpress.android.ui.notifications.NotificationEvents; import org.wordpress.android.ui.notifications.NotificationsListFragment; +import org.wordpress.android.ui.notifications.SystemNotificationsTracker; import org.wordpress.android.ui.notifications.adapters.NotesAdapter; import org.wordpress.android.ui.notifications.receivers.NotificationsPendingDraftsReceiver; import org.wordpress.android.ui.notifications.utils.NotificationsActions; @@ -118,6 +120,7 @@ import static androidx.lifecycle.Lifecycle.State.STARTED; import static org.wordpress.android.WordPress.SITE; import static org.wordpress.android.fluxc.store.SiteStore.CompleteQuickStartVariant.NEXT_STEPS; +import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; import static org.wordpress.android.ui.JetpackConnectionSource.NOTIFICATIONS; /** @@ -170,6 +173,7 @@ public class WPMainActivity extends AppCompatActivity implements @Inject NewsManager mNewsManager; @Inject QuickStartStore mQuickStartStore; @Inject UploadActionUseCase mUploadActionUseCase; + @Inject SystemNotificationsTracker mSystemNotificationsTracker; /* * fragments implement this if their contents can be scrolled, called when user @@ -237,6 +241,11 @@ public void run() { } if (FluxCUtils.isSignedInWPComOrHasWPOrgSite(mAccountStore, mSiteStore)) { + NotificationType notificationType = + (NotificationType) getIntent().getSerializableExtra(ARG_NOTIFICATION_TYPE); + if (notificationType != null) { + mSystemNotificationsTracker.trackTappedNotification(notificationType); + } // open note detail if activity called from a push boolean openedFromPush = (getIntent() != null && getIntent().getBooleanExtra(ARG_OPENED_FROM_PUSH, false)); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/SystemNotificationsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/SystemNotificationsTracker.kt index d23f726d5439..e7042d229bac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/SystemNotificationsTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/SystemNotificationsTracker.kt @@ -1,6 +1,29 @@ package org.wordpress.android.ui.notifications import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.push.NotificationType +import org.wordpress.android.push.NotificationType.ACTIONS_PROGRESS +import org.wordpress.android.push.NotificationType.ACTIONS_RESULT +import org.wordpress.android.push.NotificationType.AUTHENTICATION +import org.wordpress.android.push.NotificationType.AUTOMATTCHER +import org.wordpress.android.push.NotificationType.BADGE_RESET +import org.wordpress.android.push.NotificationType.COMMENT +import org.wordpress.android.push.NotificationType.COMMENT_LIKE +import org.wordpress.android.push.NotificationType.FOLLOW +import org.wordpress.android.push.NotificationType.GROUP_NOTIFICATION +import org.wordpress.android.push.NotificationType.LIKE +import org.wordpress.android.push.NotificationType.MEDIA_UPLOAD_ERROR +import org.wordpress.android.push.NotificationType.MEDIA_UPLOAD_SUCCESS +import org.wordpress.android.push.NotificationType.NOTE_DELETE +import org.wordpress.android.push.NotificationType.PENDING_DRAFTS +import org.wordpress.android.push.NotificationType.POST_PUBLISHED +import org.wordpress.android.push.NotificationType.POST_UPLOAD_ERROR +import org.wordpress.android.push.NotificationType.POST_UPLOAD_SUCCESS +import org.wordpress.android.push.NotificationType.QUICK_START_REMINDER +import org.wordpress.android.push.NotificationType.REBLOG +import org.wordpress.android.push.NotificationType.TEST_NOTE +import org.wordpress.android.push.NotificationType.UNKNOWN_NOTE +import org.wordpress.android.push.NotificationType.ZENDESK import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import javax.inject.Inject @@ -34,4 +57,76 @@ class SystemNotificationsTracker mapOf(SYSTEM_NOTIFICATIONS_ENABLED to notificationsEnabled) ) } + + fun trackShownNotification(notificationType: NotificationType) { + val notificationTypeValue = notificationType.toTypeValue() + val properties = mapOf(NOTIFICATION_TYPE_KEY to notificationTypeValue) + analyticsTracker.track(Stat.NOTIFICATION_SHOWN, properties) + } + + fun trackTappedNotification(notificationType: NotificationType) { + val notificationTypeValue = notificationType.toTypeValue() + val properties = mapOf(NOTIFICATION_TYPE_KEY to notificationTypeValue) + analyticsTracker.track(Stat.NOTIFICATION_TAPPED, properties) + } + + fun trackDismissedNotification(notificationType: NotificationType) { + val notificationTypeValue = notificationType.toTypeValue() + val properties = mapOf(NOTIFICATION_TYPE_KEY to notificationTypeValue) + analyticsTracker.track(Stat.NOTIFICATION_DISMISSED, properties) + } + + private fun NotificationType.toTypeValue(): String { + return when (this) { + COMMENT -> COMMENT_VALUE + LIKE -> LIKE_VALUE + COMMENT_LIKE -> COMMENT_LIKE_VALUE + AUTOMATTCHER -> AUTOMATTCHER_VALUE + FOLLOW -> FOLLOW_VALUE + REBLOG -> REBLOG_VALUE + BADGE_RESET -> BADGE_RESET_VALUE + NOTE_DELETE -> NOTE_DELETE_VALUE + TEST_NOTE -> TEST_NOTE_VALUE + UNKNOWN_NOTE -> UNKNOWN_NOTE_VALUE + AUTHENTICATION -> AUTHENTICATION_TYPE_VALUE + GROUP_NOTIFICATION -> GROUP_NOTES_TYPE_VALUE + ACTIONS_RESULT -> ACTIONS_RESULT_TYPE_VALUE + ACTIONS_PROGRESS -> ACTIONS_PROGRESS_TYPE_VALUE + QUICK_START_REMINDER -> QUICK_START_REMINDER_TYPE_VALUE + POST_UPLOAD_SUCCESS -> POST_UPLOAD_SUCCESS_TYPE_VALUE + POST_UPLOAD_ERROR -> POST_UPLOAD_ERROR_TYPE_VALUE + MEDIA_UPLOAD_SUCCESS -> MEDIA_UPLOAD_SUCCESS_TYPE_VALUE + MEDIA_UPLOAD_ERROR -> MEDIA_UPLOAD_ERROR_TYPE_VALUE + POST_PUBLISHED -> POST_PUBLISHED_TYPE_VALUE + PENDING_DRAFTS -> PENDING_DRAFT_TYPE_VALUE + ZENDESK -> ZENDESK_MESSAGE_TYPE_VALUE + } + } + + companion object { + private const val NOTIFICATION_TYPE_KEY = "notification_type" + + private const val COMMENT_VALUE = "comment" + private const val LIKE_VALUE = "like" + private const val COMMENT_LIKE_VALUE = "comment_like" + private const val AUTOMATTCHER_VALUE = "automattcher" + private const val FOLLOW_VALUE = "follow" + private const val REBLOG_VALUE = "reblog" + private const val BADGE_RESET_VALUE = "badge_reset" + private const val NOTE_DELETE_VALUE = "note_delete" + private const val TEST_NOTE_VALUE = "test_note" + private const val UNKNOWN_NOTE_VALUE = "unknown_note" + private const val AUTHENTICATION_TYPE_VALUE = "authentication" + private const val GROUP_NOTES_TYPE_VALUE = "group_notes" + private const val ACTIONS_RESULT_TYPE_VALUE = "actions_result" + private const val ACTIONS_PROGRESS_TYPE_VALUE = "actions_progress" + private const val QUICK_START_REMINDER_TYPE_VALUE = "quick_start_reminder" + private const val POST_UPLOAD_SUCCESS_TYPE_VALUE = "post_upload_success" + private const val POST_UPLOAD_ERROR_TYPE_VALUE = "post_upload_error" + private const val MEDIA_UPLOAD_SUCCESS_TYPE_VALUE = "media_upload_success" + private const val MEDIA_UPLOAD_ERROR_TYPE_VALUE = "media_upload_error" + private const val POST_PUBLISHED_TYPE_VALUE = "post_published" + private const val PENDING_DRAFT_TYPE_VALUE = "pending_draft" + private const val ZENDESK_MESSAGE_TYPE_VALUE = "zendesk_message" + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java index ca2736900453..c04055f0dbfe 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/receivers/NotificationsPendingDraftsReceiver.java @@ -15,8 +15,10 @@ import org.wordpress.android.fluxc.store.PostStore; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.push.NativeNotificationsUtils; +import org.wordpress.android.push.NotificationType; import org.wordpress.android.push.NotificationsProcessingService; import org.wordpress.android.ui.main.WPMainActivity; +import org.wordpress.android.ui.notifications.SystemNotificationsTracker; import org.wordpress.android.ui.notifications.utils.PendingDraftsNotificationsUtils; import org.wordpress.android.ui.prefs.AppPrefs; import org.wordpress.android.util.AppLog; @@ -27,6 +29,8 @@ import javax.inject.Inject; +import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; + public class NotificationsPendingDraftsReceiver extends BroadcastReceiver { public static final String POST_ID_EXTRA = "postId"; public static final String IS_PAGE_EXTRA = "isPage"; @@ -41,6 +45,7 @@ public class NotificationsPendingDraftsReceiver extends BroadcastReceiver { @Inject PostStore mPostStore; @Inject SiteStore mSiteStore; + @Inject SystemNotificationsTracker mSystemNotificationsTracker; @Override public void onReceive(Context context, Intent intent) { @@ -127,13 +132,13 @@ private String getPostTitle(Context context, String postTitle) { private void buildSinglePendingDraftNotification(Context context, String postTitle, String formattedMessage, int postId, boolean isPage) { - buildNotificationWithIntent(context, getResultIntentForOnePost(context, postId, isPage), + buildSinglePendingDraftNotification(context, getResultIntentForOnePost(context, postId, isPage), String.format(formattedMessage, getPostTitle(context, postTitle)), postId, isPage); } private void buildSinglePendingDraftNotificationGeneric(Context context, String postTitle, int postId, boolean isPage) { - buildNotificationWithIntent(context, getResultIntentForOnePost(context, postId, isPage), + buildSinglePendingDraftNotification(context, getResultIntentForOnePost(context, postId, isPage), String.format(context.getString(R.string.pending_draft_one_generic), getPostTitle(context, postTitle)), postId, isPage); @@ -148,6 +153,7 @@ private PendingIntent getResultIntentForOnePost(Context context, int postId, boo resultIntent.addCategory("android.intent.category.LAUNCHER"); resultIntent.putExtra(POST_ID_EXTRA, postId); resultIntent.putExtra(IS_PAGE_EXTRA, isPage); + resultIntent.putExtra(ARG_NOTIFICATION_TYPE, NotificationType.PENDING_DRAFTS); PendingIntent pendingIntent = PendingIntent .getActivity(context, BASE_REQUEST_CODE + PendingDraftsNotificationsUtils .makePendingDraftNotificationId(postId), @@ -156,8 +162,8 @@ private PendingIntent getResultIntentForOnePost(Context context, int postId, boo return pendingIntent; } - private void buildNotificationWithIntent(Context context, PendingIntent intent, String message, int postId, - boolean isPage) { + private void buildSinglePendingDraftNotification(Context context, PendingIntent intent, String message, int postId, + boolean isPage) { NotificationCompat.Builder builder = NativeNotificationsUtils.getBuilder(context, context.getString(R.string.notification_channel_important_id)); builder.setContentText(message) @@ -174,6 +180,7 @@ private void buildNotificationWithIntent(Context context, PendingIntent intent, NativeNotificationsUtils.showMessageToUserWithBuilder(builder, message, false, PendingDraftsNotificationsUtils .makePendingDraftNotificationId(postId), context); + mSystemNotificationsTracker.trackShownNotification(NotificationType.PENDING_DRAFTS); } private void addOpenDraftActionForNotification(Context context, NotificationCompat.Builder builder, int postId, @@ -183,6 +190,7 @@ private void addOpenDraftActionForNotification(Context context, NotificationComp openDraftIntent.putExtra(WPMainActivity.ARG_OPENED_FROM_PUSH, true); openDraftIntent.putExtra(POST_ID_EXTRA, postId); openDraftIntent.putExtra(IS_PAGE_EXTRA, isPage); + openDraftIntent.putExtra(ARG_NOTIFICATION_TYPE, NotificationType.PENDING_DRAFTS); PendingIntent pendingIntent = PendingIntent .getActivity(context, @@ -203,6 +211,7 @@ private void addIgnoreActionForNotification(Context context, NotificationCompat. NotificationsProcessingService.ARG_ACTION_DRAFT_PENDING_IGNORE); ignoreIntent.putExtra(POST_ID_EXTRA, postId); ignoreIntent.putExtra(IS_PAGE_EXTRA, isPage); + ignoreIntent.putExtra(ARG_NOTIFICATION_TYPE, NotificationType.PENDING_DRAFTS); PendingIntent ignorePendingIntent = PendingIntent .getService(context, // need to add + 2 so the request code is different, otherwise they overlap @@ -221,6 +230,8 @@ private void addDismissActionForNotification(Context context, NotificationCompat NotificationsProcessingService.ARG_ACTION_DRAFT_PENDING_DISMISS); notificationDeletedIntent.putExtra(POST_ID_EXTRA, postId); notificationDeletedIntent.putExtra(IS_PAGE_EXTRA, isPage); + notificationDeletedIntent.putExtra(NotificationsProcessingService.ARG_NOTIFICATION_TYPE, + NotificationType.PENDING_DRAFTS); PendingIntent dismissPendingIntent = PendingIntent .getService(context, // need to add + 3 so the request code is different, otherwise they overlap diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/PendingDraftsNotificationsUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/PendingDraftsNotificationsUtils.java index 850db221f7de..862aa4ed96a4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/PendingDraftsNotificationsUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/PendingDraftsNotificationsUtils.java @@ -6,14 +6,13 @@ import android.content.Intent; import org.wordpress.android.fluxc.model.PostModel; -import org.wordpress.android.push.GCMMessageService; +import org.wordpress.android.push.NotificationPushIds; import org.wordpress.android.ui.notifications.receivers.NotificationsPendingDraftsReceiver; import org.wordpress.android.util.DateTimeUtils; public class PendingDraftsNotificationsUtils { // Pending draft notification base request code for alarms private static final int BROADCAST_BASE_REQUEST_CODE = 181; - private static final int PENDING_DRAFTS_NOTIFICATION_ID = GCMMessageService.GENERIC_LOCAL_NOTIFICATION_ID + 1; /* * Schedules alarms for draft posts to remind the user they have pending drafts @@ -100,7 +99,7 @@ public static int makePendingDraftNotificationId(int localPostId) { // constructs a notification ID (int) based on a localPostId (long) which should be low numbers // by casting explicitely // Integer.MAX_VALUE should be enough notifications - return PENDING_DRAFTS_NOTIFICATION_ID + localPostId; + return NotificationPushIds.PENDING_DRAFTS_NOTIFICATION_ID + localPostId; } private static Intent getNotificationsPendingDraftReceiverIntent(Context context, long localPostId) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java index 3b170912d729..ca89c18ad560 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java @@ -15,7 +15,6 @@ import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; -import android.text.Spanned; import android.text.TextUtils; import android.util.ArrayMap; import android.view.ContextThemeWrapper; @@ -59,6 +58,7 @@ import org.wordpress.android.editor.EditorFragmentAbstract.TrackableEvent; import org.wordpress.android.editor.EditorFragmentActivity; import org.wordpress.android.editor.EditorImageMetaData; +import org.wordpress.android.editor.EditorImagePreviewListener; import org.wordpress.android.editor.EditorImageSettingsListener; import org.wordpress.android.editor.EditorMediaUploadListener; import org.wordpress.android.editor.EditorMediaUtils; @@ -104,6 +104,7 @@ import org.wordpress.android.ui.history.HistoryListItem.Revision; import org.wordpress.android.ui.media.MediaBrowserActivity; import org.wordpress.android.ui.media.MediaBrowserType; +import org.wordpress.android.ui.media.MediaPreviewActivity; import org.wordpress.android.ui.media.MediaSettingsActivity; import org.wordpress.android.ui.notifications.utils.PendingDraftsNotificationsUtils; import org.wordpress.android.ui.photopicker.PhotoPickerActivity; @@ -148,7 +149,6 @@ import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.ToastUtils.Duration; -import org.wordpress.android.util.WPHtml; import org.wordpress.android.util.WPMediaUtils; import org.wordpress.android.util.WPPermissionUtils; import org.wordpress.android.util.WPUrlUtils; @@ -186,6 +186,7 @@ public class EditPostActivity extends AppCompatActivity implements EditorFragmentActivity, EditorImageSettingsListener, + EditorImagePreviewListener, EditorDragAndDropListener, EditorFragmentListener, OnRequestPermissionsResultCallback, @@ -1677,6 +1678,11 @@ public void onImageSettingsRequested(EditorImageMetaData editorImageMetaData) { MediaSettingsActivity.showForResult(this, mSite, editorImageMetaData); } + + @Override public void onImagePreviewRequested(String mediaUrl) { + MediaPreviewActivity.showPreview(this, null, mediaUrl); + } + @Override public void onNegativeClicked(@NonNull String instanceTag) { switch (instanceTag) { @@ -2267,25 +2273,6 @@ private void addExistingMediaToEditor(@NonNull AddExistingdMediaSource source, L mEditorFragment.appendMediaFiles(mediaMap); } - private class LoadPostContentTask extends AsyncTask { - @Override - protected Spanned doInBackground(String... params) { - if (params.length < 1 || mPost == null) { - return null; - } - - String content = StringUtils.notNullStr(params[0]); - return WPHtml.fromHtml(content, EditPostActivity.this, mPost, getMaximumThumbnailWidthForEditor()); - } - - @Override - protected void onPostExecute(Spanned spanned) { - if (spanned != null) { - mEditorFragment.setContent(spanned); - } - } - } - private String getUploadErrorHtml(String mediaId, String path) { return String.format(Locale.US, "" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java index cd3968719d82..fcae017229b2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java @@ -43,7 +43,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -479,10 +478,12 @@ public static boolean hasAutoSave(PostModel post) { public static String getConflictedPostCustomStringForDialog(PostModel post) { Context context = WordPress.getContext(); String firstPart = context.getString(R.string.dialog_confirm_load_remote_post_body); + String lastModified = + TextUtils.isEmpty(post.getDateLocallyChanged()) ? post.getLastModified() : post.getDateLocallyChanged(); String secondPart = String.format(context.getString(R.string.dialog_confirm_load_remote_post_body_2), getFormattedDateForLastModified( - context, DateTimeUtils.timestampFromIso8601Millis(post.getLastModified())), + context, DateTimeUtils.timestampFromIso8601Millis(lastModified)), getFormattedDateForLastModified( context, DateTimeUtils.timestampFromIso8601Millis(post.getRemoteLastModified()))); return firstPart + secondPart; @@ -516,10 +517,8 @@ public static String getFormattedDateForLastModified(Context context, long timeS DateFormat.SHORT, LocaleManager.getSafeLocale(context)); - - // The timezone on the website is at GMT - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - timeFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + dateFormat.setTimeZone(Calendar.getInstance().getTimeZone()); + timeFormat.setTimeZone(Calendar.getInstance().getTimeZone()); return dateFormat.format(date) + " @ " + timeFormat.format(date); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishNotificationReceiver.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishNotificationReceiver.kt index 9f759100f975..60fe3814ef3d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishNotificationReceiver.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishNotificationReceiver.kt @@ -7,15 +7,20 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import org.wordpress.android.R import org.wordpress.android.WordPress +import org.wordpress.android.push.NotificationType +import org.wordpress.android.push.NotificationsProcessingService +import org.wordpress.android.ui.notifications.SystemNotificationsTracker import javax.inject.Inject class PublishNotificationReceiver : BroadcastReceiver() { @Inject lateinit var publishNotificationReceiverViewModel: PublishNotificationReceiverViewModel + @Inject lateinit var systemNotificationsTracker: SystemNotificationsTracker override fun onReceive(context: Context, intent: Intent) { (context.applicationContext as WordPress).component().inject(this) val notificationId = intent.getIntExtra(NOTIFICATION_ID, 0) val uiModel = publishNotificationReceiverViewModel.loadNotification(notificationId) if (uiModel != null) { + val notificationType = NotificationType.POST_PUBLISHED val notificationCompat = NotificationCompat.Builder( context, context.getString(R.string.notification_channel_reminder_id) @@ -24,8 +29,16 @@ class PublishNotificationReceiver : BroadcastReceiver() { .setContentText(uiModel.message) .setAutoCancel(true) .setSmallIcon(R.drawable.ic_my_sites_white_24dp) + .setDeleteIntent( + NotificationsProcessingService.getPendingIntentForNotificationDismiss( + context, + notificationId, + notificationType + ) + ) .build() NotificationManagerCompat.from(context).notify(notificationId, notificationCompat) + systemNotificationsTracker.trackShownNotification(notificationType) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index 98a13bf8c178..44f86eefd33c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -115,6 +115,7 @@ public enum DeletablePrefKey implements PrefKey { NEWS_CARD_SHOWN_VERSION, AVATAR_VERSION, GUTENBERG_DEFAULT_FOR_NEW_POSTS, + USER_IN_GUTENBERG_ROLLOUT_GROUP, SHOULD_AUTO_ENABLE_GUTENBERG_FOR_THE_NEW_POSTS, GUTENBERG_OPT_IN_DIALOG_SHOWN, @@ -607,6 +608,14 @@ public static boolean isDefaultAppWideEditorPreferenceSet() { return !"".equals(getString(DeletablePrefKey.GUTENBERG_DEFAULT_FOR_NEW_POSTS)); } + public static boolean isUserInGutenbergRolloutGroup() { + return getBoolean(DeletablePrefKey.USER_IN_GUTENBERG_ROLLOUT_GROUP, false); + } + + public static void setUserInGutenbergRolloutGroup() { + setBoolean(DeletablePrefKey.USER_IN_GUTENBERG_ROLLOUT_GROUP, true); + } + public static void removeAppWideEditorPreference() { remove(DeletablePrefKey.GUTENBERG_DEFAULT_FOR_NEW_POSTS); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeDetailFragment.java index c16aeb5b7cfc..be15e86e246c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeDetailFragment.java @@ -122,6 +122,10 @@ public void loadData() { TextView txtDescription = (TextView) mServiceCardView.findViewById(R.id.text_description); txtDescription.setText(description); + // Hide the Learn More button by default as at the moment it is only used for the Facebook warning below. + TextView learnMoreButton = (TextView) mServiceCardView.findViewById(R.id.learn_more_button); + learnMoreButton.setVisibility(View.GONE); + if (isFacebook()) { showFacebookWarning(); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartReminderReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartReminderReceiver.java index eecb92528d4d..e16fe4ed10e1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartReminderReceiver.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartReminderReceiver.java @@ -16,19 +16,23 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.fluxc.store.QuickStartStore; import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask; +import org.wordpress.android.push.NotificationPushIds; +import org.wordpress.android.push.NotificationType; import org.wordpress.android.push.NotificationsProcessingService; import org.wordpress.android.ui.main.MySiteFragment; import org.wordpress.android.ui.main.WPMainActivity; +import org.wordpress.android.ui.notifications.SystemNotificationsTracker; import org.wordpress.android.ui.prefs.AppPrefs; import javax.inject.Inject; -import static org.wordpress.android.ui.RequestCodes.QUICK_START_REMINDER_NOTIFICATION; +import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; public class QuickStartReminderReceiver extends BroadcastReceiver { public static final String ARG_QUICK_START_TASK_BATCH = "ARG_QUICK_START_TASK_BATCH"; @Inject QuickStartStore mQuickStartStore; + @Inject SystemNotificationsTracker mSystemNotificationsTracker; @Override public void onReceive(Context context, Intent intent) { @@ -55,12 +59,15 @@ public void onReceive(Context context, Intent intent) { Intent resultIntent = new Intent(context, WPMainActivity.class); resultIntent.putExtra(MySiteFragment.ARG_QUICK_START_TASK, true); + NotificationType notificationType = NotificationType.QUICK_START_REMINDER; + resultIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType); resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); resultIntent.setAction(Intent.ACTION_MAIN); resultIntent.addCategory(Intent.CATEGORY_LAUNCHER); - PendingIntent notificationContentIntent = PendingIntent.getActivity(context, QUICK_START_REMINDER_NOTIFICATION, - resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent notificationContentIntent = + PendingIntent.getActivity(context, NotificationPushIds.QUICK_START_REMINDER_NOTIFICATION_ID, + resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); Notification notification = new NotificationCompat.Builder(context, @@ -72,10 +79,13 @@ public void onReceive(Context context, Intent intent) { .setAutoCancel(true) .setContentIntent(notificationContentIntent) .setDeleteIntent(NotificationsProcessingService - .getPendingIntentForNotificationDismiss(context, QUICK_START_REMINDER_NOTIFICATION)) + .getPendingIntentForNotificationDismiss(context, + NotificationPushIds.QUICK_START_REMINDER_NOTIFICATION_ID, + notificationType)) .build(); - notificationManager.notify(QUICK_START_REMINDER_NOTIFICATION, notification); + notificationManager.notify(NotificationPushIds.QUICK_START_REMINDER_NOTIFICATION_ID, notification); AnalyticsTracker.track(Stat.QUICK_START_NOTIFICATION_SENT); + mSystemNotificationsTracker.trackShownNotification(notificationType); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index 0fcf8dd067d1..4808f75f7627 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -1312,6 +1312,8 @@ private void setEmptyTitleAndDescriptionForBookmarksList() { mActionableEmptyView.image.setVisibility(View.VISIBLE); mActionableEmptyView.title.setText(R.string.reader_empty_saved_posts_title); mActionableEmptyView.subtitle.setText(ssb); + mActionableEmptyView.subtitle + .setContentDescription(getString(R.string.reader_empty_saved_posts_content_description)); mActionableEmptyView.subtitle.setVisibility(View.VISIBLE); mActionableEmptyView.button.setText(R.string.reader_empty_followed_blogs_button_followed); mActionableEmptyView.button.setVisibility(View.VISIBLE); @@ -1375,6 +1377,7 @@ private void setEmptyTitleDescriptionAndButton(@NonNull String title, String des private void showEmptyView() { if (isAdded()) { mActionableEmptyView.setVisibility(View.VISIBLE); + mActionableEmptyView.announceEmptyStateForAccessibility(); } } @@ -1429,6 +1432,7 @@ public void onDataLoaded(boolean isEmpty) { } } else { hideEmptyView(); + announceListStateForAccessibility(); if (mRestorePosition > 0) { AppLog.d(T.READER, "reader post list > restoring position"); mRecyclerView.scrollRecycleViewToPosition(mRestorePosition); @@ -1471,6 +1475,13 @@ && getFragmentManager().findFragmentByTag(tag) == null) { } }; + private void announceListStateForAccessibility() { + if (getView() != null) { + getView().announceForAccessibility(getString(R.string.reader_acessibility_list_loaded, + getPostAdapter().getItemCount())); + } + } + private void showBookmarksSavedLocallyDialog() { mBookmarksSavedLocallyDialog = new AlertDialog.Builder(getActivity()) .setTitle(getString(R.string.reader_save_posts_locally_dialog_title)) @@ -2306,7 +2317,6 @@ public void onScrollToTop() { public static void resetLastUpdateDate() { mLastAutoUpdateDt = null; } - private class LoadTagsTask extends AsyncTask { private final FilteredRecyclerView.FilterCriteriaAsyncLoaderListener mFilterCriteriaLoaderListener; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index ca8d2b6731c9..226141c02ec0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -66,6 +66,7 @@ import org.wordpress.android.util.GravatarUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.ViewUtilsKt; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; @@ -237,6 +238,10 @@ private class ReaderPostViewHolder extends RecyclerView.ViewHolder { ViewGroup postHeaderView = itemView.findViewById(R.id.layout_post_header); mFollowButton = postHeaderView.findViewById(R.id.follow_button); + ViewUtilsKt.expandTouchTargetArea(mLayoutDiscover, R.dimen.reader_discover_layout_extra_padding, true); + ViewUtilsKt.expandTouchTargetArea(mVisit, R.dimen.reader_visit_layout_extra_padding, false); + ViewUtilsKt.expandTouchTargetArea(mImgMore, R.dimen.reader_more_image_extra_padding, false); + // show post in internal browser when "visit" is clicked View.OnClickListener visitListener = new View.OnClickListener() { @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java index df0bd59a31c0..48dde3fb350e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java @@ -23,6 +23,7 @@ import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.ViewUtilsKt; import java.lang.ref.WeakReference; @@ -136,6 +137,7 @@ class TagViewHolder extends RecyclerView.ViewHolder { mTxtTagName = (TextView) view.findViewById(R.id.text_topic); mBtnRemove = (ImageButton) view.findViewById(R.id.btn_remove); ReaderUtils.setBackgroundToRoundRipple(mBtnRemove); + ViewUtilsKt.expandTouchTargetArea(mBtnRemove, R.dimen.reader_remove_button_extra_padding, false); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java index a06b3eebbd2d..dd9e7210f157 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java @@ -22,6 +22,7 @@ import org.wordpress.android.util.PhotonUtils.Quality; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.UrlUtils; +import org.wordpress.android.util.ViewUtilsKt; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; @@ -66,6 +67,7 @@ public ReaderSiteHeaderView(Context context, AttributeSet attrs, int defStyleAtt private void initView(Context context) { View view = inflate(context, R.layout.reader_site_header_view, this); mFollowButton = view.findViewById(R.id.follow_button); + ViewUtilsKt.expandTouchTargetArea(mFollowButton, R.dimen.reader_follow_button_extra_padding, false); } public void setOnFollowListener(OnFollowListener listener) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsUtils.kt index 74d4221f2590..bab538e5d230 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsUtils.kt @@ -7,10 +7,10 @@ import java.util.TreeMap private val SUFFIXES = TreeMap(mapOf( 1_000L to "k", 1_000_000L to "M", - 1_000_000_000L to "G", + 1_000_000_000L to "B", 1_000_000_000_000L to "T", - 1_000_000_000_000_000L to "P", - 1_000_000_000_000_000_000L to "E" + 1_000_000_000_000_000L to "Qa", + 1_000_000_000_000_000_000L to "Qi" )) const val ONE_THOUSAND = 1000 diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadNotifier.java b/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadNotifier.java index 6a86cf99bf33..deaf037ec48a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadNotifier.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadNotifier.java @@ -19,9 +19,12 @@ import org.wordpress.android.fluxc.model.PostModel; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.model.post.PostStatus; +import org.wordpress.android.push.NotificationType; +import org.wordpress.android.push.NotificationsProcessingService; import org.wordpress.android.ui.RequestCodes; import org.wordpress.android.ui.media.MediaBrowserActivity; import org.wordpress.android.ui.notifications.ShareAndDismissNotificationReceiver; +import org.wordpress.android.ui.notifications.SystemNotificationsTracker; import org.wordpress.android.ui.pages.PagesActivity; import org.wordpress.android.ui.posts.EditPostActivity; import org.wordpress.android.ui.posts.PostUtils; @@ -37,6 +40,7 @@ import java.util.List; import java.util.Random; +import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; import static org.wordpress.android.ui.pages.PagesActivityKt.EXTRA_PAGE_REMOTE_ID_KEY; class PostUploadNotifier { @@ -44,6 +48,7 @@ class PostUploadNotifier { private final UploadService mService; private final NotificationManager mNotificationManager; + private final SystemNotificationsTracker mSystemNotificationsTracker; private final NotificationCompat.Builder mNotificationBuilder; private static final int BASE_MEDIA_ERROR_NOTIFICATION_ID = 72000; @@ -71,10 +76,11 @@ private class NotificationData { final List mUploadedPostsCounted = new ArrayList<>(); } - PostUploadNotifier(Context context, UploadService service) { + PostUploadNotifier(Context context, UploadService service, SystemNotificationsTracker systemNotificationsTracker) { // Add the uploader to the notification bar mContext = context; mService = service; + mSystemNotificationsTracker = systemNotificationsTracker; sNotificationData = new NotificationData(); mNotificationManager = (NotificationManager) SystemServiceFactory.get(mContext, Context.NOTIFICATION_SERVICE); @@ -126,7 +132,7 @@ private synchronized void startOrUpdateForegroundNotification(@Nullable PostMode mService.startForeground(sNotificationData.mNotificationId, mNotificationBuilder.build()); } else { // service was already started, let's just modify the notification - doNotify(sNotificationData.mNotificationId, mNotificationBuilder.build()); + doNotify(sNotificationData.mNotificationId, mNotificationBuilder.build(), null); } } @@ -331,9 +337,15 @@ void updateNotificationSuccessForPost(@NonNull PostModel post, @NonNull SiteMode notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(notificationMessage)); notificationBuilder.setOnlyAlertOnce(true); notificationBuilder.setAutoCancel(true); - long notificationId = getNotificationIdForPost(post); + + NotificationType notificationType = NotificationType.POST_UPLOAD_SUCCESS; + notificationBuilder.setDeleteIntent(NotificationsProcessingService + .getPendingIntentForNotificationDismiss(mContext, (int) notificationId, + notificationType)); + Intent notificationIntent = getNotificationIntent(post, site, notificationId); + notificationIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType); PendingIntent pendingIntentPost = PendingIntent.getActivity(mContext, (int) notificationId, @@ -361,7 +373,7 @@ void updateNotificationSuccessForPost(@NonNull PostModel post, @NonNull SiteMode pendingIntent); } - doNotify(notificationId, notificationBuilder.build()); + doNotify(notificationId, notificationBuilder.build(), notificationType); } void updateNotificationSuccessForMedia(@NonNull List mediaList, @NonNull SiteModel site) { @@ -388,6 +400,8 @@ void updateNotificationSuccessForMedia(@NonNull List mediaList, @Non notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notificationIntent.putExtra(WordPress.SITE, site); notificationIntent.setAction(String.valueOf(notificationId)); + NotificationType notificationType = NotificationType.MEDIA_UPLOAD_SUCCESS; + notificationIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, (int) notificationId, @@ -406,6 +420,9 @@ void updateNotificationSuccessForMedia(@NonNull List mediaList, @Non notificationBuilder.setContentIntent(pendingIntent); notificationBuilder.setOnlyAlertOnce(true); notificationBuilder.setAutoCancel(true); + notificationBuilder.setDeleteIntent(NotificationsProcessingService + .getPendingIntentForNotificationDismiss(mContext, (int) notificationId, + notificationType)); // Add WRITE POST action - only if there is media we can insert in the Post if (mediaList != null && !mediaList.isEmpty()) { @@ -426,7 +443,7 @@ void updateNotificationSuccessForMedia(@NonNull List mediaList, @Non actionPendingIntent); } - doNotify(notificationId, notificationBuilder.build()); + doNotify(notificationId, notificationBuilder.build(), notificationType); } public static long getNotificationIdForPost(PostModel post) { @@ -476,6 +493,8 @@ void updateNotificationErrorForPost(@NonNull PostModel post, @NonNull SiteModel long notificationId = getNotificationIdForPost(post); Intent notificationIntent = getNotificationIntent(post, site, notificationId); notificationIntent.setAction(String.valueOf(notificationId)); + NotificationType notificationType = NotificationType.POST_UPLOAD_ERROR; + notificationIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, (int) notificationId, @@ -495,6 +514,9 @@ void updateNotificationErrorForPost(@NonNull PostModel post, @NonNull SiteModel notificationBuilder.setContentIntent(pendingIntent); notificationBuilder.setAutoCancel(true); notificationBuilder.setOnlyAlertOnce(true); + notificationBuilder.setDeleteIntent(NotificationsProcessingService + .getPendingIntentForNotificationDismiss(mContext, (int) notificationId, + notificationType)); // Add RETRY action - only available on Aztec if (AppPrefs.isAztecEditorEnabled()) { @@ -509,7 +531,7 @@ void updateNotificationErrorForPost(@NonNull PostModel post, @NonNull SiteModel EventBus.getDefault().postSticky(new UploadService.UploadErrorEvent(post, snackbarMessage)); - doNotify(notificationId, notificationBuilder.build()); + doNotify(notificationId, notificationBuilder.build(), notificationType); } @NonNull @@ -545,6 +567,8 @@ void updateNotificationErrorForMedia(@NonNull List mediaList, @NonNu notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notificationIntent.putExtra(WordPress.SITE, site); notificationIntent.setAction(String.valueOf(notificationId)); + NotificationType notificationType = NotificationType.MEDIA_UPLOAD_ERROR; + notificationIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, (int) notificationId, @@ -564,6 +588,9 @@ void updateNotificationErrorForMedia(@NonNull List mediaList, @NonNu notificationBuilder.setContentIntent(pendingIntent); notificationBuilder.setAutoCancel(true); notificationBuilder.setOnlyAlertOnce(true); + notificationBuilder.setDeleteIntent(NotificationsProcessingService + .getPendingIntentForNotificationDismiss(mContext, (int) notificationId, + notificationType)); // Add RETRY action - only if there is media to retry if (mediaList != null && !mediaList.isEmpty()) { @@ -578,7 +605,7 @@ void updateNotificationErrorForMedia(@NonNull List mediaList, @NonNu } EventBus.getDefault().postSticky(new UploadService.UploadErrorEvent(mediaList, snackbarMessage)); - doNotify(notificationId, notificationBuilder.build()); + doNotify(notificationId, notificationBuilder.build(), notificationType); } private String buildErrorMessageMixed(int overrideMediaNotUploadedCount) { @@ -757,7 +784,7 @@ private void updateNotificationProgress() { } mNotificationBuilder.setProgress(100, (int) Math.ceil(getCurrentOverallProgress() * 100), false); - doNotify(sNotificationData.mNotificationId, mNotificationBuilder.build()); + doNotify(sNotificationData.mNotificationId, mNotificationBuilder.build(), null); } private void setProgressForMediaItem(int mediaId, float progress) { @@ -787,9 +814,12 @@ private float getCurrentMediaProgress() { return currentMediaProgress; } - private synchronized void doNotify(long id, Notification notification) { + private synchronized void doNotify(long id, Notification notification, NotificationType notificationType) { try { mNotificationManager.notify((int) id, notification); + if (notificationType != null) { + mSystemNotificationsTracker.trackShownNotification(notificationType); + } } catch (RuntimeException runtimeException) { CrashLoggingUtils.logException(runtimeException, AppLog.T.UTILS, "See issue #2858 / #3966"); AppLog.d(AppLog.T.POSTS, "See issue #2858 / #3966; notify failed with:" + runtimeException); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java index a43e25946a94..acbabe76d101 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java @@ -34,6 +34,7 @@ import org.wordpress.android.fluxc.store.UploadStore; import org.wordpress.android.fluxc.store.UploadStore.ClearMediaPayload; import org.wordpress.android.ui.media.services.MediaUploadReadyListener; +import org.wordpress.android.ui.notifications.SystemNotificationsTracker; import org.wordpress.android.ui.posts.PostUtils; import org.wordpress.android.ui.prefs.AppPrefs; import org.wordpress.android.util.AppLog; @@ -80,6 +81,7 @@ public class UploadService extends Service { @Inject PostStore mPostStore; @Inject SiteStore mSiteStore; @Inject UploadStore mUploadStore; + @Inject SystemNotificationsTracker mSystemNotificationsTracker; @Override public void onCreate() { @@ -95,7 +97,7 @@ public void onCreate() { } if (mPostUploadNotifier == null) { - mPostUploadNotifier = new PostUploadNotifier(getApplicationContext(), this); + mPostUploadNotifier = new PostUploadNotifier(getApplicationContext(), this, mSystemNotificationsTracker); } if (mPostUploadHandler == null) { @@ -973,7 +975,7 @@ private boolean doFinalProcessingOfPosts(Boolean isError, PostModel post) { for (PostModel postModel : mUploadStore.getAllRegisteredPosts()) { if (PostUtils.isPostCurrentlyBeingEdited(postModel)) { // don't touch a Post that is being currently open in the Editor. - break; + continue; } if (!UploadService.hasPendingOrInProgressMediaUploadsForPost(postModel)) { @@ -1000,7 +1002,8 @@ private boolean doFinalProcessingOfPosts(Boolean isError, PostModel post) { new PostEvents.PostUploadCanceled(postModel)); } else { // Do not re-enqueue a post that has already failed - if (isError != null && isError && mUploadStore.isFailedPost(post)) { + if (isError != null && isError && post.getId() == updatedPost.getId() && mUploadStore + .isFailedPost(post)) { continue; } // TODO Should do some extra validation here diff --git a/WordPress/src/main/java/org/wordpress/android/util/EncryptionUtils.java b/WordPress/src/main/java/org/wordpress/android/util/EncryptionUtils.java new file mode 100644 index 000000000000..50f8bf80d9ba --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/EncryptionUtils.java @@ -0,0 +1,122 @@ +package org.wordpress.android.util; + +import android.util.Base64; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.libsodium.jni.NaCl; + +public class EncryptionUtils { + public static final int BOX_SEALBYTES = NaCl.sodium().crypto_box_sealbytes(); + public static final int XCHACHA20POLY1305_ABYTES = NaCl.sodium().crypto_secretstream_xchacha20poly1305_abytes(); + public static final int XCHACHA20POLY1305_KEYBYTES = NaCl.sodium().crypto_secretstream_xchacha20poly1305_keybytes(); + public static final int XCHACHA20POLY1305_STATEBYTES = + NaCl.sodium().crypto_secretstream_xchacha20poly1305_statebytes(); + + public static final short XCHACHA20POLY1305_TAG_FINAL = + (short) NaCl.sodium().crypto_secretstream_xchacha20poly1305_tag_final(); + public static final short XCHACHA20POLY1305_TAG_MESSAGE = + (short) NaCl.sodium().crypto_secretstream_xchacha20poly1305_tag_message(); + + static final int XCHACHA20POLY1305_HEADERBYTES = NaCl.sodium().crypto_secretstream_xchacha20poly1305_headerbytes(); + + static final int BASE64_ENCODE_FLAGS = Base64.DEFAULT; + + static final String KEYED_WITH = "v1"; + + /* + Returns a JSON String containing following data: + { + "keyedWith": "v1", + "encryptedKey": "$key_as_base_64", // The encrypted AES key + "header": "base_64_encoded_header", // The xchacha20poly1305 stream header + "messages": [] // the stream elements, base-64 encoded + } + */ + public static String encryptStringData(final String publicKeyBase64, + final String stringData) throws JSONException { + JSONObject encryptionDataJson = new JSONObject(); + encryptionDataJson.put("keyedWith", KEYED_WITH); + + // Create data-specific key + final byte[] key = new byte[XCHACHA20POLY1305_KEYBYTES]; + NaCl.sodium().crypto_secretstream_xchacha20poly1305_keygen(key); + + final String encryptedKeyBase64 = getBoxSealEncryptedBase64String( + publicKeyBase64, + key, + XCHACHA20POLY1305_KEYBYTES); + encryptionDataJson.put("encryptedKey", encryptedKeyBase64); + + final byte[] state = new byte[XCHACHA20POLY1305_STATEBYTES]; + + final String headerBase64 = initSecretStreamXchacha20poly1305(state, key); + encryptionDataJson.put("header", headerBase64); + + JSONArray encryptedElementsJson = new JSONArray(); + if (!stringData.isEmpty()) { + final String[] splitStringData = stringData.split("\n"); // break up the data by line + + for (int i = 0; i < splitStringData.length; ++i) { + String element = splitStringData[i]; + element = element + "\n"; + final String encryptedElementBase64 = getSecretStreamXchacha20poly1305EncryptedBase64String( + state, + element, + XCHACHA20POLY1305_TAG_MESSAGE); + encryptedElementsJson.put(encryptedElementBase64); + } + } + + final String encryptedDataBase64 = getSecretStreamXchacha20poly1305EncryptedBase64String( + state, + "", + XCHACHA20POLY1305_TAG_FINAL); + encryptedElementsJson.put(encryptedDataBase64); + + encryptionDataJson.put("messages", encryptedElementsJson); + + return encryptionDataJson.toString(); + } + + private static String getBoxSealEncryptedBase64String(final String publicKeyBase64, + final byte[] data, + final int dataSize) { + final byte[] encryptedData = new byte[dataSize + BOX_SEALBYTES]; + final byte[] publicKeyBytes = Base64.decode(publicKeyBase64, Base64.DEFAULT); + NaCl.sodium().crypto_box_seal(encryptedData, data, dataSize, publicKeyBytes); + return Base64.encodeToString(encryptedData, BASE64_ENCODE_FLAGS); + } + + private static String initSecretStreamXchacha20poly1305(byte[] state, final byte[] key) { + final byte[] header = new byte[XCHACHA20POLY1305_HEADERBYTES]; + NaCl.sodium().crypto_secretstream_xchacha20poly1305_init_push(state, header, key); + + return Base64.encodeToString(header, BASE64_ENCODE_FLAGS); + } + + private static String getSecretStreamXchacha20poly1305EncryptedBase64String(final byte[] state, + final String data, + final short tag) { + final int[] encryptedDataLengthOutput = new int[0]; // opting not to get this value + final byte[] additionalData = new byte[0]; // opting not to use this value + final int additionalDataLength = 0; + + final byte[] dataBytes = data.getBytes(); + final byte[] encryptedData = new byte[dataBytes.length + XCHACHA20POLY1305_ABYTES]; + + NaCl.sodium().crypto_secretstream_xchacha20poly1305_push( + state, + encryptedData, + encryptedDataLengthOutput, + dataBytes, + dataBytes.length, + additionalData, + additionalDataLength, + tag); + + return Base64.encodeToString(encryptedData, BASE64_ENCODE_FLAGS); + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java b/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java index a004ea48c4f1..664dfbd83b7b 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java @@ -1,15 +1,16 @@ package org.wordpress.android.util; -import android.content.Context; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.fluxc.Dispatcher; import org.wordpress.android.fluxc.generated.SiteActionBuilder; import org.wordpress.android.fluxc.model.SiteModel; +import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.fluxc.store.SiteStore.DesignateMobileEditorForAllSitesPayload; import org.wordpress.android.fluxc.store.SiteStore.DesignateMobileEditorPayload; @@ -25,6 +26,7 @@ public class SiteUtils { public static final String GB_EDITOR_NAME = "gutenberg"; public static final String AZTEC_EDITOR_NAME = "aztec"; + private static final int GB_ROLLOUT_PERCENTAGE = 10; /** * Migrate the old app-wide editor preference value to per-site setting. wpcom sites will make a network call @@ -35,8 +37,58 @@ public class SiteUtils { * -- 12.9 OPTED OUT (were auto-opted in but turned it OFF) -> turn all sites OFF in 13.0 * */ - public static void migrateAppWideMobileEditorPreferenceToRemote(final Context context, + public static void migrateAppWideMobileEditorPreferenceToRemote(final AccountStore accountStore, + final SiteStore siteStore, final Dispatcher dispatcher) { + // Skip if the user is not signed in + if (!FluxCUtils.isSignedInWPComOrHasWPOrgSite(accountStore, siteStore)) { + return; + } + + // In a later version we might override mobile_editor setting if it's set to `aztec` and show a specific notice + // for these users ("We made a lot of progress on the block editor and we think it's now better than + // the classic editor, we switched it on, but you can change the configuration in your Site Settings"). + // ^ This code should be here. + + // If the user is already in the rollout group, we can skip this the migration. + if (AppPrefs.isUserInGutenbergRolloutGroup()) { + return; + } + + // Check if the user has been "randomly" selected to enter the rollout group. + // + // For self hosted sites, there are often one or two users, and the user id is probably 0, 1 in these cases. + // If we exclude low ids, we won't get an not an homogeneous distribution over self hosted and WordPress.com + // users, but the purpose of this is to do a progressive rollout, not an necessarily an homogeneous rollout. + // + // To exclude ids 0 and 1, to rollout for 10% users, + // we'll use a test like `id % 100 >= 90` instead of `id % 100 < 10`. + if (accountStore.getAccount().getUserId() % 100 >= (100 - GB_ROLLOUT_PERCENTAGE)) { + if (atLeastOneSiteHasAztecEnabled(siteStore)) { + // If the user has opt-ed out from at least one of their site, then exclude them from the cohort + return; + } + + if (!NetworkUtils.isNetworkAvailable(WordPress.getContext())) { + // If the network is not available, abort. We can't update the remote setting. + return; + } + + // Force the dialog to be shown on updated sites + for (SiteModel site : siteStore.getSites()) { + if (TextUtils.isEmpty(site.getMobileEditor())) { + AppPrefs.setShowGutenbergInfoPopupForTheNewPosts(site.getUrl(), true); + } + } + + // Enable Gutenberg for all sites using a single network call + dispatcher.dispatch(SiteActionBuilder.newDesignateMobileEditorForAllSitesAction( + new DesignateMobileEditorForAllSitesPayload(SiteUtils.GB_EDITOR_NAME))); + + // After enabling Gutenberg on these sites, we consider the user entered the rollout group + AppPrefs.setUserInGutenbergRolloutGroup(); + } + if (!AppPrefs.isDefaultAppWideEditorPreferenceSet()) { return; } @@ -51,6 +103,16 @@ public static void migrateAppWideMobileEditorPreferenceToRemote(final Context co } } + private static boolean atLeastOneSiteHasAztecEnabled(final SiteStore siteStore) { + // We want to make sure to enable Gutenberg only on the sites they didn't opt-out. + for (SiteModel site : siteStore.getSites()) { + if (TextUtils.equals(site.getMobileEditor(), AZTEC_EDITOR_NAME)) { + return true; + } + } + return false; + } + public static boolean enableBlockEditorOnSiteCreation(Dispatcher dispatcher, SiteStore siteStore, int siteLocalSiteID) { SiteModel newSiteModel = siteStore.getSiteByLocalId(siteLocalSiteID); diff --git a/WordPress/src/main/java/org/wordpress/android/util/ViewUtils.kt b/WordPress/src/main/java/org/wordpress/android/util/ViewUtils.kt index 102144cc79bc..aa9312db9ef4 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/ViewUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/ViewUtils.kt @@ -1,8 +1,11 @@ package org.wordpress.android.util +import android.graphics.Rect import android.os.Build.VERSION import android.os.Build.VERSION_CODES +import android.view.TouchDelegate import android.view.View +import androidx.annotation.DimenRes fun View.setVisible(visible: Boolean) { this.visibility = if (visible) View.VISIBLE else View.GONE @@ -13,3 +16,22 @@ fun View.redirectContextClickToLongPressListener() { this.setOnContextClickListener { it.performLongClick() } } } + +fun View.expandTouchTargetArea(@DimenRes dimenRes: Int, heightOnly: Boolean = false) { + val pixels = context.resources.getDimensionPixelSize(dimenRes) + val parent = this.parent as View + + parent.post { + val touchTargetRect = Rect() + getHitRect(touchTargetRect) + touchTargetRect.top -= pixels + touchTargetRect.bottom += pixels + + if (!heightOnly) { + touchTargetRect.right += pixels + touchTargetRect.left -= pixels + } + + parent.touchDelegate = TouchDelegate(touchTargetRect, this) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java b/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java index fea7c9bf8ae3..5e3f53e9ebf9 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java @@ -73,7 +73,8 @@ public class AnalyticsUtils { public enum BlockEditorEnabledSource { VIA_SITE_SETTINGS, ON_SITE_CREATION, - ON_BLOCK_POST_OPENING; + ON_BLOCK_POST_OPENING, + ON_PROGRESSIVE_ROLLOUT; public Map asPropertyMap() { Map properties = new HashMap<>(); diff --git a/WordPress/src/main/java/org/wordpress/android/util/analytics/service/InstallationReferrerServiceLogic.java b/WordPress/src/main/java/org/wordpress/android/util/analytics/service/InstallationReferrerServiceLogic.java index 9b5f727cb1b0..227ee52bc593 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/analytics/service/InstallationReferrerServiceLogic.java +++ b/WordPress/src/main/java/org/wordpress/android/util/analytics/service/InstallationReferrerServiceLogic.java @@ -84,9 +84,10 @@ public void onInstallReferrerSetupFinished(int responseCode) { response.getReferrerClickTimestampSeconds()); AnalyticsTracker.track(AnalyticsTracker.Stat.INSTALLATION_REFERRER_OBTAINED, properties); mReferrerClient.endConnection(); - } catch (RemoteException e) { + } catch (RemoteException | IllegalStateException e) { e.printStackTrace(); - AppLog.i(T.UTILS, "installation referrer: RemoteException occurred"); + CrashLoggingUtils.logException(e, T.UTILS); + AppLog.e(T.UTILS, "installation referrer: " + e.getClass().getSimpleName() + " occurred"); } break; case InstallReferrerResponse.FEATURE_NOT_SUPPORTED: diff --git a/WordPress/src/main/res/layout/actionable_empty_view.xml b/WordPress/src/main/res/layout/actionable_empty_view.xml index a45b78c2a4f8..e071b12ef030 100644 --- a/WordPress/src/main/res/layout/actionable_empty_view.xml +++ b/WordPress/src/main/res/layout/actionable_empty_view.xml @@ -11,6 +11,7 @@ android:layout_width="match_parent" > @@ -46,7 +47,9 @@ android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:scrollbars="vertical"/> + android:scrollbars="vertical" + android:importantForAccessibility="no" + /> diff --git a/WordPress/src/main/res/layout/reader_activity_subs.xml b/WordPress/src/main/res/layout/reader_activity_subs.xml index bfb77fef830c..031be9f91311 100644 --- a/WordPress/src/main/res/layout/reader_activity_subs.xml +++ b/WordPress/src/main/res/layout/reader_activity_subs.xml @@ -46,28 +46,27 @@ android:layout_alignParentBottom="true" android:gravity="center_vertical" android:orientation="horizontal" - android:paddingBottom="@dimen/margin_medium" - android:paddingTop="@dimen/margin_small" - android:paddingStart="@dimen/margin_medium" - android:paddingEnd="@dimen/margin_extra_large"> + > diff --git a/WordPress/src/main/res/layout/reader_cardview_post.xml b/WordPress/src/main/res/layout/reader_cardview_post.xml index ecd678d4e682..d2b0c30cad61 100644 --- a/WordPress/src/main/res/layout/reader_cardview_post.xml +++ b/WordPress/src/main/res/layout/reader_cardview_post.xml @@ -35,7 +35,7 @@ android:layout_centerVertical="true" android:layout_marginEnd="@dimen/reader_card_avatar_margin_end" android:src="@drawable/bg_rectangle_neutral_10_globe_32dp" - android:contentDescription="@string/reader_blavatar_desc" + android:importantForAccessibility="no" style="@style/ReaderImageView.Avatar"> @@ -192,7 +192,7 @@ android:layout_height="wrap_content" android:layout_width="wrap_content" android:maxLines="3" - android:textColor="@color/neutral_30" + android:textColor="@color/neutral_50" android:textSize="@dimen/text_sz_medium" tools:text="text_attribution" style="@style/ReaderTextView" > diff --git a/WordPress/src/main/res/layout/reader_site_header_view.xml b/WordPress/src/main/res/layout/reader_site_header_view.xml index b854655616bd..248c5bae7551 100644 --- a/WordPress/src/main/res/layout/reader_site_header_view.xml +++ b/WordPress/src/main/res/layout/reader_site_header_view.xml @@ -46,7 +46,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/text_blog_name" - android:textColor="@color/neutral_40" + android:textColor="@color/neutral_50" android:textSize="@dimen/text_sz_small" tools:text="text_domain" android:layout_toStartOf="@+id/follow_button" @@ -103,7 +103,7 @@ android:id="@+id/text_blog_follow_count" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/neutral_40" + android:textColor="@color/neutral_50" android:textSize="@dimen/text_sz_small" tools:text="12 followers" /> diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index dee7af0abcdd..48debc6358cd 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -164,6 +164,13 @@ 160dp + + 16dp + 12dp + 12dp + 12dp + 32dp + 24sp 32dp 8dp diff --git a/WordPress/src/main/res/values/reader_styles.xml b/WordPress/src/main/res/values/reader_styles.xml index 6316defa36fa..0e78294c697d 100644 --- a/WordPress/src/main/res/values/reader_styles.xml +++ b/WordPress/src/main/res/values/reader_styles.xml @@ -62,7 +62,7 @@