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