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 @RequiresApi(Build.VERSION_CODES.S) 53 public class ConsentNotificationGaV2Screen2Fragment extends Fragment { 54 public static final String IS_TOPICS_INFO_VIEW_EXPANDED_KEY = "is_topics_info_view_expanded"; 55 private boolean mIsEUDevice; 56 private boolean mIsInfoViewExpanded = false; 57 private @Nullable ScrollToBottomController mScrollToBottomController; 58 59 @Override onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)60 public View onCreateView( 61 @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 62 return setupActivity(inflater, container); 63 } 64 65 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)66 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 67 setupListeners(savedInstanceState); 68 69 ConsentNotificationActivity.handleAction(LANDING_PAGE_DISPLAYED); 70 } 71 72 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)73 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 74 super.onSaveInstanceState(savedInstanceState); 75 76 ConsentNotificationActivity.handleAction(LANDING_PAGE_DISMISSED); 77 if (mScrollToBottomController != null) { 78 mScrollToBottomController.saveInstanceState(savedInstanceState); 79 } 80 savedInstanceState.putBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, mIsInfoViewExpanded); 81 } 82 setupActivity(LayoutInflater inflater, ViewGroup container)83 private View setupActivity(LayoutInflater inflater, ViewGroup container) { 84 mIsEUDevice = UxUtil.isEeaDevice(requireActivity()); 85 return inflater.inflate(R.layout.consent_notification_screen_2_ga_v2_eu, container, false); 86 } 87 setupListeners(Bundle savedInstanceState)88 private void setupListeners(Bundle savedInstanceState) { 89 TextView howItWorksExpander = 90 requireActivity().findViewById(R.id.how_it_works_expander_screen_2); 91 if (savedInstanceState != null) { 92 setInfoViewState( 93 savedInstanceState.getBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, false)); 94 } 95 howItWorksExpander.setOnClickListener( 96 view -> { 97 setInfoViewState(!mIsInfoViewExpanded); 98 ConsentNotificationActivity.handleAction(LANDING_PAGE_ADDITIONAL_INFO_CLICKED); 99 }); 100 101 ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy)) 102 .setMovementMethod(LinkMovementMethod.getInstance()); 103 104 Button leftControlButton = requireActivity().findViewById(R.id.leftControlButton_screen_2); 105 leftControlButton.setOnClickListener( 106 view -> { 107 ConsentNotificationActivity.handleAction(LANDING_PAGE_OPT_OUT_CLICKED); 108 109 // opt-out confirmation activity 110 ConsentManager.getInstance() 111 .disable(requireContext(), AdServicesApiType.TOPICS); 112 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) { 113 ConsentManager.getInstance() 114 .recordUserManualInteractionWithConsent( 115 ConsentManager.MANUAL_INTERACTIONS_RECORDED); 116 } 117 requireActivity().finishAndRemoveTask(); 118 }); 119 120 Button rightControlButton = 121 requireActivity().findViewById(R.id.rightControlButton_screen_2); 122 ScrollView scrollView = 123 requireView().findViewById(R.id.notification_fragment_scrollview_screen_2); 124 125 mScrollToBottomController = 126 new ScrollToBottomController( 127 scrollView, leftControlButton, rightControlButton, savedInstanceState); 128 mScrollToBottomController.bind(); 129 // check whether it can scroll vertically and update buttons after layout can be measured 130 scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom()); 131 } 132 setInfoViewState(boolean expanded)133 private void setInfoViewState(boolean expanded) { 134 View text = requireActivity().findViewById(R.id.how_it_works_expanded_text_screen_2); 135 TextView expander = requireActivity().findViewById(R.id.how_it_works_expander_screen_2); 136 if (expanded) { 137 mIsInfoViewExpanded = true; 138 text.setVisibility(View.VISIBLE); 139 expander.setCompoundDrawablesRelativeWithIntrinsicBounds( 140 0, 0, R.drawable.ic_minimize, 0); 141 } else { 142 mIsInfoViewExpanded = false; 143 text.setVisibility(View.GONE); 144 expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0); 145 } 146 } 147 148 /** 149 * Allows the positive, acceptance button to scroll the view. 150 * 151 * <p>When the positive button first appears it will show the text "More". When the user taps 152 * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the 153 * button text will be replaced with the acceptance text. Once the text has changed, the button 154 * will trigger the positive action no matter where the view is scrolled. 155 */ 156 private class ScrollToBottomController implements OnScrollChangeListener { 157 private static final String STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE = 158 "has_scrolled_to_bottom_2"; 159 private static final int SCROLL_DIRECTION_DOWN = 1; 160 private static final double SCROLL_MULTIPLIER = 0.8; 161 162 private final ScrollView mScrollContainer; 163 private final Button mLeftControlButton; 164 private final Button mRightControlButton; 165 166 private boolean mHasScrolledToBottom; 167 ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)168 ScrollToBottomController( 169 ScrollView scrollContainer, 170 Button leftControlButton, 171 Button rightControlButton, 172 @Nullable Bundle savedInstanceState) { 173 this.mScrollContainer = scrollContainer; 174 this.mLeftControlButton = leftControlButton; 175 this.mRightControlButton = rightControlButton; 176 mHasScrolledToBottom = 177 savedInstanceState != null 178 && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE) 179 && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE); 180 } 181 bind()182 public void bind() { 183 mScrollContainer.setOnScrollChangeListener(this); 184 mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked); 185 updateControlButtons(); 186 } 187 saveInstanceState(Bundle bundle)188 public void saveInstanceState(Bundle bundle) { 189 if (mHasScrolledToBottom) { 190 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE, true); 191 } 192 } 193 updateControlButtons()194 private void updateControlButtons() { 195 if (mHasScrolledToBottom) { 196 mLeftControlButton.setVisibility(View.VISIBLE); 197 mRightControlButton.setText( 198 mIsEUDevice 199 ? R.string.notificationUI_right_control_button_ga_text_eu_v2 200 : R.string.notificationUI_right_control_button_text); 201 } else { 202 mLeftControlButton.setVisibility(View.INVISIBLE); 203 mRightControlButton.setText(R.string.notificationUI_more_button_text); 204 } 205 } 206 onMoreOrAcceptClicked(View view)207 private void onMoreOrAcceptClicked(View view) { 208 Context context = getContext(); 209 if (context == null) { 210 return; 211 } 212 213 if (mHasScrolledToBottom) { 214 ConsentNotificationActivity.handleAction(LANDING_PAGE_OPT_IN_CLICKED); 215 216 // opt-in to topics 217 ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.TOPICS); 218 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) { 219 ConsentManager.getInstance() 220 .recordUserManualInteractionWithConsent( 221 ConsentManager.MANUAL_INTERACTIONS_RECORDED); 222 } 223 requireActivity().finishAndRemoveTask(); 224 } else { 225 ConsentNotificationActivity.handleAction(LANDING_PAGE_MORE_BUTTON_CLICKED); 226 227 mScrollContainer.smoothScrollTo( 228 0, 229 mScrollContainer.getScrollY() 230 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER)); 231 } 232 } 233 234 @Override onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)235 public void onScrollChange( 236 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 237 ConsentNotificationActivity.handleAction(LANDING_PAGE_SCROLLED); 238 updateButtonsIfHasScrolledToBottom(); 239 } 240 updateButtonsIfHasScrolledToBottom()241 void updateButtonsIfHasScrolledToBottom() { 242 if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) { 243 ConsentNotificationActivity.handleAction(LANDING_PAGE_SCROLLED_TO_BOTTOM); 244 mHasScrolledToBottom = true; 245 updateControlButtons(); 246 } 247 } 248 } 249 } 250