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