1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.adservices.ui.ganotifications; 17 18 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_ADDITIONAL_INFO_CLICKED; 19 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_DISMISSED; 20 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_DISPLAYED; 21 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_MORE_BUTTON_CLICKED; 22 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_OPT_IN_CLICKED; 23 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_OPT_OUT_CLICKED; 24 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_SCROLLED; 25 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_SCROLLED_TO_BOTTOM; 26 27 import android.content.Context; 28 import android.os.Build; 29 import android.os.Bundle; 30 import android.text.method.LinkMovementMethod; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.View.OnScrollChangeListener; 34 import android.view.ViewGroup; 35 import android.widget.Button; 36 import android.widget.ScrollView; 37 import android.widget.TextView; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.annotation.RequiresApi; 42 import androidx.fragment.app.Fragment; 43 44 import com.android.adservices.api.R; 45 import com.android.adservices.service.FlagsFactory; 46 import com.android.adservices.service.consent.AdServicesApiType; 47 import com.android.adservices.service.consent.ConsentManager; 48 import com.android.adservices.ui.UxUtil; 49 import com.android.adservices.ui.notifications.ConsentNotificationActivity; 50 51 /** Fragment for the topics view of the AdServices Settings App. */ 52 // TODO(b/269798827): Enable for R. 53 @RequiresApi(Build.VERSION_CODES.S) 54 public class ConsentNotificationGaV2Screen2Fragment extends Fragment { 55 public static final String IS_TOPICS_INFO_VIEW_EXPANDED_KEY = "is_topics_info_view_expanded"; 56 private boolean mIsEUDevice; 57 private boolean mIsInfoViewExpanded = false; 58 private @Nullable ScrollToBottomController mScrollToBottomController; 59 60 @Override onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)61 public View onCreateView( 62 @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 63 return setupActivity(inflater, container); 64 } 65 66 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)67 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 68 setupListeners(savedInstanceState); 69 70 ConsentNotificationActivity.handleAction(LANDING_PAGE_DISPLAYED, getContext()); 71 } 72 73 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)74 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 75 super.onSaveInstanceState(savedInstanceState); 76 77 ConsentNotificationActivity.handleAction(LANDING_PAGE_DISMISSED, getContext()); 78 if (mScrollToBottomController != null) { 79 mScrollToBottomController.saveInstanceState(savedInstanceState); 80 } 81 savedInstanceState.putBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, mIsInfoViewExpanded); 82 } 83 setupActivity(LayoutInflater inflater, ViewGroup container)84 private View setupActivity(LayoutInflater inflater, ViewGroup container) { 85 mIsEUDevice = UxUtil.isEeaDevice(requireActivity()); 86 return inflater.inflate(R.layout.consent_notification_screen_2_ga_v2_eu, container, false); 87 } 88 setupListeners(Bundle savedInstanceState)89 private void setupListeners(Bundle savedInstanceState) { 90 TextView howItWorksExpander = 91 requireActivity().findViewById(R.id.how_it_works_expander_screen_2); 92 if (savedInstanceState != null) { 93 setInfoViewState( 94 savedInstanceState.getBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, false)); 95 } 96 howItWorksExpander.setOnClickListener( 97 view -> { 98 setInfoViewState(!mIsInfoViewExpanded); 99 ConsentNotificationActivity.handleAction( 100 LANDING_PAGE_ADDITIONAL_INFO_CLICKED, getContext()); 101 }); 102 103 ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy)) 104 .setMovementMethod(LinkMovementMethod.getInstance()); 105 106 Button leftControlButton = requireActivity().findViewById(R.id.leftControlButton_screen_2); 107 leftControlButton.setOnClickListener( 108 view -> { 109 ConsentNotificationActivity.handleAction( 110 LANDING_PAGE_OPT_OUT_CLICKED, getContext()); 111 112 // opt-out confirmation activity 113 ConsentManager.getInstance() 114 .disable(requireContext(), AdServicesApiType.TOPICS); 115 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) { 116 ConsentManager.getInstance() 117 .recordUserManualInteractionWithConsent( 118 ConsentManager.MANUAL_INTERACTIONS_RECORDED); 119 } 120 requireActivity().finishAndRemoveTask(); 121 }); 122 123 Button rightControlButton = 124 requireActivity().findViewById(R.id.rightControlButton_screen_2); 125 ScrollView scrollView = 126 requireView().findViewById(R.id.notification_fragment_scrollview_screen_2); 127 128 mScrollToBottomController = 129 new ScrollToBottomController( 130 scrollView, leftControlButton, rightControlButton, savedInstanceState); 131 mScrollToBottomController.bind(); 132 // check whether it can scroll vertically and update buttons after layout can be measured 133 scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom()); 134 } 135 setInfoViewState(boolean expanded)136 private void setInfoViewState(boolean expanded) { 137 View text = requireActivity().findViewById(R.id.how_it_works_expanded_text_screen_2); 138 TextView expander = requireActivity().findViewById(R.id.how_it_works_expander_screen_2); 139 if (expanded) { 140 mIsInfoViewExpanded = true; 141 text.setVisibility(View.VISIBLE); 142 expander.setCompoundDrawablesRelativeWithIntrinsicBounds( 143 0, 0, R.drawable.ic_minimize, 0); 144 } else { 145 mIsInfoViewExpanded = false; 146 text.setVisibility(View.GONE); 147 expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0); 148 } 149 } 150 151 /** 152 * Allows the positive, acceptance button to scroll the view. 153 * 154 * <p>When the positive button first appears it will show the text "More". When the user taps 155 * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the 156 * button text will be replaced with the acceptance text. Once the text has changed, the button 157 * will trigger the positive action no matter where the view is scrolled. 158 */ 159 private class ScrollToBottomController implements OnScrollChangeListener { 160 private static final String STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE = 161 "has_scrolled_to_bottom_2"; 162 private static final int SCROLL_DIRECTION_DOWN = 1; 163 private static final double SCROLL_MULTIPLIER = 0.8; 164 165 private final ScrollView mScrollContainer; 166 private final Button mLeftControlButton; 167 private final Button mRightControlButton; 168 169 private boolean mHasScrolledToBottom; 170 ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)171 ScrollToBottomController( 172 ScrollView scrollContainer, 173 Button leftControlButton, 174 Button rightControlButton, 175 @Nullable Bundle savedInstanceState) { 176 this.mScrollContainer = scrollContainer; 177 this.mLeftControlButton = leftControlButton; 178 this.mRightControlButton = rightControlButton; 179 mHasScrolledToBottom = 180 savedInstanceState != null 181 && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE) 182 && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE); 183 } 184 bind()185 public void bind() { 186 mScrollContainer.setOnScrollChangeListener(this); 187 mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked); 188 updateControlButtons(); 189 } 190 saveInstanceState(Bundle bundle)191 public void saveInstanceState(Bundle bundle) { 192 if (mHasScrolledToBottom) { 193 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE, true); 194 } 195 } 196 updateControlButtons()197 private void updateControlButtons() { 198 if (mHasScrolledToBottom) { 199 mLeftControlButton.setVisibility(View.VISIBLE); 200 mRightControlButton.setText( 201 mIsEUDevice 202 ? R.string.notificationUI_right_control_button_ga_text_eu_v2 203 : R.string.notificationUI_right_control_button_text); 204 } else { 205 mLeftControlButton.setVisibility(View.INVISIBLE); 206 mRightControlButton.setText(R.string.notificationUI_more_button_text); 207 } 208 } 209 onMoreOrAcceptClicked(View view)210 private void onMoreOrAcceptClicked(View view) { 211 Context context = getContext(); 212 if (context == null) { 213 return; 214 } 215 216 if (mHasScrolledToBottom) { 217 ConsentNotificationActivity.handleAction( 218 LANDING_PAGE_OPT_IN_CLICKED, getContext()); 219 220 // opt-in to topics 221 ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.TOPICS); 222 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) { 223 ConsentManager.getInstance() 224 .recordUserManualInteractionWithConsent( 225 ConsentManager.MANUAL_INTERACTIONS_RECORDED); 226 } 227 requireActivity().finishAndRemoveTask(); 228 } else { 229 ConsentNotificationActivity.handleAction( 230 LANDING_PAGE_MORE_BUTTON_CLICKED, getContext()); 231 232 mScrollContainer.smoothScrollTo( 233 0, 234 mScrollContainer.getScrollY() 235 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER)); 236 } 237 } 238 239 @Override onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)240 public void onScrollChange( 241 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 242 ConsentNotificationActivity.handleAction(LANDING_PAGE_SCROLLED, getContext()); 243 updateButtonsIfHasScrolledToBottom(); 244 } 245 updateButtonsIfHasScrolledToBottom()246 void updateButtonsIfHasScrolledToBottom() { 247 if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) { 248 ConsentNotificationActivity.handleAction( 249 LANDING_PAGE_SCROLLED_TO_BOTTOM, getContext()); 250 mHasScrolledToBottom = true; 251 updateControlButtons(); 252 } 253 } 254 } 255 } 256