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.service.FlagsConstants.KEY_EEA_PAS_UX_ENABLED; 19 import static com.android.adservices.service.consent.ConsentManager.MANUAL_INTERACTIONS_RECORDED; 20 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_DISMISSED; 21 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_DISPLAYED; 22 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_MORE_INFO_CLICKED; 23 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED; 24 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_ADDITIONAL_INFO_2_CLICKED; 25 import static com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity.FROM_NOTIFICATION_KEY; 26 27 import android.content.Intent; 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.ViewGroup; 34 import android.widget.Button; 35 import android.widget.ScrollView; 36 import android.widget.TextView; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.RequiresApi; 41 import androidx.fragment.app.Fragment; 42 43 import com.android.adservices.api.R; 44 import com.android.adservices.service.consent.AdServicesApiType; 45 import com.android.adservices.service.consent.ConsentManager; 46 import com.android.adservices.service.ui.data.UxStatesManager; 47 import com.android.adservices.ui.notifications.ConsentNotificationActivity; 48 import com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity; 49 50 /** 51 * Fragment for the confirmation view after accepting or rejecting to be part of Privacy Sandbox 52 * Beta. 53 */ 54 @RequiresApi(Build.VERSION_CODES.S) 55 public class ConsentNotificationPasFragment extends Fragment { 56 public static final String IS_RENOTIFY_KEY = "IS_RENOTIFY_KEY"; 57 58 /** This includes EEA devices and ROW AdID disabled devices */ 59 public static final String IS_STRICT_CONSENT_BEHAVIOR = "IS_STRICT_CONSENT_BEHAVIOR"; 60 61 public static final String INFO_VIEW_EXPANDED_1 = "info_view_expanded_1"; 62 public static final String INFO_VIEW_EXPANDED_2 = "info_view_expanded_2"; 63 private boolean mIsInfoViewExpanded1 = false; 64 private boolean mIsInfoViewExpanded2 = false; 65 private boolean mIsStrictConsentBehavior; 66 private boolean mIsRenotify; 67 private boolean mIsFirstTimeRow; 68 private @Nullable ScrollToBottomController mScrollToBottomController; 69 70 @Override onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)71 public View onCreateView( 72 @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 73 View inflatedView; 74 mIsStrictConsentBehavior = 75 requireActivity().getIntent().getBooleanExtra(IS_STRICT_CONSENT_BEHAVIOR, false); 76 mIsRenotify = requireActivity().getIntent().getBooleanExtra(IS_RENOTIFY_KEY, false); 77 mIsFirstTimeRow = false; 78 if (mIsRenotify) { 79 // renotify version 80 inflatedView = 81 inflater.inflate(R.layout.consent_notification_screen_1_pas, container, false); 82 TextView title = inflatedView.findViewById(R.id.notification_title); 83 title.setText(R.string.notificationUI_pas_renotify_header_title); 84 } else if (mIsStrictConsentBehavior) { 85 // first-time version 86 inflatedView = 87 inflater.inflate(R.layout.consent_notification_screen_1_pas, container, false); 88 } else { 89 // combined version 90 mIsFirstTimeRow = true; 91 inflatedView = 92 inflater.inflate( 93 R.layout.consent_notification_pas_first_time_row, container, false); 94 } 95 return inflatedView; 96 } 97 98 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)99 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 100 setupListeners(savedInstanceState); 101 ConsentManager consentManager = ConsentManager.getInstance(); 102 boolean isNotRenotifyNoManualInteraction = 103 !mIsRenotify 104 && consentManager.getUserManualInteractionWithConsent() 105 != MANUAL_INTERACTIONS_RECORDED; 106 boolean isAdultFromRvcMsmtEnabled = 107 consentManager.isOtaAdultUserFromRvc() 108 && consentManager.getConsent(AdServicesApiType.MEASUREMENTS).isGiven(); 109 if (UxStatesManager.getInstance().getFlag(KEY_EEA_PAS_UX_ENABLED)) { 110 consentManager.recordPasNotificationOpened(true); 111 if (mIsStrictConsentBehavior 112 && (isNotRenotifyNoManualInteraction || isAdultFromRvcMsmtEnabled)) { 113 consentManager.enable(requireContext(), AdServicesApiType.FLEDGE); 114 consentManager.enable(requireContext(), AdServicesApiType.MEASUREMENTS); 115 } 116 } 117 ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISPLAYED, getContext()); 118 } 119 120 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)121 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 122 super.onSaveInstanceState(savedInstanceState); 123 if (mScrollToBottomController != null) { 124 mScrollToBottomController.saveInstanceState(savedInstanceState); 125 } 126 ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISMISSED, getContext()); 127 } 128 setupListeners(Bundle savedInstanceState)129 private void setupListeners(Bundle savedInstanceState) { 130 TextView howItWorksExpander = requireActivity().findViewById(R.id.how_it_works_expander); 131 if (savedInstanceState != null) { 132 setInfoViewState1(savedInstanceState.getBoolean(INFO_VIEW_EXPANDED_1, false)); 133 } 134 howItWorksExpander.setOnClickListener( 135 view -> { 136 ConsentNotificationActivity.handleAction( 137 CONFIRMATION_PAGE_OPT_OUT_MORE_INFO_CLICKED, getContext()); 138 139 setInfoViewState1(!mIsInfoViewExpanded1); 140 }); 141 ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy1)) 142 .setMovementMethod(LinkMovementMethod.getInstance()); 143 144 if (!mIsFirstTimeRow) { 145 TextView howItWorksExpander2 = 146 requireActivity().findViewById(R.id.how_it_works_expander2); 147 if (savedInstanceState != null) { 148 setInfoViewState2(savedInstanceState.getBoolean(INFO_VIEW_EXPANDED_2, false)); 149 } 150 howItWorksExpander2.setOnClickListener( 151 view -> { 152 ConsentNotificationActivity.handleAction( 153 LANDING_PAGE_ADDITIONAL_INFO_2_CLICKED, getContext()); 154 setInfoViewState2(!mIsInfoViewExpanded2); 155 }); 156 ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy2)) 157 .setMovementMethod(LinkMovementMethod.getInstance()); 158 } 159 160 Button leftControlButton = requireActivity().findViewById(R.id.leftControlButton); 161 leftControlButton.setOnClickListener( 162 view -> { 163 ConsentNotificationActivity.handleAction( 164 CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED, getContext()); 165 166 // go to settings activity 167 Intent intent = 168 new Intent(requireActivity(), AdServicesSettingsMainActivity.class); 169 // users should be able to go back to notification if clicked manage settings 170 intent.putExtra(FROM_NOTIFICATION_KEY, true); 171 startActivity(intent); 172 requireActivity().finish(); 173 }); 174 175 Button rightControlButton = requireActivity().findViewById(R.id.rightControlButton); 176 ScrollView scrollView = requireView().findViewById(R.id.notification_fragment_scrollview); 177 mScrollToBottomController = 178 new ScrollToBottomController( 179 scrollView, leftControlButton, rightControlButton, savedInstanceState); 180 mScrollToBottomController.bind(); 181 // check whether it can scroll vertically and update buttons after layout can be measured 182 scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom()); 183 } 184 setInfoViewState1(boolean expanded)185 private void setInfoViewState1(boolean expanded) { 186 View text = requireActivity().findViewById(R.id.how_it_works_expanded_text); 187 TextView expander = requireActivity().findViewById(R.id.how_it_works_expander); 188 mIsInfoViewExpanded1 = infoViewChanger(expanded, text, expander); 189 } 190 setInfoViewState2(boolean expanded)191 private void setInfoViewState2(boolean expanded) { 192 View text = requireActivity().findViewById(R.id.how_it_works_expanded_text2); 193 TextView expander = requireActivity().findViewById(R.id.how_it_works_expander2); 194 mIsInfoViewExpanded2 = infoViewChanger(expanded, text, expander); 195 } 196 197 // returns the state of the info view infoViewChanger(boolean expanded, View text, TextView expander)198 private boolean infoViewChanger(boolean expanded, View text, TextView expander) { 199 if (expanded) { 200 text.setVisibility(View.VISIBLE); 201 expander.setCompoundDrawablesRelativeWithIntrinsicBounds( 202 0, 0, R.drawable.ic_minimize, 0); 203 } else { 204 text.setVisibility(View.GONE); 205 expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0); 206 } 207 return expanded; 208 } 209 210 // helper method to start topics consent notification screen for EU users startTopicsConsentNotificationFragment()211 private void startTopicsConsentNotificationFragment() { 212 requireActivity() 213 .getSupportFragmentManager() 214 .beginTransaction() 215 .replace( 216 R.id.fragment_container_view, 217 ConsentNotificationGaV2Screen2Fragment.class, 218 null) 219 .setReorderingAllowed(true) 220 .addToBackStack(null) 221 .commit(); 222 } 223 224 /** 225 * Allows the positive, acceptance button to scroll the view. 226 * 227 * <p>When the positive button first appears it will show the text "More". When the user taps 228 * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the 229 * button text will be replaced with the acceptance text. Once the text has changed, the button 230 * will trigger the positive action no matter where the view is scrolled. 231 */ 232 private class ScrollToBottomController implements View.OnScrollChangeListener { 233 private static final String STATE_HAS_SCROLLED_TO_BOTTOM = "has_scrolled_to_bottom"; 234 private static final int SCROLL_DIRECTION_DOWN = 1; 235 private static final double SCROLL_MULTIPLIER = 0.8; 236 237 private final ScrollView mScrollContainer; 238 private final Button mLeftControlButton; 239 private final Button mRightControlButton; 240 241 private boolean mHasScrolledToBottom; 242 ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)243 ScrollToBottomController( 244 ScrollView scrollContainer, 245 Button leftControlButton, 246 Button rightControlButton, 247 @Nullable Bundle savedInstanceState) { 248 this.mScrollContainer = scrollContainer; 249 this.mLeftControlButton = leftControlButton; 250 this.mRightControlButton = rightControlButton; 251 mHasScrolledToBottom = 252 savedInstanceState != null 253 && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM) 254 && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM); 255 } 256 bind()257 public void bind() { 258 mScrollContainer.setOnScrollChangeListener(this); 259 mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked); 260 updateControlButtons(); 261 } 262 saveInstanceState(Bundle bundle)263 public void saveInstanceState(Bundle bundle) { 264 if (mHasScrolledToBottom) { 265 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM, true); 266 } 267 } 268 updateControlButtons()269 private void updateControlButtons() { 270 if (mHasScrolledToBottom) { 271 mLeftControlButton.setVisibility(View.VISIBLE); 272 mRightControlButton.setText( 273 R.string.notificationUI_confirmation_right_control_button_text); 274 } else { 275 mLeftControlButton.setVisibility(View.INVISIBLE); 276 mRightControlButton.setText(R.string.notificationUI_more_button_text); 277 } 278 } 279 onMoreOrAcceptClicked(View view)280 private void onMoreOrAcceptClicked(View view) { 281 if (mHasScrolledToBottom) { 282 // screen 2 283 if (!mIsRenotify && mIsStrictConsentBehavior) { 284 startTopicsConsentNotificationFragment(); 285 } else { 286 requireActivity().finishAndRemoveTask(); 287 } 288 } else { 289 mScrollContainer.smoothScrollTo( 290 0, 291 mScrollContainer.getScrollY() 292 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER)); 293 } 294 } 295 296 @Override onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)297 public void onScrollChange( 298 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 299 updateButtonsIfHasScrolledToBottom(); 300 } 301 updateButtonsIfHasScrolledToBottom()302 void updateButtonsIfHasScrolledToBottom() { 303 if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) { 304 mHasScrolledToBottom = true; 305 updateControlButtons(); 306 } 307 } 308 } 309 } 310