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