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_GOT_IT_CLICKED; 22 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_MORE_BUTTON_CLICKED; 23 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_OPT_IN_CLICKED; 24 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_OPT_OUT_CLICKED; 25 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_SCROLLED; 26 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_SCROLLED_TO_BOTTOM; 27 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_SETTINGS_BUTTON_CLICKED; 28 import static com.android.adservices.ui.notifications.ConsentNotificationConfirmationFragment.IS_CONSENT_GIVEN_ARGUMENT_KEY; 29 import static com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity.FROM_NOTIFICATION_KEY; 30 31 import android.content.Context; 32 import android.content.Intent; 33 import android.os.Build; 34 import android.os.Bundle; 35 import android.text.method.LinkMovementMethod; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.View.OnScrollChangeListener; 39 import android.view.ViewGroup; 40 import android.widget.Button; 41 import android.widget.ScrollView; 42 import android.widget.TextView; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.annotation.RequiresApi; 47 import androidx.fragment.app.Fragment; 48 49 import com.android.adservices.api.R; 50 import com.android.adservices.service.FlagsFactory; 51 import com.android.adservices.service.consent.AdServicesApiType; 52 import com.android.adservices.service.consent.ConsentManager; 53 import com.android.adservices.ui.UxUtil; 54 import com.android.adservices.ui.notifications.ConsentNotificationActivity; 55 import com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity; 56 57 /** Fragment for the topics view of the AdServices Settings App. */ 58 // TODO(b/269798827): Enable for R. 59 @RequiresApi(Build.VERSION_CODES.S) 60 public class ConsentNotificationGaFragment extends Fragment { 61 public static final String IS_TOPICS_INFO_VIEW_EXPANDED_KEY = "is_topics_info_view_expanded"; 62 private boolean mIsEUDevice; 63 private boolean mIsInfoViewExpanded = false; 64 private @Nullable ScrollToBottomController mScrollToBottomController; 65 66 @Override onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)67 public View onCreateView( 68 @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 69 return setupActivity(inflater, container); 70 } 71 72 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)73 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 74 setupListeners(savedInstanceState); 75 76 ConsentNotificationActivity.handleAction(LANDING_PAGE_DISPLAYED, getContext()); 77 } 78 79 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)80 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 81 super.onSaveInstanceState(savedInstanceState); 82 83 ConsentNotificationActivity.handleAction(LANDING_PAGE_DISMISSED, getContext()); 84 if (mScrollToBottomController != null) { 85 mScrollToBottomController.saveInstanceState(savedInstanceState); 86 } 87 savedInstanceState.putBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, mIsInfoViewExpanded); 88 } 89 setupActivity(LayoutInflater inflater, ViewGroup container)90 private View setupActivity(LayoutInflater inflater, ViewGroup container) { 91 mIsEUDevice = UxUtil.isEeaDevice(requireActivity()); 92 View rootView; 93 if (mIsEUDevice) { 94 rootView = 95 inflater.inflate( 96 R.layout.consent_notification_topics_fragment_eu, container, false); 97 } else { 98 rootView = 99 inflater.inflate(R.layout.consent_notification_ga_fragment, container, false); 100 } 101 return rootView; 102 } 103 setupListeners(Bundle savedInstanceState)104 private void setupListeners(Bundle savedInstanceState) { 105 // set up how it works expander 106 TextView howItWorksExpander = requireActivity().findViewById(R.id.how_it_works_expander); 107 if (savedInstanceState != null) { 108 setInfoViewState( 109 savedInstanceState.getBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, false)); 110 } 111 howItWorksExpander.setOnClickListener( 112 view -> { 113 setInfoViewState(!mIsInfoViewExpanded); 114 ConsentNotificationActivity.handleAction( 115 LANDING_PAGE_ADDITIONAL_INFO_CLICKED, getContext()); 116 }); 117 118 // set up privacy policy link movement 119 ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy)) 120 .setMovementMethod(LinkMovementMethod.getInstance()); 121 122 // set up left control button and right control button 123 Button leftControlButton = requireActivity().findViewById(R.id.leftControlButton); 124 leftControlButton.setOnClickListener( 125 view -> { 126 if (mIsEUDevice) { 127 ConsentNotificationActivity.handleAction( 128 LANDING_PAGE_OPT_OUT_CLICKED, getContext()); 129 130 // opt-out confirmation activity 131 ConsentManager.getInstance() 132 .disable(requireContext(), AdServicesApiType.TOPICS); 133 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) { 134 ConsentManager.getInstance() 135 .recordUserManualInteractionWithConsent( 136 ConsentManager.MANUAL_INTERACTIONS_RECORDED); 137 } 138 Bundle args = new Bundle(); 139 args.putBoolean(IS_CONSENT_GIVEN_ARGUMENT_KEY, false); 140 startConfirmationFragment(args); 141 } else { 142 ConsentNotificationActivity.handleAction( 143 LANDING_PAGE_SETTINGS_BUTTON_CLICKED, getContext()); 144 145 // go to settings activity 146 Intent intent = 147 new Intent(requireActivity(), AdServicesSettingsMainActivity.class); 148 intent.putExtra(FROM_NOTIFICATION_KEY, true); 149 startActivity(intent); 150 requireActivity().finish(); 151 } 152 }); 153 154 Button rightControlButton = requireActivity().findViewById(R.id.rightControlButton); 155 ScrollView scrollView = requireView().findViewById(R.id.notification_fragment_scrollview); 156 157 mScrollToBottomController = 158 new ScrollToBottomController( 159 scrollView, leftControlButton, rightControlButton, savedInstanceState); 160 mScrollToBottomController.bind(); 161 // check whether it can scroll vertically and update buttons after layout can be measured 162 scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom()); 163 } 164 setInfoViewState(boolean expanded)165 private void setInfoViewState(boolean expanded) { 166 View text = requireActivity().findViewById(R.id.how_it_works_expanded_text); 167 TextView expander = requireActivity().findViewById(R.id.how_it_works_expander); 168 if (expanded) { 169 mIsInfoViewExpanded = true; 170 text.setVisibility(View.VISIBLE); 171 expander.setCompoundDrawablesRelativeWithIntrinsicBounds( 172 0, 0, R.drawable.ic_minimize, 0); 173 } else { 174 mIsInfoViewExpanded = false; 175 text.setVisibility(View.GONE); 176 expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0); 177 } 178 } 179 startConfirmationFragment(Bundle args)180 private void startConfirmationFragment(Bundle args) { 181 requireActivity() 182 .getSupportFragmentManager() 183 .beginTransaction() 184 .replace( 185 R.id.fragment_container_view, 186 ConsentNotificationConfirmationGaFragment.class, 187 args) 188 .setReorderingAllowed(true) 189 .addToBackStack(null) 190 .commit(); 191 } 192 193 /** 194 * Allows the positive, acceptance button to scroll the view. 195 * 196 * <p>When the positive button first appears it will show the text "More". When the user taps 197 * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the 198 * button text will be replaced with the acceptance text. Once the text has changed, the button 199 * will trigger the positive action no matter where the view is scrolled. 200 */ 201 private class ScrollToBottomController implements OnScrollChangeListener { 202 private static final String STATE_HAS_SCROLLED_TO_BOTTOM = "has_scrolled_to_bottom"; 203 private static final int SCROLL_DIRECTION_DOWN = 1; 204 private static final double SCROLL_MULTIPLIER = 0.8; 205 206 private final ScrollView mScrollContainer; 207 private final Button mLeftControlButton; 208 private final Button mRightControlButton; 209 210 private boolean mHasScrolledToBottom; 211 ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)212 ScrollToBottomController( 213 ScrollView scrollContainer, 214 Button leftControlButton, 215 Button rightControlButton, 216 @Nullable Bundle savedInstanceState) { 217 this.mScrollContainer = scrollContainer; 218 this.mLeftControlButton = leftControlButton; 219 this.mRightControlButton = rightControlButton; 220 mHasScrolledToBottom = 221 savedInstanceState != null 222 && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM) 223 && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM); 224 } 225 bind()226 public void bind() { 227 mScrollContainer.setOnScrollChangeListener(this); 228 mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked); 229 updateControlButtons(); 230 } 231 saveInstanceState(Bundle bundle)232 public void saveInstanceState(Bundle bundle) { 233 if (mHasScrolledToBottom) { 234 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM, true); 235 } 236 } 237 updateControlButtons()238 private void updateControlButtons() { 239 if (mHasScrolledToBottom) { 240 mLeftControlButton.setVisibility(View.VISIBLE); 241 mRightControlButton.setText( 242 mIsEUDevice 243 ? R.string.notificationUI_right_control_button_ga_text_eu 244 : R.string.notificationUI_right_control_button_text); 245 } else { 246 mLeftControlButton.setVisibility(View.INVISIBLE); 247 mRightControlButton.setText(R.string.notificationUI_more_button_text); 248 } 249 } 250 onMoreOrAcceptClicked(View view)251 private void onMoreOrAcceptClicked(View view) { 252 Context context = getContext(); 253 if (context == null) { 254 return; 255 } 256 257 if (mHasScrolledToBottom) { 258 if (mIsEUDevice) { 259 ConsentNotificationActivity.handleAction( 260 LANDING_PAGE_OPT_IN_CLICKED, getContext()); 261 262 // opt-in confirmation activity 263 ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.TOPICS); 264 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) { 265 ConsentManager.getInstance() 266 .recordUserManualInteractionWithConsent( 267 ConsentManager.MANUAL_INTERACTIONS_RECORDED); 268 } 269 Bundle args = new Bundle(); 270 args.putBoolean(IS_CONSENT_GIVEN_ARGUMENT_KEY, true); 271 startConfirmationFragment(args); 272 } else { 273 ConsentNotificationActivity.handleAction( 274 LANDING_PAGE_GOT_IT_CLICKED, getContext()); 275 276 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) { 277 ConsentManager.getInstance() 278 .recordUserManualInteractionWithConsent( 279 ConsentManager.MANUAL_INTERACTIONS_RECORDED); 280 } 281 // acknowledge and dismiss 282 requireActivity().finishAndRemoveTask(); 283 } 284 } else { 285 ConsentNotificationActivity.handleAction( 286 LANDING_PAGE_MORE_BUTTON_CLICKED, getContext()); 287 288 mScrollContainer.smoothScrollTo( 289 0, 290 mScrollContainer.getScrollY() 291 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER)); 292 } 293 } 294 295 @Override onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)296 public void onScrollChange( 297 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 298 ConsentNotificationActivity.handleAction(LANDING_PAGE_SCROLLED, getContext()); 299 updateButtonsIfHasScrolledToBottom(); 300 } 301 updateButtonsIfHasScrolledToBottom()302 void updateButtonsIfHasScrolledToBottom() { 303 if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) { 304 ConsentNotificationActivity.handleAction( 305 LANDING_PAGE_SCROLLED_TO_BOTTOM, getContext()); 306 mHasScrolledToBottom = true; 307 updateControlButtons(); 308 } 309 } 310 } 311 } 312