diff --git a/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java index 56cca449f174..8f43cc67b609 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java +++ b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java @@ -26,7 +26,20 @@ public class NotificationsSettings { public enum Channel { OTHER, BLOGS, - WPCOM + WPCOM; + + public static Channel toNotificationChannel(Integer ordinal) { + switch (ordinal) { + case 0: + return OTHER; + case 1: + return BLOGS; + case 2: + return WPCOM; + default: + throw new IllegalArgumentException("Ordinal does not conform to any existing enum."); + } + } } // The notification setting type, used in BLOGS and OTHER channels @@ -35,6 +48,19 @@ public enum Type { EMAIL, DEVICE; + public static Type toNotificationType(Integer ordinal) { + switch (ordinal) { + case 0: + return TIMELINE; + case 1: + return EMAIL; + case 2: + return DEVICE; + default: + throw new IllegalArgumentException("Ordinal does not conform to any existing enum."); + } + } + public String toString() { switch (this) { case TIMELINE: diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index 9561a8ab4fca..5b25d97a5b35 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -121,6 +121,8 @@ import org.wordpress.android.ui.prefs.homepage.HomepageSettingsDialog; import org.wordpress.android.ui.prefs.language.LocalePickerBottomSheet; import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsFragment; +import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsMySitesFragment; +import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsTypesFragment; import org.wordpress.android.ui.prefs.timezone.SiteSettingsTimezoneBottomSheet; import org.wordpress.android.ui.publicize.PublicizeAccountChooserListAdapter; import org.wordpress.android.ui.publicize.PublicizeButtonPrefsFragment; @@ -294,6 +296,10 @@ public interface AppComponent { void inject(NotificationsSettingsFragment object); + void inject(NotificationsSettingsTypesFragment object); + + void inject(NotificationsSettingsMySitesFragment object); + void inject(NotificationsDetailActivity object); void inject(NotificationsPendingDraftsReceiver object); 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 e46b5cd8a090..1a51263620a5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java @@ -28,6 +28,7 @@ public class RequestCodes { public static final int SMART_LOCK_SAVE = 1400; public static final int SMART_LOCK_READ = 1500; public static final int NOTIFICATION_SETTINGS = 1600; + public static final int NOTIFICATION_SETTINGS_ALERT_RINGTONE = 1610; public static final int ACTIVITY_LOG_DETAIL = 1700; public static final int BACKUP_DOWNLOAD = 1710; public static final int RESTORE = 1720; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java index fe43b25964fc..abe77d2cd604 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java @@ -19,6 +19,7 @@ import org.wordpress.android.R; +/**@see WPSwitchPreferenceX*/ public class WPSwitchPreference extends SwitchPreference implements PreferenceHint { private String mHint; private ColorStateList mTint; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreferenceX.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreferenceX.kt new file mode 100644 index 000000000000..5a92d6109a01 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreferenceX.kt @@ -0,0 +1,109 @@ +package org.wordpress.android.ui.prefs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.text.TextUtils +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.Switch +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.ViewCompat +import androidx.preference.PreferenceViewHolder +import androidx.preference.SwitchPreference +import org.wordpress.android.R + +/** AndroidX implementation of [WPSwitchPreference] + * @see WPSwitchPreference*/ +class WPSwitchPreferenceX(context: Context, attrs: AttributeSet?) : + SwitchPreference(context, attrs), PreferenceHint { + private var mHint: String? = null + private var mTint: ColorStateList? = null + private var mThumbTint: ColorStateList? = null + private var mStartOffset = 0 + + init { + val array = context.obtainStyledAttributes(attrs, R.styleable.SummaryEditTextPreference) + for (i in 0 until array.indexCount) { + val index = array.getIndex(i) + if (index == R.styleable.SummaryEditTextPreference_longClickHint) { + mHint = array.getString(index) + } else if (index == R.styleable.SummaryEditTextPreference_iconTint) { + val resourceId = array.getResourceId(index, 0) + if (resourceId != 0) { + mTint = AppCompatResources.getColorStateList(context, resourceId) + } + } else if (index == R.styleable.SummaryEditTextPreference_switchThumbTint) { + mThumbTint = array.getColorStateList(index) + } else if (index == R.styleable.SummaryEditTextPreference_startOffset) { + mStartOffset = array.getDimensionPixelSize(index, 0) + } + } + array.recycle() + } + + @SuppressLint("UseSwitchCompatOrMaterialCode") + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + val view = holder.itemView + val icon = view.findViewById(android.R.id.icon) + if (icon != null && mTint != null) { + icon.imageTintList = mTint + } + val titleView = view.findViewById(android.R.id.title) + if (titleView != null) { + val res = context.resources + + // add padding to the start of nested preferences + if (!TextUtils.isEmpty(dependency)) { + val margin = res.getDimensionPixelSize(R.dimen.margin_large) + ViewCompat.setPaddingRelative(titleView, margin + mStartOffset, 0, 0, 0) + } else { + ViewCompat.setPaddingRelative(titleView, mStartOffset, 0, 0, 0) + } + } + + // style custom switch preference + val switchControl = getSwitch(view as ViewGroup) + if (switchControl != null) { + if (mThumbTint != null) { + switchControl.thumbTintList = mThumbTint + } + } + + // Add padding to start of switch. + ViewCompat.setPaddingRelative( + getSwitch(view)!!, + context.resources.getDimensionPixelSize(R.dimen.margin_extra_large), 0, 0, 0 + ) + } + @SuppressLint("UseSwitchCompatOrMaterialCode") + private fun getSwitch(parentView: ViewGroup): Switch? { + for (i in 0 until parentView.childCount) { + val childView = parentView.getChildAt(i) + if (childView is Switch) { + return childView + } else if (childView is ViewGroup) { + val theSwitch = getSwitch(childView) + if (theSwitch != null) { + return theSwitch + } + } + } + return null + } + + override fun hasHint(): Boolean { + return !TextUtils.isEmpty(mHint) + } + + override fun getHint(): String { + return mHint!! + } + + override fun setHint(hint: String) { + mHint = hint + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/ChildNotificationSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/ChildNotificationSettingsFragment.kt new file mode 100644 index 000000000000..358c8606ab7d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/ChildNotificationSettingsFragment.kt @@ -0,0 +1,38 @@ +package org.wordpress.android.ui.prefs.notifications + +import android.os.Bundle +import android.view.Gravity +import android.view.View +import androidx.preference.PreferenceFragmentCompat +import androidx.transition.Slide +import androidx.transition.Transition +import androidx.transition.TransitionManager +import com.google.android.material.appbar.AppBarLayout +import org.wordpress.android.R +import org.wordpress.android.util.AniUtils + +/** Child Notification fragments should inherit from this class in order to make navigation consistent.*/ +abstract class ChildNotificationSettingsFragment: PreferenceFragmentCompat() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setMainSwitchVisibility(View.GONE) + } + + override fun onDestroy() { + super.onDestroy() + setMainSwitchVisibility(View.VISIBLE) + } + + private fun setMainSwitchVisibility(visibility: Int) { + with(requireActivity()) { + val mainSwitchToolBarView = findViewById(R.id.main_switch) + val rootView = findViewById(R.id.app_bar_layout) + val transition: Transition = Slide(Gravity.TOP) + transition.duration = AniUtils.Duration.SHORT.toMillis(context) + transition.addTarget(R.id.main_switch) + + TransitionManager.beginDelayedTransition(rootView, transition) + mainSwitchToolBarView.visibility = visibility + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationSettingsFollowedDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationSettingsFollowedDialog.java index 40fe0e268902..4ac719671bd8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationSettingsFollowedDialog.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationSettingsFollowedDialog.java @@ -3,7 +3,6 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; -import android.app.DialogFragment; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; @@ -15,6 +14,8 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SwitchCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -52,7 +53,7 @@ public class NotificationSettingsFollowedDialog extends DialogFragment implement @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - LayoutInflater inflater = getActivity().getLayoutInflater(); + LayoutInflater inflater = requireActivity().getLayoutInflater(); @SuppressLint("InflateParams") View layout = inflater.inflate(R.layout.followed_sites_dialog, null); @@ -88,7 +89,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { } } - AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity()); + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity()); builder.setTitle(getString(R.string.notification_settings_followed_dialog_title)); builder.setPositiveButton(android.R.string.ok, this); builder.setNegativeButton(R.string.cancel, this); @@ -122,10 +123,7 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onDismiss(DialogInterface dialog) { - // TODO: android.app.Fragment is deprecated since Android P. - // Needs to be replaced with android.support.v4.app.Fragment - // See https://developer.android.com/reference/android/app/Fragment - android.app.Fragment target = getTargetFragment(); + Fragment target = getTargetFragment(); if (target != null) { target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getResultIntent()); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsMySitesSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsMySitesSettingsFragment.kt new file mode 100644 index 000000000000..5bbff34b0f91 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsMySitesSettingsFragment.kt @@ -0,0 +1,94 @@ +package org.wordpress.android.ui.prefs.notifications + +import android.content.Intent +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.datasets.ReaderBlogTable +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.generated.AccountActionBuilder +import org.wordpress.android.fluxc.store.AccountStore.AddOrDeleteSubscriptionPayload +import org.wordpress.android.fluxc.store.AccountStore.AddOrDeleteSubscriptionPayload.SubscriptionAction +import org.wordpress.android.fluxc.store.AccountStore.UpdateSubscriptionPayload + +/** Any notification preference fragment that deals with **My Sites** or **My Followed Sites** + * should implement this interface.*/ +interface NotificationsMySitesSettingsFragment { + var mNotificationUpdatedSite: String? + var mPreviousEmailComments: Boolean + var mPreviousEmailPosts: Boolean + var mPreviousNotifyPosts: Boolean + var mUpdateEmailPostsFirst: Boolean + var mPreviousEmailPostsFrequency: String? + var mUpdateSubscriptionFrequencyPayload: UpdateSubscriptionPayload? + var mDispatcher: Dispatcher + + fun onMySiteSettingsChanged(data: Intent?) { + if (data == null) return + val notifyPosts = data.getBooleanExtra(NotificationSettingsFollowedDialog.KEY_NOTIFICATION_POSTS, false) + val emailPosts = data.getBooleanExtra(NotificationSettingsFollowedDialog.KEY_EMAIL_POSTS, false) + val emailPostsFrequency = data.getStringExtra(NotificationSettingsFollowedDialog.KEY_EMAIL_POSTS_FREQUENCY) + val emailComments = data.getBooleanExtra(NotificationSettingsFollowedDialog.KEY_EMAIL_COMMENTS, false) + if (notifyPosts != mPreviousNotifyPosts) { + ReaderBlogTable.setNotificationsEnabledByBlogId(mNotificationUpdatedSite!!.toLong(), notifyPosts) + val payload: AddOrDeleteSubscriptionPayload = if (notifyPosts) { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_ON) + AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite!!, SubscriptionAction.NEW) + } else { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_OFF) + AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite!!, SubscriptionAction.DELETE) + } + mDispatcher.dispatch(AccountActionBuilder.newUpdateSubscriptionNotificationPostAction(payload)) + } + if (emailPosts != mPreviousEmailPosts) { + val payload: AddOrDeleteSubscriptionPayload = if (emailPosts) { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_ON) + AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite!!, SubscriptionAction.NEW) + } else { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_OFF) + AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite!!, SubscriptionAction.DELETE) + } + mDispatcher.dispatch(AccountActionBuilder.newUpdateSubscriptionEmailPostAction(payload)) + } + if (emailPostsFrequency != null && !emailPostsFrequency.equals(mPreviousEmailPostsFrequency, ignoreCase = true) + ) { + val subscriptionFrequency = getSubscriptionFrequencyFromString(emailPostsFrequency) + mUpdateSubscriptionFrequencyPayload = UpdateSubscriptionPayload(mNotificationUpdatedSite!!, + subscriptionFrequency) + /* + * The email post frequency update will be overridden by the email post update if the email post + * frequency callback returns first. Thus, the updates must be dispatched sequentially when the + * email post update is switched from disabled to enabled. + */ + if (emailPosts != mPreviousEmailPosts && emailPosts) { + mUpdateEmailPostsFirst = true + } else { + mDispatcher.dispatch( + AccountActionBuilder.newUpdateSubscriptionEmailPostFrequencyAction( + mUpdateSubscriptionFrequencyPayload) + ) + } + } + if (emailComments != mPreviousEmailComments) { + val payload: AddOrDeleteSubscriptionPayload = if (emailComments) { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_COMMENTS_ON) + AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite!!, SubscriptionAction.NEW) + } else { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_COMMENTS_OFF) + AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite!!, SubscriptionAction.DELETE) + } + mDispatcher.dispatch(AccountActionBuilder.newUpdateSubscriptionEmailCommentAction(payload)) + } + } + + fun getSubscriptionFrequencyFromString(s: String): UpdateSubscriptionPayload.SubscriptionFrequency { + return if (s.equals(UpdateSubscriptionPayload.SubscriptionFrequency.DAILY.toString(), ignoreCase = true)) { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_DAILY) + UpdateSubscriptionPayload.SubscriptionFrequency.DAILY + } else if (s.equals(UpdateSubscriptionPayload.SubscriptionFrequency.WEEKLY.toString(), ignoreCase = true)) { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_WEEKLY) + UpdateSubscriptionPayload.SubscriptionFrequency.WEEKLY + } else { + AnalyticsTracker.track(AnalyticsTracker.Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_INSTANTLY) + UpdateSubscriptionPayload.SubscriptionFrequency.INSTANTLY + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt index 32a4f5910436..8957701207a4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt @@ -8,6 +8,9 @@ import android.widget.CompoundButton import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.commit +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -29,7 +32,8 @@ import javax.inject.Named import android.R as AndroidR @AndroidEntryPoint -class NotificationsSettingsActivity : LocaleAwareActivity(), MainSwitchToolbarListener { +class NotificationsSettingsActivity : LocaleAwareActivity(), + MainSwitchToolbarListener, PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { @Inject lateinit var updateNotificationSettingsUseCase: UpdateNotificationSettingsUseCase @@ -52,10 +56,9 @@ class NotificationsSettingsActivity : LocaleAwareActivity(), MainSwitchToolbarLi setUpMainSwitch() if (savedInstanceState == null) { - @Suppress("DEPRECATION") - fragmentManager.beginTransaction() - .add(R.id.fragment_container, NotificationsSettingsFragment()) - .commit() + supportFragmentManager.commit { + add(R.id.fragment_container, NotificationsSettingsFragment()) + } } messageContainer = findViewById(R.id.notifications_settings_message_container) @@ -82,6 +85,33 @@ class NotificationsSettingsActivity : LocaleAwareActivity(), MainSwitchToolbarLi return super.onOptionsItemSelected(item) } + @Suppress("DEPRECATION") + override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean { + val args = pref.extras + val fragment = supportFragmentManager.fragmentFactory.instantiate( + classLoader, + pref.fragment!! + ) + + val titleView = findViewById(R.id.toolbar_title) + titleView.text = pref.title + + fragment.arguments = args + fragment.setTargetFragment(caller, 0) + // Replace the existing Fragment with the new Fragment. + supportFragmentManager.commit { + setCustomAnimations( + R.anim.fade_in, + R.anim.fade_out, + R.anim.fade_in, + R.anim.fade_out, + ) + replace(R.id.fragment_container, fragment) + addToBackStack(null) + } + return true + } + @Subscribe(threadMode = MAIN) fun onEventMainThread(event: NotificationsSettingsStatusChanged) { if (TextUtils.isEmpty(event.message)) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogFragment.kt new file mode 100644 index 000000000000..9e5a9c41e24a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogFragment.kt @@ -0,0 +1,367 @@ +package org.wordpress.android.ui.prefs.notifications + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.json.JSONException +import org.json.JSONObject +import org.wordpress.android.R +import org.wordpress.android.databinding.NotificationsSettingsSwitchBinding +import org.wordpress.android.models.NotificationsSettings +import org.wordpress.android.ui.prefs.AppPrefs +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.JSONUtils +import org.wordpress.android.util.extensions.getDrawableFromAttribute + +class NotificationsSettingsDialogFragment( + private val channel: NotificationsSettings.Channel, + private val type: NotificationsSettings.Type, + private val blogId: Long = 0, + private val settings: NotificationsSettings, + private val onNotificationsSettingsChangedListener: + NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener, + private val bloggingRemindersProvider: NotificationsSettingsDialogPreference.BloggingRemindersProvider? = null, + private val title: String +): DialogFragment(), PrefMainSwitchToolbarView.MainSwitchToolbarListener, DialogInterface.OnClickListener { + companion object { + const val TAG = "Notifications_Settings_Dialog_Fragment" + private const val SETTING_VALUE_ACHIEVEMENT = "achievement" + } + + private val mUpdatedJson = JSONObject() + private var mTitleViewWithMainSwitch: ViewGroup? = null + + // view to display when main switch is on + private var mDisabledView: View? = null + + // view to display when main switch is off + private var mOptionsView: LinearLayout? = null + + private var mMainSwitchToolbarView: PrefMainSwitchToolbarView? = null + private val mShouldDisplayMainSwitch: Boolean = settings.shouldDisplayMainSwitch(channel, type) + + private var mSettingsArray = arrayOfNulls(0) + private var mSettingsValues: Array? = arrayOfNulls(0) + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + @SuppressLint("InflateParams") + val layout = requireActivity().layoutInflater.inflate(R.layout.notifications_settings_types_dialog, null) + + val outerView = layout.findViewById(R.id.outer_view) + outerView.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + val innerView = LinearLayout(context) + innerView.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + innerView.orientation = LinearLayout.VERTICAL + if (mShouldDisplayMainSwitch) { + val dividerView = View(context) + val dividerHeight = context.resources.getDimensionPixelSize( + R.dimen.notifications_settings_dialog_divider_height + ) + dividerView.background = context.getDrawableFromAttribute(android.R.attr.listDivider) + dividerView.layoutParams = ViewGroup.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, dividerHeight) + innerView.addView(dividerView) + } else { + val spacerView = View(context) + val spacerHeight = context.resources.getDimensionPixelSize(R.dimen.margin_medium) + spacerView.layoutParams = ViewGroup.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, spacerHeight) + innerView.addView(spacerView) + } + mDisabledView = View.inflate(context, R.layout.notifications_tab_disabled_text_layout, null) + mDisabledView?.layoutParams = ViewGroup.LayoutParams( + ActionBar.LayoutParams.MATCH_PARENT, + ActionBar.LayoutParams.WRAP_CONTENT + ) + mOptionsView = LinearLayout(context) + mOptionsView?.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + mOptionsView?.orientation = LinearLayout.VERTICAL + innerView.addView(mDisabledView) + innerView.addView(mOptionsView) + outerView.addView(innerView) + configureLayoutForView(mOptionsView!!) + + val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(requireActivity()).apply { + setTitle(title) + setPositiveButton(android.R.string.ok, this@NotificationsSettingsDialogFragment) + setNegativeButton(R.string.cancel, this@NotificationsSettingsDialogFragment) + setView(layout) + } + if (mShouldDisplayMainSwitch) { + setupTitleViewWithMainSwitch(outerView) + if (mTitleViewWithMainSwitch == null) + AppLog.e(AppLog.T.NOTIFS, "Main switch enabled but layout not set") + else + builder.setCustomTitle(mTitleViewWithMainSwitch) + } + return builder.create() + } + + private fun setupTitleViewWithMainSwitch(view: View) { + when (this.channel) { + NotificationsSettings.Channel.BLOGS -> if (this.type == NotificationsSettings.Type.TIMELINE) { + mTitleViewWithMainSwitch = layoutInflater + .inflate(R.layout.notifications_tab_for_blog_title_layout, view as ViewGroup, false) as ViewGroup + } + + NotificationsSettings.Channel.OTHER, NotificationsSettings.Channel.WPCOM -> {} + } + if (mTitleViewWithMainSwitch != null) { + val titleView = mTitleViewWithMainSwitch!!.findViewById(R.id.title) + titleView.text = title + mMainSwitchToolbarView = mTitleViewWithMainSwitch!!.findViewById(R.id.main_switch) + mMainSwitchToolbarView!!.setMainSwitchToolbarListener(this) + mMainSwitchToolbarView!! + .setBackgroundColor(ContextCompat.getColor(requireContext(), android.R.color.transparent)) + + // Main Switch initial state: + // On: If at least one of the settings options is on + // Off: If all settings options are off + val settingsJson = settings.getSettingsJsonForChannelAndType(channel, type, blogId) + val checkMainSwitch = settings.isAtLeastOneSettingsEnabled( + settingsJson, + mSettingsArray, + mSettingsValues + ) + mMainSwitchToolbarView!!.loadInitialState(checkMainSwitch) + hideDisabledView(mMainSwitchToolbarView!!.isMainChecked) + } + } + + override fun onClick(dialog: DialogInterface?, which: Int) { + if (which == DialogInterface.BUTTON_POSITIVE && mUpdatedJson.length() > 0) { + onNotificationsSettingsChangedListener.onSettingsChanged(this.channel, + this.type, this.blogId, mUpdatedJson) + + // Update the settings json + val keys: Iterator<*> = mUpdatedJson.keys() + while (keys.hasNext()) { + val settingName = keys.next() as String + settings.updateSettingForChannelAndType( + this.channel, this.type, settingName, + mUpdatedJson.optBoolean(settingName), this.blogId + ) + } + } + } + + private fun configureLayoutForView(view: LinearLayout): View { + val settingsJson = settings.getSettingsJsonForChannelAndType(this.channel, this.type, this.blogId) + var summaryArray = arrayOfNulls(0) + when (this.channel) { + NotificationsSettings.Channel.BLOGS -> { + mSettingsArray = requireContext().resources.getStringArray(R.array.notifications_blog_settings) + mSettingsValues = requireContext().resources.getStringArray(R.array.notifications_blog_settings_values) + } + + NotificationsSettings.Channel.OTHER -> { + mSettingsArray = requireContext().resources.getStringArray(R.array.notifications_other_settings) + mSettingsValues = requireContext().resources.getStringArray(R.array.notifications_other_settings_values) + } + + NotificationsSettings.Channel.WPCOM -> { + mSettingsArray = requireContext().resources.getStringArray(R.array.notifications_wpcom_settings) + mSettingsValues = requireContext().resources.getStringArray(R.array.notifications_wpcom_settings_values) + summaryArray = requireContext().resources.getStringArray(R.array.notifications_wpcom_settings_summaries) + } + } + val shouldShowLocalNotifications = + this.channel == NotificationsSettings.Channel.BLOGS && this.type == NotificationsSettings.Type.DEVICE + if (settingsJson != null && mSettingsArray.size == mSettingsValues!!.size) { + for (i in mSettingsArray.indices) { + val settingName = mSettingsArray[i]!! + val settingValue = mSettingsValues!![i]!! + + // Skip a few settings for 'Email' section + if (this.type == NotificationsSettings.Type.EMAIL && settingValue == SETTING_VALUE_ACHIEVEMENT) { + continue + } + + // Add special summary text for the WPCOM section + var settingSummary: String? = null + if (this.channel == NotificationsSettings.Channel.WPCOM && i < summaryArray.size) { + settingSummary = summaryArray[i] + } + val isSettingChecked = JSONUtils.queryJSON(settingsJson, settingValue, true) + val isSettingLast = !shouldShowLocalNotifications && i == mSettingsArray.size - 1 + view.addView( + setupSwitchSettingView( + settingName, settingValue, settingSummary, isSettingChecked, + isSettingLast, mOnCheckedChangedListener + ) + ) + } + } + if (shouldShowLocalNotifications) { + val isBloggingRemindersEnabled = bloggingRemindersProvider != null + addWeeklyRoundupSetting(view, !isBloggingRemindersEnabled) + if (isBloggingRemindersEnabled) { + addBloggingReminderSetting(view) + } + } + return view + } + + private fun addWeeklyRoundupSetting(view: LinearLayout, isLast: Boolean) { + view.addView(setupSwitchSettingView( + requireContext().getString(R.string.weekly_roundup), + null, + null, + AppPrefs.shouldShowWeeklyRoundupNotification(this.blogId), + isLast + ) { compoundButton: CompoundButton?, isChecked: Boolean -> + AppPrefs.setShouldShowWeeklyRoundupNotification( + this.blogId, + isChecked + ) + }) + } + + private fun addBloggingReminderSetting(view: LinearLayout) { + view.addView(setupClickSettingView( + requireContext().getString(R.string.site_settings_blogging_reminders_notification_title), + this.bloggingRemindersProvider?.getSummary(this.blogId), + true + ) { v: View? -> + this.bloggingRemindersProvider?.onClick(this.blogId) + requireDialog().dismiss() + }) + } + + private fun setupSwitchSettingView(settingName: String, settingValue: String?, settingSummary: String?, + isSettingChecked: Boolean, isSettingLast: Boolean, + onCheckedChangeListener: CompoundButton.OnCheckedChangeListener + ): View { + return setupSettingView( + settingName, settingValue, settingSummary, isSettingChecked, + isSettingLast, onCheckedChangeListener, null + ) + } + + private fun setupClickSettingView(settingName: String, settingSummary: String?, isSettingLast: Boolean, + onClickListener: View.OnClickListener + ): View { + return setupSettingView( + settingName, null, settingSummary, false, + isSettingLast, null, onClickListener + ) + } + + private fun setupSettingView(settingName: String, settingValue: String?, settingSummary: String?, + isSettingChecked: Boolean, isSettingLast: Boolean, + onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?, + onClickListener: View.OnClickListener? + ): View { + NotificationsSettingsSwitchBinding.inflate(layoutInflater).apply { + notificationsSwitchTitle.text = settingName + if (!TextUtils.isEmpty(settingSummary)) { + notificationsSwitchSummary.visibility = View.VISIBLE + notificationsSwitchSummary.text = settingSummary + } + if (onCheckedChangeListener != null) { + notificationsSwitch.isChecked = isSettingChecked + notificationsSwitch.tag = settingValue + notificationsSwitch.setOnCheckedChangeListener(onCheckedChangeListener) + rowContainer.setOnClickListener { v -> notificationsSwitch.toggle() } + } else { + notificationsSwitch.visibility = View.GONE + } + if (onClickListener != null) { + rowContainer.setOnClickListener(onClickListener) + } + if (mShouldDisplayMainSwitch && isSettingLast) { + val divider: View = notificationsListDivider + val mlp = divider.layoutParams as ViewGroup.MarginLayoutParams + mlp.leftMargin = 0 + mlp.rightMargin = 0 + divider.layoutParams = mlp + } + return root + } + } + + private val mOnCheckedChangedListener = + CompoundButton.OnCheckedChangeListener { compoundButton, isChecked -> + try { + mUpdatedJson.put(compoundButton.tag.toString(), isChecked) + + // Switch off main switch if all current settings switches are off + if (mMainSwitchToolbarView != null && !isChecked + && areAllSettingsSwitchesUnchecked() + ) { + mMainSwitchToolbarView!!.setChecked(false) + } + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not add notification setting change to JSONObject") + } + } + + override fun onMainSwitchCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { + setSettingsSwitchesChecked(isChecked) + hideDisabledView(isChecked) + } + + /** + * Hide view when Notifications Tab Settings are disabled by toggling the main switch off. + * + * @param isMainChecked TRUE to hide disabled view, FALSE to show disabled view + */ + private fun hideDisabledView(isMainChecked: Boolean) { + mDisabledView!!.visibility = if (isMainChecked) View.GONE else View.VISIBLE + mOptionsView!!.visibility = if (isMainChecked) View.VISIBLE else View.GONE + } + + /** + * Updates Notifications current settings switches state based on the main switch state + * + * @param isMainChecked TRUE to switch on the settings switches. + * FALSE to switch off the settings switches. + */ + private fun setSettingsSwitchesChecked(isMainChecked: Boolean) { + for (settingValue in mSettingsValues!!) { + val toggleSwitch = mOptionsView!!.findViewWithTag(settingValue) + if (toggleSwitch != null) { + toggleSwitch.isChecked = isMainChecked + } + } + } + + // returns true if all current settings switches on the dialog are unchecked + private fun areAllSettingsSwitchesUnchecked(): Boolean { + var settingsSwitchesUnchecked = true + for (settingValue in mSettingsValues!!) { + val toggleSwitch = mOptionsView!!.findViewWithTag(settingValue) + if (toggleSwitch != null) { + val isChecked = toggleSwitch.isChecked + if (isChecked) { + settingsSwitchesUnchecked = false + break + } + } + } + return settingsSwitchesUnchecked + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreferenceX.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreferenceX.kt new file mode 100644 index 000000000000..121f959b4521 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreferenceX.kt @@ -0,0 +1,19 @@ +package org.wordpress.android.ui.prefs.notifications + +import android.content.Context +import android.util.AttributeSet +import androidx.annotation.StringRes +import androidx.preference.DialogPreference +import org.wordpress.android.models.NotificationsSettings + +class NotificationsSettingsDialogPreferenceX( + context: Context, + attrs: AttributeSet?, + val channel: NotificationsSettings.Channel, + val type: NotificationsSettings.Type, + val blogId: Long, + val settings: NotificationsSettings, + val listener: NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener, + val bloggingRemindersProvider: NotificationsSettingsDialogPreference.BloggingRemindersProvider? = null, + @StringRes val dialogTitleRes: Int +) : DialogPreference(context, attrs) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java deleted file mode 100644 index 47c9aaba611a..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java +++ /dev/null @@ -1,1038 +0,0 @@ -package org.wordpress.android.ui.prefs.notifications; - -import android.Manifest; -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.PorterDuff.Mode; -import android.os.Build; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ListView; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.SearchView; -import androidx.core.view.ViewCompat; -import androidx.lifecycle.ViewModelProvider; -import androidx.preference.PreferenceManager; - -import com.android.volley.VolleyError; -import com.wordpress.rest.RestRequest; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.wordpress.android.R; -import org.wordpress.android.WordPress; -import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.analytics.AnalyticsTracker.Stat; -import org.wordpress.android.databinding.JetpackBadgeFooterBinding; -import org.wordpress.android.datasets.ReaderBlogTable; -import org.wordpress.android.fluxc.Dispatcher; -import org.wordpress.android.fluxc.generated.AccountActionBuilder; -import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.fluxc.store.AccountStore.AddOrDeleteSubscriptionPayload; -import org.wordpress.android.fluxc.store.AccountStore.AddOrDeleteSubscriptionPayload.SubscriptionAction; -import org.wordpress.android.fluxc.store.AccountStore.OnSubscriptionUpdated; -import org.wordpress.android.fluxc.store.AccountStore.OnSubscriptionsChanged; -import org.wordpress.android.fluxc.store.AccountStore.SubscriptionType; -import org.wordpress.android.fluxc.store.AccountStore.UpdateSubscriptionPayload; -import org.wordpress.android.fluxc.store.AccountStore.UpdateSubscriptionPayload.SubscriptionFrequency; -import org.wordpress.android.fluxc.store.SiteStore; -import org.wordpress.android.models.JetpackPoweredScreen; -import org.wordpress.android.models.NotificationsSettings; -import org.wordpress.android.models.NotificationsSettings.Channel; -import org.wordpress.android.models.NotificationsSettings.Type; -import org.wordpress.android.ui.WPLaunchActivity; -import org.wordpress.android.ui.bloggingreminders.BloggingReminderUtils; -import org.wordpress.android.ui.bloggingreminders.BloggingRemindersViewModel; -import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment; -import org.wordpress.android.ui.notifications.NotificationEvents; -import org.wordpress.android.ui.notifications.utils.NotificationsUtils; -import org.wordpress.android.ui.prefs.notifications.FollowedBlogsProvider.PreferenceModel; -import org.wordpress.android.ui.prefs.notifications.FollowedBlogsProvider.PreferenceModel.ClickHandler; -import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsDialogPreference.BloggingRemindersProvider; -import org.wordpress.android.ui.utils.UiHelpers; -import org.wordpress.android.ui.utils.UiString; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.BuildConfigWrapper; -import org.wordpress.android.util.JetpackBrandingUtils; -import org.wordpress.android.util.SiteUtils; -import org.wordpress.android.util.ToastUtils; -import org.wordpress.android.util.ToastUtils.Duration; -import org.wordpress.android.util.WPActivityUtils; -import org.wordpress.android.util.WPPermissionUtils; -import org.wordpress.android.util.extensions.ContextExtensionsKt; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import javax.inject.Inject; - -import static org.wordpress.android.fluxc.generated.AccountActionBuilder.newUpdateSubscriptionEmailCommentAction; -import static org.wordpress.android.fluxc.generated.AccountActionBuilder.newUpdateSubscriptionEmailPostAction; -import static org.wordpress.android.fluxc.generated.AccountActionBuilder.newUpdateSubscriptionEmailPostFrequencyAction; -import static org.wordpress.android.fluxc.generated.AccountActionBuilder.newUpdateSubscriptionNotificationPostAction; -import static org.wordpress.android.ui.RequestCodes.NOTIFICATION_SETTINGS; -import static org.wordpress.android.util.WPPermissionUtils.NOTIFICATIONS_PERMISSION_REQUEST_CODE; - -public class NotificationsSettingsFragment extends PreferenceFragment - implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final String KEY_SEARCH_QUERY = "search_query"; - private static final int SITE_SEARCH_VISIBILITY_COUNT = 15; - // The number of notification types we support (e.g. timeline, email, mobile) - private static final int TYPE_COUNT = 3; - private static final int NO_MAXIMUM = -1; - private static final int MAX_SITES_TO_SHOW_ON_FIRST_SCREEN = 3; - - private NotificationsSettings mNotificationsSettings; - private SearchView mSearchView; - private MenuItem mSearchMenuItem; - private boolean mSearchMenuItemCollapsed = true; - - private String mDeviceId; - private String mNotificationUpdatedSite; - private String mPreviousEmailPostsFrequency; - private String mRestoredQuery; - private UpdateSubscriptionPayload mUpdateSubscriptionFrequencyPayload; - private boolean mNotificationsEnabled; - private boolean mPreviousEmailComments; - private boolean mPreviousEmailPosts; - private boolean mPreviousNotifyPosts; - private boolean mUpdateEmailPostsFirst; - private int mSiteCount; - private int mSubscriptionCount; - - private final List mTypePreferenceCategories = new ArrayList<>(); - private PreferenceCategory mBlogsCategory; - @Nullable private PreferenceCategory mFollowedBlogsCategory; - - @Inject AccountStore mAccountStore; - @Inject SiteStore mSiteStore; - @Inject Dispatcher mDispatcher; - @Inject FollowedBlogsProvider mFollowedBlogsProvider; - @Inject BuildConfigWrapper mBuildConfigWrapper; - @Inject ViewModelProvider.Factory mViewModelFactory; - @Inject JetpackBrandingUtils mJetpackBrandingUtils; - @Inject UiHelpers mUiHelpers; - - private BloggingRemindersViewModel mBloggingRemindersViewModel; - private final Map mBloggingRemindersSummariesBySiteId = new HashMap<>(); - - private static final String BLOGGING_REMINDERS_BOTTOM_SHEET_TAG = "blogging-reminders-dialog-tag"; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) getActivity().getApplication()).component().inject(this); - - addPreferencesFromResource(R.xml.notifications_settings); - setHasOptionsMenu(true); - removeSightAndSoundsForAPI26(); - removeFollowedBlogsPreferenceForIfDisabled(); - - // Bump Analytics - if (savedInstanceState == null) { - AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_LIST_OPENED); - } - } - - private void removeSightAndSoundsForAPI26() { - // on API26 we removed the Sight & Sounds category altogether, as it can always be - // overriden by the user in the Device settings, and the settings here - // wouldn't either reflect nor have any effect anyway. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PreferenceScreen preferenceScreen = - (PreferenceScreen) findPreference(getActivity().getString(R.string.wp_pref_notifications_root)); - - PreferenceCategory categorySightsAndSounds = (PreferenceCategory) preferenceScreen - .findPreference(getActivity().getString(R.string.pref_notification_sights_sounds)); - preferenceScreen.removePreference(categorySightsAndSounds); - } - } - - private void removeFollowedBlogsPreferenceForIfDisabled() { - if (!mBuildConfigWrapper.isFollowedSitesSettingsEnabled()) { - PreferenceScreen preferenceScreen = - (PreferenceScreen) findPreference(getActivity().getString(R.string.wp_pref_notifications_root)); - - PreferenceCategory categoryFollowedBlogs = (PreferenceCategory) preferenceScreen - .findPreference(getActivity().getString(R.string.pref_notification_blogs_followed)); - preferenceScreen.removePreference(categoryFollowedBlogs); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - boolean isLoggedIn = mAccountStore.hasAccessToken(); - if (!isLoggedIn) { - // Not logged in users can start Notification Settings from App info > Notifications menu. - // If there isn't a logged in user, just show the entry screen. - Intent intent = new Intent(getContext(), WPLaunchActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - getActivity().finish(); - return; - } - - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity()); - mDeviceId = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, ""); - - if (hasNotificationsSettings()) { - loadNotificationsAndUpdateUI(true); - } - - if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SEARCH_QUERY)) { - mRestoredQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); - } - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - final ListView lv = (ListView) view.findViewById(android.R.id.list); - if (lv != null) { - ViewCompat.setNestedScrollingEnabled(lv, true); - addJetpackBadgeAsFooterIfEnabled(lv); - } - initBloggingReminders(); - } - - @Override public void onViewStateRestored(Bundle savedInstanceState) { - super.onViewStateRestored(savedInstanceState); - PreferenceScreen otherBlogsScreen = (PreferenceScreen) findPreference( - getString(R.string.pref_notification_other_blogs)); - addToolbarToDialog(otherBlogsScreen); - } - - private void addJetpackBadgeAsFooterIfEnabled(ListView listView) { - if (mJetpackBrandingUtils.shouldShowJetpackBranding()) { - final JetpackPoweredScreen screen = JetpackPoweredScreen.WithDynamicText.NOTIFICATIONS_SETTINGS; - final Context context = getContext(); - final LayoutInflater inflater = LayoutInflater.from(context); - final JetpackBadgeFooterBinding binding = JetpackBadgeFooterBinding.inflate(inflater); - binding.footerJetpackBadge.jetpackPoweredBadge.setText( - mUiHelpers.getTextOfUiString( - context, - mJetpackBrandingUtils.getBrandingTextForScreen(screen) - ) - ); - if (mJetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) { - binding.footerJetpackBadge.jetpackPoweredBadge.setOnClickListener(v -> { - mJetpackBrandingUtils.trackBadgeTapped(screen); - new JetpackPoweredBottomSheetFragment().show( - ((AppCompatActivity) getActivity()).getSupportFragmentManager(), - JetpackPoweredBottomSheetFragment.TAG); - }); - } - listView.addFooterView(binding.getRoot(), null, false); - } - } - - @Override - public void onStart() { - super.onStart(); - mDispatcher.register(this); - getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onResume() { - super.onResume(); - - mNotificationsEnabled = NotificationsUtils.isNotificationsEnabled(getActivity()); - refreshSettings(); - } - - @Override - public void onStop() { - super.onStop(); - mDispatcher.unregister(this); - getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - inflater.inflate(R.menu.notifications_settings, menu); - - mSearchMenuItem = menu.findItem(R.id.menu_notifications_settings_search); - mSearchView = (SearchView) mSearchMenuItem.getActionView(); - mSearchView.setQueryHint(getString(R.string.search_sites)); - mBlogsCategory = (PreferenceCategory) findPreference( - getString(R.string.pref_notification_blogs)); - mFollowedBlogsCategory = (PreferenceCategory) findPreference( - getString(R.string.pref_notification_blogs_followed)); - - mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - configureBlogsSettings(mBlogsCategory, true); - configureFollowedBlogsSettings(mFollowedBlogsCategory, true); - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - // we need to perform this check because when the search menu item is collapsed - // a new queryTExtChange event is triggered with an empty value "", and we only - // would want to take care of it when the user actively opened/cleared the search term - configureBlogsSettings(mBlogsCategory, !mSearchMenuItemCollapsed); - configureFollowedBlogsSettings(mFollowedBlogsCategory, !mSearchMenuItemCollapsed); - return true; - } - }); - - mSearchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(@NonNull MenuItem item) { - mSearchMenuItemCollapsed = false; - configureBlogsSettings(mBlogsCategory, true); - configureFollowedBlogsSettings(mFollowedBlogsCategory, true); - return true; - } - - @Override - public boolean onMenuItemActionCollapse(@NonNull MenuItem item) { - mSearchMenuItemCollapsed = true; - configureBlogsSettings(mBlogsCategory, false); - configureFollowedBlogsSettings(mFollowedBlogsCategory, false); - return true; - } - }); - - updateSearchMenuVisibility(); - - // Check for a restored search query (if device was rotated, etc) - if (!TextUtils.isEmpty(mRestoredQuery)) { - mSearchMenuItem.expandActionView(); - mSearchView.setQuery(mRestoredQuery, true); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (mSearchView != null && !TextUtils.isEmpty(mSearchView.getQuery())) { - outState.putString(KEY_SEARCH_QUERY, mSearchView.getQuery().toString()); - } - - super.onSaveInstanceState(outState); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (data != null && requestCode == NOTIFICATION_SETTINGS) { - boolean notifyPosts = - data.getBooleanExtra(NotificationSettingsFollowedDialog.KEY_NOTIFICATION_POSTS, false); - boolean emailPosts = - data.getBooleanExtra(NotificationSettingsFollowedDialog.KEY_EMAIL_POSTS, false); - String emailPostsFrequency = - data.getStringExtra(NotificationSettingsFollowedDialog.KEY_EMAIL_POSTS_FREQUENCY); - boolean emailComments = - data.getBooleanExtra(NotificationSettingsFollowedDialog.KEY_EMAIL_COMMENTS, false); - - if (notifyPosts != mPreviousNotifyPosts) { - ReaderBlogTable.setNotificationsEnabledByBlogId(Long.parseLong(mNotificationUpdatedSite), notifyPosts); - AddOrDeleteSubscriptionPayload payload; - - if (notifyPosts) { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_ON); - payload = new AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite, SubscriptionAction.NEW); - } else { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_OFF); - payload = new AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite, SubscriptionAction.DELETE); - } - - mDispatcher.dispatch(newUpdateSubscriptionNotificationPostAction(payload)); - } - - if (emailPosts != mPreviousEmailPosts) { - AddOrDeleteSubscriptionPayload payload; - - if (emailPosts) { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_ON); - payload = new AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite, SubscriptionAction.NEW); - } else { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_OFF); - payload = new AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite, SubscriptionAction.DELETE); - } - - mDispatcher.dispatch(newUpdateSubscriptionEmailPostAction(payload)); - } - - if (emailPostsFrequency != null && !emailPostsFrequency.equalsIgnoreCase(mPreviousEmailPostsFrequency)) { - SubscriptionFrequency subscriptionFrequency = getSubscriptionFrequencyFromString(emailPostsFrequency); - mUpdateSubscriptionFrequencyPayload = new UpdateSubscriptionPayload(mNotificationUpdatedSite, - subscriptionFrequency); - /* - * The email post frequency update will be overridden by the email post update if the email post - * frequency callback returns first. Thus, the updates must be dispatched sequentially when the - * email post update is switched from disabled to enabled. - */ - if (emailPosts != mPreviousEmailPosts && emailPosts) { - mUpdateEmailPostsFirst = true; - } else { - mDispatcher.dispatch(newUpdateSubscriptionEmailPostFrequencyAction( - mUpdateSubscriptionFrequencyPayload)); - } - } - - if (emailComments != mPreviousEmailComments) { - AddOrDeleteSubscriptionPayload payload; - - if (emailComments) { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_COMMENTS_ON); - payload = new AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite, SubscriptionAction.NEW); - } else { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_COMMENTS_OFF); - payload = new AddOrDeleteSubscriptionPayload(mNotificationUpdatedSite, SubscriptionAction.DELETE); - } - - mDispatcher.dispatch(newUpdateSubscriptionEmailCommentAction(payload)); - } - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onSubscriptionsChanged(OnSubscriptionsChanged event) { - if (event.isError()) { - AppLog.e(T.API, "NotificationsSettingsFragment.onSubscriptionsChanged: " + event.error.message); - } else { - configureFollowedBlogsSettings(mFollowedBlogsCategory, !mSearchMenuItemCollapsed); - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onSubscriptionUpdated(OnSubscriptionUpdated event) { - if (event.isError()) { - AppLog.e(T.API, "NotificationsSettingsFragment.onSubscriptionUpdated: " + event.error.message); - } else if (event.type == SubscriptionType.EMAIL_POST && mUpdateEmailPostsFirst) { - mUpdateEmailPostsFirst = false; - mDispatcher.dispatch(newUpdateSubscriptionEmailPostFrequencyAction(mUpdateSubscriptionFrequencyPayload)); - } else { - mDispatcher.dispatch(AccountActionBuilder.newFetchSubscriptionsAction()); - } - } - - private SubscriptionFrequency getSubscriptionFrequencyFromString(String s) { - if (s.equalsIgnoreCase(SubscriptionFrequency.DAILY.toString())) { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_DAILY); - return SubscriptionFrequency.DAILY; - } else if (s.equalsIgnoreCase(SubscriptionFrequency.WEEKLY.toString())) { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_WEEKLY); - return SubscriptionFrequency.WEEKLY; - } else { - AnalyticsTracker.track(Stat.FOLLOWED_BLOG_NOTIFICATIONS_SETTINGS_EMAIL_INSTANTLY); - return SubscriptionFrequency.INSTANTLY; - } - } - - private void refreshSettings() { - if (!hasNotificationsSettings()) { - EventBus.getDefault() - .post(new NotificationEvents.NotificationsSettingsStatusChanged(getString(R.string.loading))); - } - - if (hasNotificationsSettings()) { - updateUIForNotificationsEnabledState(); - } - - if (!mAccountStore.hasAccessToken()) { - return; - } - - NotificationsUtils.getPushNotificationSettings(getActivity(), new RestRequest.Listener() { - @Override - public void onResponse(JSONObject response) { - AppLog.d(T.NOTIFS, "Get settings action succeeded"); - if (!isAdded()) { - return; - } - - boolean settingsExisted = hasNotificationsSettings(); - if (!settingsExisted) { - EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged(null)); - } - - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity()); - SharedPreferences.Editor editor = settings.edit(); - editor.putString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, response.toString()); - editor.apply(); - - loadNotificationsAndUpdateUI(!settingsExisted); - updateUIForNotificationsEnabledState(); - } - }, new RestRequest.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - if (!isAdded()) { - return; - } - AppLog.e(T.NOTIFS, "Get settings action failed", error); - - if (!hasNotificationsSettings()) { - EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged( - getString(R.string.error_loading_notifications))); - } - } - }); - } - - private void loadNotificationsAndUpdateUI(boolean shouldUpdateUI) { - JSONObject settingsJson; - try { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - settingsJson = new JSONObject( - sharedPreferences.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, "") - ); - } catch (JSONException e) { - AppLog.e(T.NOTIFS, "Could not parse notifications settings JSON"); - return; - } - - if (mNotificationsSettings == null) { - mNotificationsSettings = new NotificationsSettings(settingsJson); - } else { - mNotificationsSettings.updateJson(settingsJson); - } - - if (shouldUpdateUI) { - if (mBlogsCategory == null) { - mBlogsCategory = (PreferenceCategory) findPreference( - getString(R.string.pref_notification_blogs)); - } - - if (mFollowedBlogsCategory == null) { - mFollowedBlogsCategory = (PreferenceCategory) findPreference( - getString(R.string.pref_notification_blogs_followed)); - } - - configureBlogsSettings(mBlogsCategory, false); - configureFollowedBlogsSettings(mFollowedBlogsCategory, false); - configureOtherSettings(); - configureWPComSettings(); - } - } - - private boolean hasNotificationsSettings() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - - return sharedPreferences.contains(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS); - } - - // Updates the UI for preference screens based on if notifications are enabled or not - private void updateUIForNotificationsEnabledState() { - if (mTypePreferenceCategories.size() == 0) { - return; - } - - for (final PreferenceCategory category : mTypePreferenceCategories) { - if (mNotificationsEnabled && category.getPreferenceCount() > TYPE_COUNT) { - category.removePreference(category.getPreference(TYPE_COUNT)); - } else if (!mNotificationsEnabled && category.getPreferenceCount() == TYPE_COUNT) { - Preference disabledMessage = new Preference(getActivity()); - category.addPreference(disabledMessage); - } - - if (category.getPreferenceCount() >= TYPE_COUNT - && category.getPreference(TYPE_COUNT - 1) != null) { - category.getPreference(TYPE_COUNT - 1).setEnabled(mNotificationsEnabled); - } - - if (category.getPreferenceCount() > TYPE_COUNT - && category.getPreference(TYPE_COUNT) != null) { - updateDisabledMessagePreference(category.getPreference(TYPE_COUNT)); - } - } - } - - private void updateDisabledMessagePreference(Preference disabledMessagePreference) { - disabledMessagePreference.setSummary(getDisabledMessageResId()); - disabledMessagePreference.setOnPreferenceClickListener(preference -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && shouldRequestRuntimePermission()) { - // Request runtime permission. - requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, - NOTIFICATIONS_PERMISSION_REQUEST_CODE); - } else { - // Navigate to app settings. - WPPermissionUtils.showNotificationsSettings(getContext()); - } - return true; - }); - } - - private boolean shouldRequestRuntimePermission() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - && !WPPermissionUtils.isPermissionAlwaysDenied(getActivity(), Manifest.permission.POST_NOTIFICATIONS); - } - - @StringRes - private int getDisabledMessageResId() { - if (shouldRequestRuntimePermission()) { - return R.string.notifications_disabled_permission_dialog; - } else { - return R.string.notifications_disabled; - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, - @NonNull String[] permissions, - @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - WPPermissionUtils.setPermissionListAsked(getActivity(), requestCode, permissions, grantResults, false); - } - - private void configureBlogsSettings(PreferenceCategory blogsCategory, boolean showAll) { - if (!isAdded()) { - return; - } - - List sites; - String trimmedQuery = ""; - if (mSearchView != null && !TextUtils.isEmpty(mSearchView.getQuery())) { - trimmedQuery = mSearchView.getQuery().toString().trim(); - sites = mSiteStore.getSitesAccessedViaWPComRestByNameOrUrlMatching(trimmedQuery); - } else { - sites = mSiteStore.getSitesAccessedViaWPComRest(); - } - mSiteCount = sites.size(); - - if (mSiteCount > 0) { - Collections.sort(sites, new Comparator() { - @Override public int compare(SiteModel o1, SiteModel o2) { - return SiteUtils.getSiteNameOrHomeURL(o1).compareToIgnoreCase(SiteUtils.getSiteNameOrHomeURL(o2)); - } - }); - } - - Context context = getActivity(); - - blogsCategory.removeAll(); - - int maxSitesToShow = showAll ? NO_MAXIMUM : MAX_SITES_TO_SHOW_ON_FIRST_SCREEN; - int count = 0; - for (SiteModel site : sites) { - if (context == null) { - return; - } - - count++; - if (maxSitesToShow != NO_MAXIMUM && count > maxSitesToShow) { - break; - } - - PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(context); - prefScreen.setTitle(SiteUtils.getSiteNameOrHomeURL(site)); - prefScreen.setSummary(SiteUtils.getHomeURLOrHostName(site)); - addPreferencesForPreferenceScreen(prefScreen, Channel.BLOGS, site.getSiteId()); - blogsCategory.addPreference(prefScreen); - } - - // Add a message in a preference if there are no matching search results - if (mSiteCount == 0 && !TextUtils.isEmpty(trimmedQuery)) { - Preference searchResultsPref = new Preference(context); - searchResultsPref - .setSummary(String.format(getString(R.string.notifications_no_search_results), trimmedQuery)); - blogsCategory.addPreference(searchResultsPref); - } - - if (mSiteCount > maxSitesToShow && !showAll) { - // append a "view all" option - appendViewAllSitesOption(context, getString(R.string.pref_notification_blogs), false); - } - - updateSearchMenuVisibility(); - } - - private void configureFollowedBlogsSettings(@Nullable PreferenceCategory blogsCategory, final boolean showAll) { - if (!isAdded() || blogsCategory == null) { - return; - } - - List models; - String query = ""; - - if (mSearchView != null && !TextUtils.isEmpty(mSearchView.getQuery())) { - query = mSearchView.getQuery().toString().trim(); - models = mFollowedBlogsProvider.getAllFollowedBlogs(query); - } else { - models = mFollowedBlogsProvider.getAllFollowedBlogs(null); - } - - Context context = getActivity(); - blogsCategory.removeAll(); - - int maxSitesToShow = showAll ? NO_MAXIMUM : MAX_SITES_TO_SHOW_ON_FIRST_SCREEN; - mSubscriptionCount = 0; - - if (models.size() > 0) { - Collections.sort(models, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle())); - } - - for (final PreferenceModel preferenceModel : models) { - if (context == null) { - return; - } - - mSubscriptionCount++; - - if (!showAll && mSubscriptionCount > maxSitesToShow) { - break; - } - - PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(context); - prefScreen.setTitle(preferenceModel.getTitle()); - prefScreen.setSummary(preferenceModel.getSummary()); - - ClickHandler clickHandler = preferenceModel.getClickHandler(); - if (clickHandler != null) { - prefScreen.setOnPreferenceClickListener(preference -> { - mNotificationUpdatedSite = preferenceModel.getBlogId(); - mPreviousNotifyPosts = clickHandler.getShouldNotifyPosts(); - mPreviousEmailPosts = clickHandler.getShouldEmailPosts(); - mPreviousEmailPostsFrequency = clickHandler.getEmailPostFrequency(); - mPreviousEmailComments = clickHandler.getShouldEmailComments(); - NotificationSettingsFollowedDialog dialog = new NotificationSettingsFollowedDialog(); - Bundle args = new Bundle(); - args.putBoolean(NotificationSettingsFollowedDialog.ARG_NOTIFICATION_POSTS, - mPreviousNotifyPosts); - args.putBoolean(NotificationSettingsFollowedDialog.ARG_EMAIL_POSTS, - mPreviousEmailPosts); - args.putString(NotificationSettingsFollowedDialog.ARG_EMAIL_POSTS_FREQUENCY, - mPreviousEmailPostsFrequency); - args.putBoolean(NotificationSettingsFollowedDialog.ARG_EMAIL_COMMENTS, - mPreviousEmailComments); - dialog.setArguments(args); - dialog.setTargetFragment(NotificationsSettingsFragment.this, NOTIFICATION_SETTINGS); - dialog.show(getFragmentManager(), NotificationSettingsFollowedDialog.TAG); - return true; - }); - } else { - prefScreen.setEnabled(false); - } - - blogsCategory.addPreference(prefScreen); - } - - // Add message if there are no matching search results. - if (mSubscriptionCount == 0 && !TextUtils.isEmpty(query)) { - Preference searchResultsPref = new Preference(context); - searchResultsPref.setSummary(String.format(getString(R.string.notifications_no_search_results), query)); - blogsCategory.addPreference(searchResultsPref); - } - - // Add view all entry when more sites than maximum to show. - if (!showAll && mSubscriptionCount > maxSitesToShow) { - appendViewAllSitesOption(context, getString(R.string.pref_notification_blogs_followed), true); - } - - updateSearchMenuVisibility(); - } - - private void appendViewAllSitesOption(Context context, String preference, boolean isFollowed) { - PreferenceCategory blogsCategory = (PreferenceCategory) findPreference(preference); - - PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(context); - prefScreen.setTitle(isFollowed ? R.string.notification_settings_item_your_sites_all_followed_sites - : R.string.notification_settings_item_your_sites_all_your_sites); - addSitesForViewAllSitesScreen(prefScreen, isFollowed); - blogsCategory.addPreference(prefScreen); - } - - private void updateSearchMenuVisibility() { - // Show the search menu item in the toolbar if we have enough sites - if (mSearchMenuItem != null) { - mSearchMenuItem.setVisible(mSiteCount > SITE_SEARCH_VISIBILITY_COUNT - || mSubscriptionCount > SITE_SEARCH_VISIBILITY_COUNT); - } - } - - private void configureOtherSettings() { - PreferenceScreen otherBlogsScreen = (PreferenceScreen) findPreference( - getString(R.string.pref_notification_other_blogs)); - addPreferencesForPreferenceScreen(otherBlogsScreen, Channel.OTHER, 0); - } - - private void configureWPComSettings() { - PreferenceCategory otherPreferenceCategory = (PreferenceCategory) findPreference( - getString(R.string.pref_notification_other_category)); - NotificationsSettingsDialogPreference devicePreference = new NotificationsSettingsDialogPreference( - getActivity(), null, Channel.WPCOM, NotificationsSettings.Type.DEVICE, 0, mNotificationsSettings, - mOnSettingsChangedListener - ); - devicePreference.setTitle(R.string.notification_settings_item_other_account_emails); - devicePreference.setDialogTitle(R.string.notification_settings_item_other_account_emails); - devicePreference.setSummary(R.string.notification_settings_item_other_account_emails_summary); - otherPreferenceCategory.addPreference(devicePreference); - } - - private void addPreferencesForPreferenceScreen(PreferenceScreen preferenceScreen, Channel channel, long blogId) { - Context context = getActivity(); - if (context == null) { - return; - } - - PreferenceCategory rootCategory = new PreferenceCategory(context); - rootCategory.setTitle(R.string.notification_types); - preferenceScreen.addPreference(rootCategory); - - NotificationsSettingsDialogPreference timelinePreference = new NotificationsSettingsDialogPreference( - context, null, channel, NotificationsSettings.Type.TIMELINE, blogId, mNotificationsSettings, - mOnSettingsChangedListener - ); - - setPreferenceIcon(timelinePreference, R.drawable.ic_bell_white_24dp); - timelinePreference.setTitle(R.string.notifications_tab); - timelinePreference.setDialogTitle(R.string.notifications_tab); - timelinePreference.setSummary(R.string.notifications_tab_summary); - rootCategory.addPreference(timelinePreference); - - NotificationsSettingsDialogPreference emailPreference = new NotificationsSettingsDialogPreference( - context, null, channel, NotificationsSettings.Type.EMAIL, blogId, mNotificationsSettings, - mOnSettingsChangedListener - ); - - setPreferenceIcon(emailPreference, R.drawable.ic_mail_white_24dp); - emailPreference.setTitle(R.string.email); - emailPreference.setDialogTitle(R.string.email); - emailPreference.setSummary(R.string.notifications_email_summary); - rootCategory.addPreference(emailPreference); - - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - String deviceID = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, null); - if (!TextUtils.isEmpty(deviceID)) { - NotificationsSettingsDialogPreference devicePreference = new NotificationsSettingsDialogPreference( - context, null, channel, NotificationsSettings.Type.DEVICE, blogId, mNotificationsSettings, - mOnSettingsChangedListener, mBloggingRemindersProvider - ); - setPreferenceIcon(devicePreference, R.drawable.ic_phone_white_24dp); - devicePreference.setTitle(R.string.app_notifications); - devicePreference.setDialogTitle(R.string.app_notifications); - devicePreference.setSummary(R.string.notifications_push_summary); - devicePreference.setEnabled(mNotificationsEnabled); - rootCategory.addPreference(devicePreference); - } - - mTypePreferenceCategories.add(rootCategory); - } - - private void setPreferenceIcon(NotificationsSettingsDialogPreference preference, @DrawableRes int drawableRes) { - preference.setIcon(drawableRes); - preference.getIcon().setTintMode(Mode.SRC_IN); - preference.getIcon().setTintList(ContextExtensionsKt - .getColorStateListFromAttribute(preference.getContext(), R.attr.wpColorOnSurfaceMedium)); - } - - private void addSitesForViewAllSitesScreen(PreferenceScreen preferenceScreen, boolean isFollowed) { - Context context = getActivity(); - if (context == null) { - return; - } - - PreferenceCategory rootCategory = new PreferenceCategory(context); - rootCategory.setTitle(isFollowed ? R.string.notification_settings_category_followed_sites - : R.string.notification_settings_category_your_sites); - preferenceScreen.addPreference(rootCategory); - - if (isFollowed) { - configureFollowedBlogsSettings(rootCategory, true); - } else { - configureBlogsSettings(rootCategory, true); - } - } - - private final NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener - mOnSettingsChangedListener = - new NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener() { - @SuppressWarnings("unchecked") - @Override - public void onSettingsChanged(Channel channel, NotificationsSettings.Type type, long blogId, - JSONObject newValues) { - if (!isAdded()) { - return; - } - - // Construct a new settings JSONObject to send back to WP.com - JSONObject settingsObject = new JSONObject(); - switch (channel) { - case BLOGS: - try { - JSONObject blogObject = new JSONObject(); - blogObject.put(NotificationsSettings.KEY_BLOG_ID, blogId); - - JSONArray blogsArray = new JSONArray(); - if (type == Type.DEVICE) { - newValues.put(NotificationsSettings.KEY_DEVICE_ID, Long.parseLong(mDeviceId)); - JSONArray devicesArray = new JSONArray(); - devicesArray.put(newValues); - blogObject.put(NotificationsSettings.KEY_DEVICES, devicesArray); - blogsArray.put(blogObject); - } else { - blogObject.put(type.toString(), newValues); - blogsArray.put(blogObject); - } - - settingsObject.put(NotificationsSettings.KEY_BLOGS, blogsArray); - } catch (JSONException e) { - AppLog.e(T.NOTIFS, "Could not build notification settings object"); - } - break; - case OTHER: - try { - JSONObject otherObject = new JSONObject(); - if (type == Type.DEVICE) { - newValues.put(NotificationsSettings.KEY_DEVICE_ID, Long.parseLong(mDeviceId)); - JSONArray devicesArray = new JSONArray(); - devicesArray.put(newValues); - otherObject.put(NotificationsSettings.KEY_DEVICES, devicesArray); - } else { - otherObject.put(type.toString(), newValues); - } - - settingsObject.put(NotificationsSettings.KEY_OTHER, otherObject); - } catch (JSONException e) { - AppLog.e(T.NOTIFS, "Could not build notification settings object"); - } - break; - case WPCOM: - try { - settingsObject.put(NotificationsSettings.KEY_WPCOM, newValues); - } catch (JSONException e) { - AppLog.e(T.NOTIFS, "Could not build notification settings object"); - } - break; - } - - if (settingsObject.length() > 0) { - WordPress.getRestClientUtilsV1_1() - .post("/me/notifications/settings", settingsObject, null, null, null); - } - } - }; - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, @NonNull Preference preference) { - super.onPreferenceTreeClick(preferenceScreen, preference); - - if (preference instanceof PreferenceScreen) { - addToolbarToDialog(preference); - AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_STREAMS_OPENED); - } else { - AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_DETAILS_OPENED); - } - - return false; - } - - private void addToolbarToDialog(Preference preference) { - Dialog prefDialog = ((PreferenceScreen) preference).getDialog(); - if (prefDialog != null) { - String title = String.valueOf(preference.getTitle()); - WPActivityUtils.addToolbarToDialog(this, prefDialog, title); - } - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(getString(R.string.pref_key_notification_pending_drafts))) { - if (getActivity() != null) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - boolean shouldNotifyOfPendingDrafts = prefs.getBoolean("wp_pref_notification_pending_drafts", true); - if (shouldNotifyOfPendingDrafts) { - AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_PENDING_DRAFTS_SETTINGS_ENABLED); - } else { - AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_PENDING_DRAFTS_SETTINGS_DISABLED); - } - } - } else if (key.equals(getString(R.string.wp_pref_custom_notification_sound))) { - final String defaultPath = - getString(R.string.notification_settings_item_sights_and_sounds_choose_sound_default); - final String value = sharedPreferences.getString(key, defaultPath); - - if (value.trim().toLowerCase(Locale.ROOT).startsWith("file://")) { - // sound path begins with 'file://` which will lead to FileUriExposedException when used. Revert to - // default and let the user know. - AppLog.w(T.NOTIFS, "Notification sound starts with unacceptable scheme: " + value); - - Context context = WordPress.getContext(); - if (context != null) { - // let the user know we won't be using the selected sound - ToastUtils.showToast(context, R.string.notification_sound_has_invalid_path, Duration.LONG); - } - } - } - } - - @Nullable - private AppCompatActivity getAppCompatActivity() { - final Activity activity = getActivity(); - if (activity instanceof AppCompatActivity) { - return (AppCompatActivity) activity; - } - return null; - } - - private final BloggingRemindersProvider mBloggingRemindersProvider = new BloggingRemindersProvider() { - @Override public String getSummary(long blogId) { - UiString uiString = mBloggingRemindersSummariesBySiteId.get(blogId); - return uiString != null ? mUiHelpers.getTextOfUiString(getContext(), uiString).toString() : null; - } - - @Override public void onClick(long blogId) { - mBloggingRemindersViewModel.onNotificationSettingsItemClicked(blogId); - } - }; - - private void initBloggingReminders() { - if (!isAdded()) { - return; - } - - final AppCompatActivity appCompatActivity = getAppCompatActivity(); - if (appCompatActivity != null) { - mBloggingRemindersViewModel = new ViewModelProvider(appCompatActivity, mViewModelFactory) - .get(BloggingRemindersViewModel.class); - BloggingReminderUtils.observeBottomSheet( - mBloggingRemindersViewModel.isBottomSheetShowing(), - appCompatActivity, - BLOGGING_REMINDERS_BOTTOM_SHEET_TAG, - appCompatActivity::getSupportFragmentManager - ); - - mBloggingRemindersViewModel.getNotificationsSettingsUiState() - .observe(appCompatActivity, mBloggingRemindersSummariesBySiteId::putAll); - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.kt new file mode 100644 index 000000000000..8ac032452e19 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.kt @@ -0,0 +1,900 @@ +package org.wordpress.android.ui.prefs.notifications + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.ListView +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.core.content.edit +import androidx.core.view.ViewCompat +import androidx.preference.DialogPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import androidx.preference.PreferenceScreen +import com.wordpress.rest.RestRequest +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.WordPress.Companion.getRestClientUtilsV1_1 +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.databinding.JetpackBadgeFooterBinding +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.generated.AccountActionBuilder +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.AccountStore.OnSubscriptionUpdated +import org.wordpress.android.fluxc.store.AccountStore.OnSubscriptionsChanged +import org.wordpress.android.fluxc.store.AccountStore.SubscriptionType +import org.wordpress.android.fluxc.store.AccountStore.UpdateSubscriptionPayload +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.models.JetpackPoweredScreen +import org.wordpress.android.models.NotificationsSettings +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.WPLaunchActivity +import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment +import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsSettingsStatusChanged +import org.wordpress.android.ui.notifications.utils.NotificationsUtils +import org.wordpress.android.ui.prefs.notifications.FollowedBlogsProvider.PreferenceModel +import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener +import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsMySitesFragment.Companion.ARG_IS_FOLLOWED +import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsTypesFragment.Companion.ARG_BLOG_ID +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.BuildConfigWrapper +import org.wordpress.android.util.JetpackBrandingUtils +import org.wordpress.android.util.SiteUtils +import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.WPPermissionUtils +import javax.inject.Inject + +class NotificationsSettingsFragment : PreferenceFragmentCompat(), NotificationsMySitesSettingsFragment, + OnSharedPreferenceChangeListener { + private var mNotificationsSettings: NotificationsSettings? = null + private var mSearchView: SearchView? = null + private var mSearchMenuItem: MenuItem? = null + private var mSearchMenuItemCollapsed = true + private var mDeviceId: String? = null + private var mRestoredQuery: String? = null + private var mNotificationsEnabled = false + override var mNotificationUpdatedSite: String? = null + override var mPreviousEmailPostsFrequency: String? = null + override var mUpdateSubscriptionFrequencyPayload: UpdateSubscriptionPayload? = null + override var mPreviousEmailComments = false + override var mPreviousEmailPosts = false + override var mPreviousNotifyPosts = false + override var mUpdateEmailPostsFirst = false + private var mSiteCount = 0 + private var mSubscriptionCount = 0 + private val mTypePreferenceCategories: MutableList = ArrayList() + private var mBlogsCategory: PreferenceCategory? = null + private var mFollowedBlogsCategory: PreferenceCategory? = null + + @Inject + lateinit var mAccountStore: AccountStore + + @Inject + lateinit var mSiteStore: SiteStore + + @Inject + override lateinit var mDispatcher: Dispatcher + + @Inject + lateinit var mFollowedBlogsProvider: FollowedBlogsProvider + + @Inject + lateinit var mBuildConfigWrapper: BuildConfigWrapper + + @Inject + lateinit var mJetpackBrandingUtils: JetpackBrandingUtils + + @Inject + lateinit var mUiHelpers: UiHelpers + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + addPreferencesFromResource(R.xml.notifications_settings) + setHasOptionsMenu(true) + removeSightAndSoundsForAPI26() + removeFollowedBlogsPreferenceForIfDisabled() + + // Bump Analytics + if (savedInstanceState == null) { + AnalyticsTracker.track(Stat.NOTIFICATION_SETTINGS_LIST_OPENED) + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) = Unit + + @Suppress("DEPRECATION") + override fun onDisplayPreferenceDialog(preference: Preference) { + if (preference is NotificationsSettingsDialogPreferenceX) { + if (parentFragmentManager.findFragmentByTag(NotificationsSettingsDialogFragment.TAG) != null) { + return + } + with(preference) { + NotificationsSettingsDialogFragment( + channel = channel, + type = type, + blogId = blogId, + settings = settings, + onNotificationsSettingsChangedListener = listener, + bloggingRemindersProvider = bloggingRemindersProvider, + title = context.getString(dialogTitleRes) + ).apply { + setTargetFragment( + this@NotificationsSettingsFragment, + RequestCodes.NOTIFICATION_SETTINGS + ) + }.show( + parentFragmentManager, + NotificationsSettingsDialogFragment.TAG + ) + } + } else { + super.onDisplayPreferenceDialog(preference) + } + } + + private fun removeSightAndSoundsForAPI26() { + // on API26 we removed the Sight & Sounds category altogether, as it can always be + // overridden by the user in the Device settings, and the settings here + // wouldn't either reflect nor have any effect anyway. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val (preferenceScreen, categorySightsAndSounds) = + getPreferenceScreenAndCategory(R.string.pref_notification_sights_sounds) + + if (categorySightsAndSounds != null) { + preferenceScreen?.removePreference(categorySightsAndSounds) + } + } + } + + private fun removeFollowedBlogsPreferenceForIfDisabled() { + if (!mBuildConfigWrapper.isFollowedSitesSettingsEnabled) { + val (preferenceScreen, categoryFollowedBlogs) = + getPreferenceScreenAndCategory(R.string.pref_notification_blogs_followed) + + if (categoryFollowedBlogs != null) { + preferenceScreen?.removePreference(categoryFollowedBlogs) + } + } + } + + private fun getPreferenceScreenAndCategory(pref: Int): Pair { + val preferenceScreen = + findPreference(requireActivity().getString(R.string.wp_pref_notifications_root)) as PreferenceScreen? + val requiredPreference = preferenceScreen + ?.findPreference(requireActivity().getString(pref)) as PreferenceCategory? + return Pair(preferenceScreen, requiredPreference) + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + val isLoggedIn = mAccountStore.hasAccessToken() + if (!isLoggedIn) { + // Not logged in users can start Notification Settings from App info > Notifications menu. + // If there isn't a logged in user, just show the entry screen. + val intent = Intent(context, WPLaunchActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + requireActivity().finish() + return + } + val settings = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + mDeviceId = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, "") + if (hasNotificationsSettings()) { + loadNotificationsAndUpdateUI(true) + } + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SEARCH_QUERY)) { + mRestoredQuery = savedInstanceState.getString(KEY_SEARCH_QUERY) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val lv = view.findViewById(R.id.list) as ListView? + if (lv != null) { + ViewCompat.setNestedScrollingEnabled(lv, true) + addJetpackBadgeAsFooterIfEnabled(lv) + } + } + + private fun addJetpackBadgeAsFooterIfEnabled(listView: ListView) { + if (mJetpackBrandingUtils.shouldShowJetpackBranding()) { + val screen: JetpackPoweredScreen = JetpackPoweredScreen.WithDynamicText.NOTIFICATIONS_SETTINGS + val inflater = LayoutInflater.from(context) + val binding: JetpackBadgeFooterBinding = JetpackBadgeFooterBinding.inflate(inflater) + binding.footerJetpackBadge.jetpackPoweredBadge.text = mUiHelpers.getTextOfUiString( + requireContext(), + mJetpackBrandingUtils.getBrandingTextForScreen(screen) + ) + if (mJetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) { + binding.footerJetpackBadge.jetpackPoweredBadge.setOnClickListener { + mJetpackBrandingUtils.trackBadgeTapped(screen) + JetpackPoweredBottomSheetFragment().show( + (activity as AppCompatActivity).supportFragmentManager, + JetpackPoweredBottomSheetFragment.TAG + ) + } + } + listView.addFooterView(binding.root, null, false) + } + } + + override fun onStart() { + super.onStart() + mDispatcher.register(this) + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onResume() { + super.onResume() + mNotificationsEnabled = NotificationsUtils.isNotificationsEnabled(activity) + setToolbarTitle() + refreshSettings() + } + + override fun onStop() { + super.onStop() + mDispatcher.unregister(this) + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.notifications_settings, menu) + mSearchMenuItem = menu.findItem(R.id.menu_notifications_settings_search) + mSearchView = mSearchMenuItem?.actionView as SearchView? + mSearchView?.queryHint = getString(R.string.search_sites) + mBlogsCategory = findPreference( + getString(R.string.pref_notification_blogs) + ) as PreferenceCategory? + mFollowedBlogsCategory = findPreference( + getString(R.string.pref_notification_blogs_followed) + ) as PreferenceCategory? + mSearchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + configureBlogsSettings(mBlogsCategory, true) + configureFollowedBlogsSettings(mFollowedBlogsCategory, true) + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + // we need to perform this check because when the search menu item is collapsed + // a new queryTExtChange event is triggered with an empty value "", and we only + // would want to take care of it when the user actively opened/cleared the search term + configureBlogsSettings(mBlogsCategory, !mSearchMenuItemCollapsed) + configureFollowedBlogsSettings(mFollowedBlogsCategory, !mSearchMenuItemCollapsed) + return true + } + }) + mSearchMenuItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + mSearchMenuItemCollapsed = false + configureBlogsSettings(mBlogsCategory, true) + configureFollowedBlogsSettings(mFollowedBlogsCategory, true) + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + mSearchMenuItemCollapsed = true + configureBlogsSettings(mBlogsCategory, false) + configureFollowedBlogsSettings(mFollowedBlogsCategory, false) + return true + } + }) + updateSearchMenuVisibility() + + // Check for a restored search query (if device was rotated, etc) + if (!TextUtils.isEmpty(mRestoredQuery)) { + mSearchMenuItem?.expandActionView() + mSearchView?.setQuery(mRestoredQuery, true) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + if (mSearchView != null && !TextUtils.isEmpty(mSearchView!!.query)) { + outState.putString(KEY_SEARCH_QUERY, mSearchView!!.query.toString()) + } + super.onSaveInstanceState(outState) + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == RequestCodes.NOTIFICATION_SETTINGS) { + this.onMySiteSettingsChanged(data) + } else if (requestCode == RequestCodes.NOTIFICATION_SETTINGS_ALERT_RINGTONE && data != null) { + val ringtone: Uri? = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + val settings = PreferenceManager.getDefaultSharedPreferences(requireContext()) + settings.edit { + putString( + getString(R.string.wp_pref_custom_notification_sound), + (ringtone ?: "").toString() + ) + } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onSubscriptionsChanged(event: OnSubscriptionsChanged) { + if (event.isError) { + AppLog.e(AppLog.T.API, "NotificationsSettingsFragment.onSubscriptionsChanged: " + event.error.message) + } else { + configureFollowedBlogsSettings(mFollowedBlogsCategory, !mSearchMenuItemCollapsed) + } + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onSubscriptionUpdated(event: OnSubscriptionUpdated) { + if (event.isError) { + AppLog.e(AppLog.T.API, "NotificationsSettingsFragment.onSubscriptionUpdated: " + event.error.message) + } else if (event.type == SubscriptionType.EMAIL_POST && mUpdateEmailPostsFirst) { + mUpdateEmailPostsFirst = false + mDispatcher.dispatch( + AccountActionBuilder.newUpdateSubscriptionEmailPostFrequencyAction( + mUpdateSubscriptionFrequencyPayload + ) + ) + } else { + mDispatcher.dispatch(AccountActionBuilder.newFetchSubscriptionsAction()) + } + } + + private fun refreshSettings() { + if (!hasNotificationsSettings()) { + EventBus.getDefault() + .post(NotificationsSettingsStatusChanged(getString(R.string.loading))) + } + if (hasNotificationsSettings()) { + updateUIForNotificationsEnabledState() + } + if (!mAccountStore.hasAccessToken()) { + return + } + NotificationsUtils.getPushNotificationSettings(activity, RestRequest.Listener { response -> + AppLog.d(AppLog.T.NOTIFS, "Get settings action succeeded") + if (!isAdded) { + return@Listener + } + val settingsExisted = hasNotificationsSettings() + if (!settingsExisted) { + EventBus.getDefault().post(NotificationsSettingsStatusChanged(null)) + } + val settings = PreferenceManager.getDefaultSharedPreferences( + requireActivity() + ) + val editor = settings.edit() + editor.putString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, response.toString()) + editor.apply() + loadNotificationsAndUpdateUI(!settingsExisted) + updateUIForNotificationsEnabledState() + }, RestRequest.ErrorListener { error -> + if (!isAdded) { + return@ErrorListener + } + AppLog.e(AppLog.T.NOTIFS, "Get settings action failed", error) + if (!hasNotificationsSettings()) { + EventBus.getDefault().post( + NotificationsSettingsStatusChanged( + getString(R.string.error_loading_notifications) + ) + ) + } + }) + } + + private fun loadNotificationsAndUpdateUI(shouldUpdateUI: Boolean) { + val settingsJson: JSONObject = try { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences( + requireActivity() + ) + JSONObject( + sharedPreferences.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, "")!! + ) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not parse notifications settings JSON") + return + } + if (mNotificationsSettings == null) { + mNotificationsSettings = NotificationsSettings(settingsJson) + } else { + mNotificationsSettings!!.updateJson(settingsJson) + } + if (shouldUpdateUI) { + if (mBlogsCategory == null) { + mBlogsCategory = findPreference( + getString(R.string.pref_notification_blogs) + ) as PreferenceCategory? + } + if (mFollowedBlogsCategory == null) { + mFollowedBlogsCategory = findPreference( + getString(R.string.pref_notification_blogs_followed) + ) as PreferenceCategory? + } + configureBlogsSettings(mBlogsCategory, false) + configureFollowedBlogsSettings(mFollowedBlogsCategory, false) + configureOtherSettings() + configureWPComSettings() + } + } + + private fun hasNotificationsSettings(): Boolean { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences( + requireActivity() + ) + return sharedPreferences.contains(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS) + } + + // Updates the UI for preference screens based on if notifications are enabled or not + private fun updateUIForNotificationsEnabledState() { + if (mTypePreferenceCategories.size == 0) { + return + } + for (category in mTypePreferenceCategories) { + if (mNotificationsEnabled && category.preferenceCount > TYPE_COUNT) { + category.removePreference(category.getPreference(TYPE_COUNT)) + } else if (!mNotificationsEnabled && category.preferenceCount == TYPE_COUNT) { + val disabledMessage = Preference(requireActivity()) + category.addPreference(disabledMessage) + } + if (category.preferenceCount >= TYPE_COUNT + ) { + category.getPreference(TYPE_COUNT - 1).isEnabled = + mNotificationsEnabled + } + if (category.preferenceCount > TYPE_COUNT + ) { + updateDisabledMessagePreference(category.getPreference(TYPE_COUNT)) + } + } + } + + @Suppress("DEPRECATION") + private fun updateDisabledMessagePreference(disabledMessagePreference: Preference) { + disabledMessagePreference.setSummary(disabledMessageResId) + disabledMessagePreference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && shouldRequestRuntimePermission()) { + // Request runtime permission. + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + WPPermissionUtils.NOTIFICATIONS_PERMISSION_REQUEST_CODE + ) + } else { + // Navigate to app settings. + WPPermissionUtils.showNotificationsSettings(requireContext()) + } + true + } + } + + private fun shouldRequestRuntimePermission(): Boolean { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + !WPPermissionUtils.isPermissionAlwaysDenied(requireActivity(), Manifest.permission.POST_NOTIFICATIONS)) + } + + @get:StringRes + private val disabledMessageResId: Int + get() = if (shouldRequestRuntimePermission()) { + R.string.notifications_disabled_permission_dialog + } else { + R.string.notifications_disabled + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + WPPermissionUtils.setPermissionListAsked(requireActivity(), requestCode, permissions, grantResults, false) + } + + private fun configureBlogsSettings(blogsCategory: PreferenceCategory?, showAll: Boolean) { + if (!isAdded) { + return + } + val sites: List + var trimmedQuery = "" + if (mSearchView != null && !TextUtils.isEmpty(mSearchView!!.query)) { + trimmedQuery = mSearchView!!.query.toString().trim { it <= ' ' } + sites = mSiteStore.getSitesAccessedViaWPComRestByNameOrUrlMatching(trimmedQuery) + } else { + sites = mSiteStore.sitesAccessedViaWPComRest + } + mSiteCount = sites.size + if (mSiteCount > 0) { + sites.sortedWith { o1, o2 -> + SiteUtils.getSiteNameOrHomeURL(o1) + .compareTo(SiteUtils.getSiteNameOrHomeURL(o2), ignoreCase = true) + } + } + blogsCategory!!.removeAll() + val maxSitesToShow = if (showAll) NO_MAXIMUM else MAX_SITES_TO_SHOW_ON_FIRST_SCREEN + + setBlogsPreferenceScreen(sites, maxSitesToShow, blogsCategory) + + // Add a message in a preference if there are no matching search results + if (mSiteCount == 0 && !TextUtils.isEmpty(trimmedQuery)) { + val searchResultsPref = Preference(requireContext()) + searchResultsPref.summary = + String.format(getString(R.string.notifications_no_search_results), trimmedQuery) + blogsCategory.addPreference(searchResultsPref) + } + if (mSiteCount > maxSitesToShow && !showAll) { + // append a "view all" option + appendViewAllSitesOption(getString(R.string.pref_notification_blogs), false) + } + updateSearchMenuVisibility() + } + + private fun setBlogsPreferenceScreen(sites: List, maxSitesToShow: Int, + blogsCategory: PreferenceCategory) { + val context: Context? = activity + var count = 0 + for (site in sites) { + if (context == null) { + return + } + count++ + if (maxSitesToShow != NO_MAXIMUM && count > maxSitesToShow) { + break + } + val prefScreen = preferenceManager.createPreferenceScreen(context) + prefScreen.title = SiteUtils.getSiteNameOrHomeURL(site) + prefScreen.summary = SiteUtils.getHomeURLOrHostName(site) + prefScreen.extras.apply { + putLong(ARG_BLOG_ID, site.siteId) + putInt( + NotificationsSettingsTypesFragment.ARG_NOTIFICATION_CHANNEL, + NotificationsSettings.Channel.BLOGS.ordinal + ) + } + prefScreen.fragment = NotificationsSettingsTypesFragment::class.qualifiedName + blogsCategory.addPreference(prefScreen) + } + } + + private fun configureFollowedBlogsSettings(blogsCategory: PreferenceCategory?, showAll: Boolean) { + if (!isAdded || blogsCategory == null) + return + + var models: List + var query = "" + if (mSearchView != null && !TextUtils.isEmpty(mSearchView!!.query)) { + query = mSearchView!!.query.toString().trim { it <= ' ' } + models = mFollowedBlogsProvider.getAllFollowedBlogs(query) + } else { + models = mFollowedBlogsProvider.getAllFollowedBlogs(null) + } + blogsCategory.removeAll() + + val maxSitesToShow = if (showAll) NO_MAXIMUM else MAX_SITES_TO_SHOW_ON_FIRST_SCREEN + mSubscriptionCount = 0 + + models = models.sortedWith { (title): PreferenceModel, (otherTitle): PreferenceModel -> + title.compareTo(otherTitle, ignoreCase = true) + } + + setFollowedBlogsPreferenceScreen(models, maxSitesToShow, showAll, blogsCategory) + + // Add message if there are no matching search results. + if (mSubscriptionCount == 0 && !TextUtils.isEmpty(query)) { + val searchResultsPref = Preference(requireContext()) + searchResultsPref.summary = String.format(getString(R.string.notifications_no_search_results), query) + blogsCategory.addPreference(searchResultsPref) + } + + // Add view all entry when more sites than maximum to show. + if (!showAll && mSubscriptionCount > maxSitesToShow) { + appendViewAllSitesOption(getString(R.string.pref_notification_blogs_followed), true) + } + updateSearchMenuVisibility() + } + + @Suppress("DEPRECATION") + private fun setFollowedBlogsPreferenceScreen(models: List, maxSitesToShow: Int, showAll: Boolean, + blogsCategory: PreferenceCategory) { + val context: Context? = activity + for ((title, summary, blogId, clickHandler) in models) { + if (context == null) + return + mSubscriptionCount++ + if (!showAll && mSubscriptionCount > maxSitesToShow) + break + val prefScreen = preferenceManager.createPreferenceScreen(context) + prefScreen.title = title + prefScreen.summary = summary + if (clickHandler != null) { + prefScreen.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + mNotificationUpdatedSite = blogId + mPreviousNotifyPosts = clickHandler.shouldNotifyPosts + mPreviousEmailPosts = clickHandler.shouldEmailPosts + mPreviousEmailPostsFrequency = clickHandler.emailPostFrequency + mPreviousEmailComments = clickHandler.shouldEmailComments + val dialog = NotificationSettingsFollowedDialog() + val args = Bundle().apply { + putBoolean(NotificationSettingsFollowedDialog.ARG_NOTIFICATION_POSTS, mPreviousNotifyPosts) + putBoolean(NotificationSettingsFollowedDialog.ARG_EMAIL_POSTS, mPreviousEmailPosts) + putString(NotificationSettingsFollowedDialog.ARG_EMAIL_POSTS_FREQUENCY, + mPreviousEmailPostsFrequency) + putBoolean(NotificationSettingsFollowedDialog.ARG_EMAIL_COMMENTS, mPreviousEmailComments) + } + dialog.arguments = args + dialog.setTargetFragment(this@NotificationsSettingsFragment, + RequestCodes.NOTIFICATION_SETTINGS) + dialog.show(parentFragmentManager, NotificationSettingsFollowedDialog.TAG) + true + } + } else { + prefScreen.isEnabled = false + } + blogsCategory.addPreference(prefScreen) + } + } + + private fun appendViewAllSitesOption(preference: String, isFollowed: Boolean) { + val blogsCategory = findPreference(preference) as PreferenceCategory? + val prefScreen = preferenceManager.createPreferenceScreen(requireContext()) + prefScreen.fragment = NotificationsSettingsMySitesFragment::class.qualifiedName + prefScreen.setTitle( + if (isFollowed) + R.string.notification_settings_item_your_sites_all_followed_sites + else + R.string.notification_settings_item_your_sites_all_your_sites + ) + prefScreen.extras.apply { + putBoolean(ARG_IS_FOLLOWED, isFollowed) + } + addSitesForViewAllSitesScreen(prefScreen, isFollowed) + blogsCategory?.addPreference(prefScreen) + } + + private fun updateSearchMenuVisibility() { + // Show the search menu item in the toolbar if we have enough sites + if (mSearchMenuItem != null) { + mSearchMenuItem!!.isVisible = (mSiteCount > SITE_SEARCH_VISIBILITY_COUNT + || mSubscriptionCount > SITE_SEARCH_VISIBILITY_COUNT) + } + } + + private fun configureOtherSettings() { + val otherBlogsScreen = findPreference( + getString(R.string.pref_notification_other_blogs) + ) as PreferenceScreen? + otherBlogsScreen?.let { + it.extras.apply { + putLong(ARG_BLOG_ID, 0) + putInt( + NotificationsSettingsTypesFragment.ARG_NOTIFICATION_CHANNEL, + NotificationsSettings.Channel.OTHER.ordinal + ) + } + it.fragment = NotificationsSettingsTypesFragment::class.qualifiedName + } + } + + private fun configureWPComSettings() { + val otherPreferenceCategory = findPreference( + getString(R.string.pref_notification_other_category) + ) as PreferenceCategory? + + // Remove previously configured preference. + val previouslyConfiguredPreference = otherPreferenceCategory?.findPreference( + getString(R.string.notification_settings_item_other_account_emails) + ) as DialogPreference? + if (previouslyConfiguredPreference != null) { + otherPreferenceCategory?.removePreference(previouslyConfiguredPreference) + } + + // Add the preference back with updated details + val devicePreference = NotificationsSettingsDialogPreferenceX( + context = requireContext(), + attrs = null, + channel = NotificationsSettings.Channel.WPCOM, + type = NotificationsSettings.Type.DEVICE, + blogId = 0, + settings = mNotificationsSettings!!, + listener = mOnSettingsChangedListener, + dialogTitleRes = R.string.notification_settings_item_other_account_emails + ).apply { + setTitle(R.string.notification_settings_item_other_account_emails) + key = getString(R.string.notification_settings_item_other_account_emails) + setSummary(R.string.notification_settings_item_other_account_emails_summary) + } + + otherPreferenceCategory?.addPreference(devicePreference) + } + + private fun addSitesForViewAllSitesScreen(preferenceScreen: PreferenceScreen, isFollowed: Boolean) { + val context = activity ?: return + val rootCategory = PreferenceCategory(context) + rootCategory.setTitle( + if (isFollowed) + R.string.notification_settings_category_followed_sites + else + R.string.notification_settings_category_your_sites + ) + preferenceScreen.addPreference(rootCategory) + if (isFollowed) { + configureFollowedBlogsSettings(rootCategory, true) + } else { + configureBlogsSettings(rootCategory, true) + } + } + + private val mOnSettingsChangedListener = + OnNotificationsSettingsChangedListener { channel, type, blogId, newValues -> + if (!isAdded) { + return@OnNotificationsSettingsChangedListener + } + + // Construct a new settings JSONObject to send back to WP.com + val settingsObject = JSONObject() + when (channel!!) { + NotificationsSettings.Channel.BLOGS -> try { + val blogObject = JSONObject() + blogObject.put(NotificationsSettings.KEY_BLOG_ID, blogId) + val blogsArray = JSONArray() + if (type == NotificationsSettings.Type.DEVICE) { + newValues.put(NotificationsSettings.KEY_DEVICE_ID, mDeviceId!!.toLong()) + val devicesArray = JSONArray() + devicesArray.put(newValues) + blogObject.put(NotificationsSettings.KEY_DEVICES, devicesArray) + blogsArray.put(blogObject) + } else { + blogObject.put(type.toString(), newValues) + blogsArray.put(blogObject) + } + settingsObject.put(NotificationsSettings.KEY_BLOGS, blogsArray) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not build notification settings object") + } + + NotificationsSettings.Channel.OTHER -> try { + val otherObject = JSONObject() + if (type == NotificationsSettings.Type.DEVICE) { + newValues.put(NotificationsSettings.KEY_DEVICE_ID, mDeviceId!!.toLong()) + val devicesArray = JSONArray() + devicesArray.put(newValues) + otherObject.put(NotificationsSettings.KEY_DEVICES, devicesArray) + } else { + otherObject.put(type.toString(), newValues) + } + settingsObject.put(NotificationsSettings.KEY_OTHER, otherObject) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not build notification settings object") + } + + NotificationsSettings.Channel.WPCOM -> try { + settingsObject.put(NotificationsSettings.KEY_WPCOM, newValues) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not build notification settings object") + } + } + if (settingsObject.length() > 0) { + getRestClientUtilsV1_1() + .post("/me/notifications/settings", settingsObject, null, null, null) + } + } + + @Suppress("DEPRECATION") + override fun onPreferenceTreeClick(preference: Preference): Boolean { + if (preference is PreferenceScreen) { + AnalyticsTracker.track(Stat.NOTIFICATION_SETTINGS_STREAMS_OPENED) + } else { + AnalyticsTracker.track(Stat.NOTIFICATION_SETTINGS_DETAILS_OPENED) + } + + /* Since ringtone preference has been removed in androidx.preference, we use a workaround as + * recommended here: https://issuetracker.google.com/issues/37057453#comment3 */ + val notificationSoundPreferenceKey = getString(R.string.wp_pref_custom_notification_sound) + return if (preference.key?.equals(notificationSoundPreferenceKey) == true) { + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI) + val settings = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val existingValue: String? = settings.getString(notificationSoundPreferenceKey, null) + if (existingValue != null) { + if (existingValue.isEmpty()) { + // Select "Silent" + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, null as Uri?) + } else { + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Uri.parse(existingValue)) + } + } else { + // No ringtone has been selected, set to the default + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Settings.System.DEFAULT_NOTIFICATION_URI) + } + startActivityForResult(intent, RequestCodes.NOTIFICATION_SETTINGS_ALERT_RINGTONE) + true + } else { + super.onPreferenceTreeClick(preference) + } + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { + if (key == getString(R.string.pref_key_notification_pending_drafts)) { + activity?.let { + val prefs = PreferenceManager.getDefaultSharedPreferences(it) + val shouldNotifyOfPendingDrafts = prefs.getBoolean("wp_pref_notification_pending_drafts", true) + if (shouldNotifyOfPendingDrafts) { + AnalyticsTracker.track(Stat.NOTIFICATION_PENDING_DRAFTS_SETTINGS_ENABLED) + } else { + AnalyticsTracker.track(Stat.NOTIFICATION_PENDING_DRAFTS_SETTINGS_DISABLED) + } + } + } else if (key == getString(R.string.wp_pref_custom_notification_sound)) { + val defaultPath = getString(R.string.notification_settings_item_sights_and_sounds_choose_sound_default) + val value = sharedPreferences.getString(key, defaultPath) + if (value!!.trim { it <= ' ' }.lowercase().startsWith("file://")) { + // sound path begins with 'file://` which will lead to FileUriExposedException when used. Revert to + // default and let the user know. + AppLog.w( + AppLog.T.NOTIFS, + "Notification sound starts with unacceptable scheme: $value" + ) + val context = WordPress.getContext() + ToastUtils.showToast( + context, + R.string.notification_sound_has_invalid_path, + ToastUtils.Duration.LONG + ) + } + } + } + + private fun setToolbarTitle() { + with(requireActivity() as AppCompatActivity) { + val titleView = findViewById(R.id.toolbar_title) + titleView.text = getString(R.string.notification_settings) + } + } + + companion object { + const val TAG = "NOTIFICATION_SETTINGS_FRAGMENT_TAG" + private const val KEY_SEARCH_QUERY = "search_query" + private const val SITE_SEARCH_VISIBILITY_COUNT = 15 + + // The number of notification types we support (e.g. timeline, email, mobile) + private const val TYPE_COUNT = 3 + private const val NO_MAXIMUM = -1 + private const val MAX_SITES_TO_SHOW_ON_FIRST_SCREEN = 3 + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsMySitesFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsMySitesFragment.kt new file mode 100644 index 000000000000..610027a2d000 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsMySitesFragment.kt @@ -0,0 +1,208 @@ +package org.wordpress.android.ui.prefs.notifications + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.generated.AccountActionBuilder +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.AccountStore.OnSubscriptionUpdated +import org.wordpress.android.fluxc.store.AccountStore.OnSubscriptionsChanged +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.models.NotificationsSettings +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.SiteUtils +import javax.inject.Inject + +class NotificationsSettingsMySitesFragment: ChildNotificationSettingsFragment(), NotificationsMySitesSettingsFragment { + companion object { + const val ARG_IS_FOLLOWED = "ARG_IS_FOLLOWED" + } + override var mNotificationUpdatedSite: String? = null + override var mPreviousEmailPostsFrequency: String? = null + override var mUpdateSubscriptionFrequencyPayload: AccountStore.UpdateSubscriptionPayload? = null + override var mPreviousEmailComments: Boolean = false + override var mPreviousEmailPosts: Boolean = false + override var mPreviousNotifyPosts: Boolean = false + override var mUpdateEmailPostsFirst: Boolean = false + + @Inject + override lateinit var mDispatcher: Dispatcher + + @Inject + lateinit var mSiteStore: SiteStore + + @Inject + lateinit var mFollowedBlogsProvider: FollowedBlogsProvider + + private lateinit var rootCategory: PreferenceCategory + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + + val isFollowed: Boolean + requireArguments().apply { + isFollowed = getBoolean(ARG_IS_FOLLOWED) + } + rootCategory = PreferenceCategory(requireContext()) + rootCategory.setTitle( + if (isFollowed) + R.string.notification_settings_category_followed_sites + else + R.string.notification_settings_category_your_sites + ) + preferenceScreen?.addPreference(rootCategory) + if (isFollowed) { + configureFollowedBlogsSettings(rootCategory) + } else { + configureBlogsSettings(rootCategory) + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.notification_settings_my_sites, rootKey) + } + + override fun onStart() { + super.onStart() + mDispatcher.register(this) + } + + override fun onStop() { + super.onStop() + mDispatcher.unregister(this) + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onSubscriptionsChanged(event: OnSubscriptionsChanged) { + if (event.isError) { + AppLog.e(AppLog.T.API, "NotificationsSettingsFragment.onSubscriptionsChanged: " + event.error.message) + } else { + configureFollowedBlogsSettings(rootCategory) + } + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onSubscriptionUpdated(event: OnSubscriptionUpdated) { + if (event.isError) { + AppLog.e(AppLog.T.API, "NotificationsSettingsFragment.onSubscriptionUpdated: " + event.error.message) + } else if (event.type == AccountStore.SubscriptionType.EMAIL_POST && mUpdateEmailPostsFirst) { + mUpdateEmailPostsFirst = false + mDispatcher.dispatch( + AccountActionBuilder.newUpdateSubscriptionEmailPostFrequencyAction( + mUpdateSubscriptionFrequencyPayload + ) + ) + } else { + mDispatcher.dispatch(AccountActionBuilder.newFetchSubscriptionsAction()) + } + } + + private fun configureFollowedBlogsSettings(blogsCategory: PreferenceCategory?) { + if (!isAdded || blogsCategory == null) + return + + val models: List = + mFollowedBlogsProvider.getAllFollowedBlogs(null) + .sortedWith { (title): FollowedBlogsProvider.PreferenceModel, + (otherTitle): FollowedBlogsProvider.PreferenceModel -> + title.compareTo( + otherTitle, + ignoreCase = true + ) + } + blogsCategory.removeAll() + + setFollowedBlogsPreferenceScreen(models, blogsCategory) + } + + @Suppress("DEPRECATION") + private fun setFollowedBlogsPreferenceScreen(models: List, + blogsCategory: PreferenceCategory) { + val context: Context? = activity + for ((title, summary, blogId, clickHandler) in models) { + if (context == null) + return + val prefScreen = preferenceManager.createPreferenceScreen(context) + prefScreen.title = title + prefScreen.summary = summary + if (clickHandler != null) { + prefScreen.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + mNotificationUpdatedSite = blogId + mPreviousNotifyPosts = clickHandler.shouldNotifyPosts + mPreviousEmailPosts = clickHandler.shouldEmailPosts + mPreviousEmailPostsFrequency = clickHandler.emailPostFrequency + mPreviousEmailComments = clickHandler.shouldEmailComments + val dialog = NotificationSettingsFollowedDialog() + val args = Bundle().apply { + putBoolean(NotificationSettingsFollowedDialog.ARG_NOTIFICATION_POSTS, mPreviousNotifyPosts) + putBoolean(NotificationSettingsFollowedDialog.ARG_EMAIL_POSTS, mPreviousEmailPosts) + putString(NotificationSettingsFollowedDialog.ARG_EMAIL_POSTS_FREQUENCY, + mPreviousEmailPostsFrequency) + putBoolean(NotificationSettingsFollowedDialog.ARG_EMAIL_COMMENTS, mPreviousEmailComments) + } + dialog.arguments = args + dialog.setTargetFragment(this@NotificationsSettingsMySitesFragment, + RequestCodes.NOTIFICATION_SETTINGS) + dialog.show(parentFragmentManager, NotificationSettingsFollowedDialog.TAG) + true + } + } else { + prefScreen.isEnabled = false + } + blogsCategory.addPreference(prefScreen) + } + } + + private fun configureBlogsSettings(blogsCategory: PreferenceCategory?) { + if (!isAdded || blogsCategory == null) { + return + } + val sites: List = mSiteStore.sitesAccessedViaWPComRest + .sortedWith { o1, o2 -> + SiteUtils.getSiteNameOrHomeURL(o1) + .compareTo(SiteUtils.getSiteNameOrHomeURL(o2), ignoreCase = true) + } + blogsCategory.removeAll() + + val context: Context? = activity + for (site in sites) { + if (context == null) { + return + } + val prefScreen = preferenceManager.createPreferenceScreen(context) + prefScreen.title = SiteUtils.getSiteNameOrHomeURL(site) + prefScreen.summary = SiteUtils.getHomeURLOrHostName(site) + prefScreen.extras.apply { + putLong(NotificationsSettingsTypesFragment.ARG_BLOG_ID, site.siteId) + putInt( + NotificationsSettingsTypesFragment.ARG_NOTIFICATION_CHANNEL, + NotificationsSettings.Channel.BLOGS.ordinal + ) + } + prefScreen.fragment = NotificationsSettingsTypesFragment::class.qualifiedName + blogsCategory.addPreference(prefScreen) + } + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == RequestCodes.NOTIFICATION_SETTINGS) { + this.onMySiteSettingsChanged(data) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsTypesFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsTypesFragment.kt new file mode 100644 index 000000000000..65c2aa61e076 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsTypesFragment.kt @@ -0,0 +1,263 @@ +package org.wordpress.android.ui.prefs.notifications + +import android.graphics.PorterDuff +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceManager +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.models.NotificationsSettings +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.bloggingreminders.BloggingReminderUtils +import org.wordpress.android.ui.bloggingreminders.BloggingRemindersViewModel +import org.wordpress.android.ui.notifications.utils.NotificationsUtils +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.extensions.getColorStateListFromAttribute +import javax.inject.Inject + +class NotificationsSettingsTypesFragment: ChildNotificationSettingsFragment() { + companion object { + const val ARG_BLOG_ID = "ARG_BLOG_ID" + const val ARG_NOTIFICATION_CHANNEL = "ARG_NOTIFICATION_CHANNEL" + const val ARG_NOTIFICATIONS_ENABLED = "ARG_NOTIFICATIONS_ENABLED" + + private const val BLOGGING_REMINDERS_BOTTOM_SHEET_TAG = "blogging-reminders-dialog-tag" + } + + @Inject + lateinit var mViewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var mUiHelpers: UiHelpers + + private var mDeviceId: String? = null + private var mNotificationsSettings: NotificationsSettings? = null + private var mNotificationsEnabled: Boolean = false + private var mBloggingRemindersViewModel: BloggingRemindersViewModel? = null + private val mBloggingRemindersSummariesBySiteId: MutableMap = HashMap() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initBloggingReminders() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.notification_settings_types, rootKey) + + loadNotificationsSettings() + + val settings = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + mDeviceId = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, "") + + val blogId: Long + val channel: NotificationsSettings.Channel + requireArguments().apply { + blogId = getLong(ARG_BLOG_ID) + channel = NotificationsSettings.Channel.toNotificationChannel(getInt(ARG_NOTIFICATION_CHANNEL)) + mNotificationsEnabled = getBoolean(ARG_NOTIFICATIONS_ENABLED) + } + + val context = requireContext() + val category = PreferenceCategory(context) + category.setTitle(R.string.notification_types) + preferenceScreen.addPreference(category) + + val timelinePreference = NotificationsSettingsDialogPreferenceX( + context = context, attrs = null, channel = channel, type = NotificationsSettings.Type.TIMELINE, + blogId = blogId, settings = mNotificationsSettings!!, listener = mOnSettingsChangedListener, + dialogTitleRes = R.string.notifications_tab + ).apply { + setPreferenceIcon(R.drawable.ic_bell_white_24dp) + setTitle(R.string.notifications_tab) + setSummary(R.string.notifications_tab_summary) + key = getString(R.string.notifications_tab) + } + category.addPreference(timelinePreference) + + val emailPreference = NotificationsSettingsDialogPreferenceX( + context = context, attrs = null, channel = channel, type = NotificationsSettings.Type.EMAIL, + blogId = blogId, settings = mNotificationsSettings!!, listener = mOnSettingsChangedListener, + dialogTitleRes = R.string.email + ).apply { + setPreferenceIcon(R.drawable.ic_mail_white_24dp) + setTitle(R.string.email) + setSummary(R.string.notifications_email_summary) + key = getString(R.string.email) + } + category.addPreference(emailPreference) + + if (!TextUtils.isEmpty(mDeviceId)) { + val devicePreference = NotificationsSettingsDialogPreferenceX( + context = context, attrs = null, channel = channel, type = NotificationsSettings.Type.DEVICE, + blogId = blogId, settings = mNotificationsSettings!!, listener = mOnSettingsChangedListener, + bloggingRemindersProvider = mBloggingRemindersProvider, dialogTitleRes = R.string.app_notifications + ).apply { + setPreferenceIcon(R.drawable.ic_phone_white_24dp) + setTitle(R.string.app_notifications) + setSummary(R.string.notifications_push_summary) + key = getString(R.string.app_notifications) + isEnabled = mNotificationsEnabled + } + category.addPreference(devicePreference) + } + } + + @Suppress("DEPRECATION", "Warnings") + override fun onDisplayPreferenceDialog(preference: Preference) { + if (preference is NotificationsSettingsDialogPreferenceX) { + if (parentFragmentManager.findFragmentByTag(NotificationsSettingsDialogFragment.TAG) != null) { + return + } + + with(preference) { + NotificationsSettingsDialogFragment( + channel = channel, + type = type, + blogId = blogId, + settings = settings, + onNotificationsSettingsChangedListener = listener, + bloggingRemindersProvider = bloggingRemindersProvider, + title = context.getString(dialogTitleRes) + ).apply { + setTargetFragment( + this@NotificationsSettingsTypesFragment, + RequestCodes.NOTIFICATION_SETTINGS + ) + }.show( + parentFragmentManager, + NotificationsSettingsDialogFragment.TAG + ) + } + } else { + super.onDisplayPreferenceDialog(preference) + } + } + + private val mOnSettingsChangedListener = + NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener { channel, type, blogId, + newValues -> + if (!isAdded) { + return@OnNotificationsSettingsChangedListener + } + + // Construct a new settings JSONObject to send back to WP.com + val settingsObject = JSONObject() + when (channel!!) { + NotificationsSettings.Channel.BLOGS -> try { + val blogObject = JSONObject() + blogObject.put(NotificationsSettings.KEY_BLOG_ID, blogId) + val blogsArray = JSONArray() + if (type == NotificationsSettings.Type.DEVICE) { + newValues.put(NotificationsSettings.KEY_DEVICE_ID, mDeviceId!!.toLong()) + val devicesArray = JSONArray() + devicesArray.put(newValues) + blogObject.put(NotificationsSettings.KEY_DEVICES, devicesArray) + blogsArray.put(blogObject) + } else { + blogObject.put(type.toString(), newValues) + blogsArray.put(blogObject) + } + settingsObject.put(NotificationsSettings.KEY_BLOGS, blogsArray) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not build notification settings object") + } + + NotificationsSettings.Channel.OTHER -> try { + val otherObject = JSONObject() + if (type == NotificationsSettings.Type.DEVICE) { + newValues.put(NotificationsSettings.KEY_DEVICE_ID, mDeviceId!!.toLong()) + val devicesArray = JSONArray() + devicesArray.put(newValues) + otherObject.put(NotificationsSettings.KEY_DEVICES, devicesArray) + } else { + otherObject.put(type.toString(), newValues) + } + settingsObject.put(NotificationsSettings.KEY_OTHER, otherObject) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not build notification settings object") + } + + NotificationsSettings.Channel.WPCOM -> try { + settingsObject.put(NotificationsSettings.KEY_WPCOM, newValues) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not build notification settings object") + } + } + if (settingsObject.length() > 0) { + WordPress.getRestClientUtilsV1_1() + .post("/me/notifications/settings", settingsObject, null, null, null) + } + } + + private val mBloggingRemindersProvider: NotificationsSettingsDialogPreference.BloggingRemindersProvider = object : + NotificationsSettingsDialogPreference.BloggingRemindersProvider { + override fun getSummary(blogId: Long): String? { + val uiString = mBloggingRemindersSummariesBySiteId[blogId] + return if (uiString != null) mUiHelpers.getTextOfUiString(requireContext(), uiString).toString() else null + } + + override fun onClick(blogId: Long) { + mBloggingRemindersViewModel!!.onNotificationSettingsItemClicked(blogId) + } + } + + private fun initBloggingReminders() { + if (!isAdded) { + return + } + (activity as AppCompatActivity?)?.let { activity -> + mBloggingRemindersViewModel = ViewModelProvider( + activity, + mViewModelFactory + )[BloggingRemindersViewModel::class.java] + BloggingReminderUtils.observeBottomSheet( + mBloggingRemindersViewModel!!.isBottomSheetShowing, + activity, + BLOGGING_REMINDERS_BOTTOM_SHEET_TAG + ) { activity.supportFragmentManager } + mBloggingRemindersViewModel!!.notificationsSettingsUiState + .observe(activity) { map -> + mBloggingRemindersSummariesBySiteId.putAll(map) + } + } + } + + private fun loadNotificationsSettings() { + val settingsJson: JSONObject = try { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + JSONObject( + sharedPreferences.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, "")!! + ) + } catch (e: JSONException) { + AppLog.e(AppLog.T.NOTIFS, "Could not parse notifications settings JSON") + return + } + if (mNotificationsSettings == null) { + mNotificationsSettings = NotificationsSettings(settingsJson) + } else { + mNotificationsSettings!!.updateJson(settingsJson) + } + } + + private fun NotificationsSettingsDialogPreferenceX.setPreferenceIcon(@DrawableRes drawableRes: Int) { + setIcon(drawableRes) + icon?.setTintMode(PorterDuff.Mode.SRC_IN) + icon?.setTintList(context.getColorStateListFromAttribute(R.attr.wpColorOnSurfaceMedium)) + } +} diff --git a/WordPress/src/main/res/layout/notifications_settings_activity.xml b/WordPress/src/main/res/layout/notifications_settings_activity.xml index 1f7978403be7..d041fdb99886 100644 --- a/WordPress/src/main/res/layout/notifications_settings_activity.xml +++ b/WordPress/src/main/res/layout/notifications_settings_activity.xml @@ -10,6 +10,7 @@ @@ -28,6 +29,7 @@ app:theme="@style/WordPress.ActionBar"> - - + + + + diff --git a/WordPress/src/main/res/xml/notification_settings_my_sites.xml b/WordPress/src/main/res/xml/notification_settings_my_sites.xml new file mode 100644 index 000000000000..7bf2aa65d884 --- /dev/null +++ b/WordPress/src/main/res/xml/notification_settings_my_sites.xml @@ -0,0 +1,4 @@ + + + + diff --git a/WordPress/src/main/res/xml/notification_settings_types.xml b/WordPress/src/main/res/xml/notification_settings_types.xml new file mode 100644 index 000000000000..7bf2aa65d884 --- /dev/null +++ b/WordPress/src/main/res/xml/notification_settings_types.xml @@ -0,0 +1,4 @@ + + + + diff --git a/WordPress/src/main/res/xml/notifications_settings.xml b/WordPress/src/main/res/xml/notifications_settings.xml index caa42ec5917e..22b530c439d3 100644 --- a/WordPress/src/main/res/xml/notifications_settings.xml +++ b/WordPress/src/main/res/xml/notifications_settings.xml @@ -19,7 +19,7 @@ android:key="@string/pref_notification_other_blogs" android:title="@string/notification_settings_item_other_comments_other_blogs" /> - @@ -30,19 +30,19 @@ android:key="@string/pref_notification_sights_sounds" android:title="@string/notification_settings_category_sights_and_sounds"> - - -