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.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DISMISS_NOTIFICATION_FAILURE; 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX; 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_IN_GOT_IT_BUTTON_CLICKED; 23 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_IN_MORE_INFO_CLICKED; 24 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_IN_SETTINGS_CLICKED; 25 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_GOT_IT_BUTTON_CLICKED; 26 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_MORE_INFO_CLICKED; 27 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED; 28 import static com.android.adservices.ui.notifications.ConsentNotificationTrigger.NOTIFICATION_ID; 29 import static com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity.FROM_NOTIFICATION_KEY; 30 31 import android.content.Intent; 32 import android.os.Build; 33 import android.os.Bundle; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.View.OnScrollChangeListener; 37 import android.view.ViewGroup; 38 import android.widget.Button; 39 import android.widget.ScrollView; 40 import android.widget.TextView; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 import androidx.annotation.RequiresApi; 45 import androidx.core.app.NotificationManagerCompat; 46 import androidx.fragment.app.Fragment; 47 48 import com.android.adservices.LogUtil; 49 import com.android.adservices.api.R; 50 import com.android.adservices.errorlogging.ErrorLogUtil; 51 import com.android.adservices.service.consent.AdServicesApiConsent; 52 import com.android.adservices.service.consent.AdServicesApiType; 53 import com.android.adservices.service.consent.ConsentManager; 54 import com.android.adservices.ui.notifications.ConsentNotificationActivity; 55 import com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity; 56 57 /** 58 * Fragment for the confirmation view after accepting or rejecting to be part of Privacy Sandbox 59 * Beta. 60 */ 61 // TODO(b/269798827): Enable for R. 62 // TODO(b/274955086): add logging for more button and scrolling 63 @RequiresApi(Build.VERSION_CODES.S) 64 public class ConsentNotificationConfirmationGaFragment extends Fragment { 65 public static final String IS_FlEDGE_MEASUREMENT_INFO_VIEW_EXPANDED_KEY = 66 "is_fledge_measurement_info_view_expanded"; 67 private boolean mIsInfoViewExpanded = false; 68 private @Nullable ScrollToBottomController mScrollToBottomController; 69 70 private boolean mTopicsOptIn; 71 72 @Override onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)73 public View onCreateView( 74 @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 75 AdServicesApiConsent topicsConsent = 76 ConsentManager.getInstance().getConsent(AdServicesApiType.TOPICS); 77 mTopicsOptIn = topicsConsent != null ? topicsConsent.isGiven() : false; 78 79 dismissNotificationIfNeeded(); 80 81 ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.FLEDGE); 82 ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.MEASUREMENTS); 83 return inflater.inflate( 84 R.layout.consent_notification_fledge_measurement_fragment_eu, container, false); 85 } 86 dismissNotificationIfNeeded()87 private void dismissNotificationIfNeeded() { 88 try { 89 NotificationManagerCompat notificationManager = 90 NotificationManagerCompat.from(requireContext()); 91 notificationManager.cancel(NOTIFICATION_ID); 92 } catch (Exception e) { 93 LogUtil.e(e.toString()); 94 ErrorLogUtil.e(e, 95 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DISMISS_NOTIFICATION_FAILURE, 96 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 97 } 98 } 99 100 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)101 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 102 setupListeners(savedInstanceState); 103 104 ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISPLAYED, getContext()); 105 } 106 107 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)108 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 109 super.onSaveInstanceState(savedInstanceState); 110 if (mScrollToBottomController != null) { 111 mScrollToBottomController.saveInstanceState(savedInstanceState); 112 } 113 ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISMISSED, getContext()); 114 } 115 setupListeners(Bundle savedInstanceState)116 private void setupListeners(Bundle savedInstanceState) { 117 TextView howItWorksExpander = 118 requireActivity().findViewById(R.id.how_it_works_fledge_measurement_expander); 119 if (savedInstanceState != null) { 120 setInfoViewState( 121 savedInstanceState.getBoolean( 122 IS_FlEDGE_MEASUREMENT_INFO_VIEW_EXPANDED_KEY, false)); 123 } 124 howItWorksExpander.setOnClickListener( 125 view -> { 126 if (mTopicsOptIn) { 127 ConsentNotificationActivity.handleAction( 128 CONFIRMATION_PAGE_OPT_IN_MORE_INFO_CLICKED, getContext()); 129 } else { 130 ConsentNotificationActivity.handleAction( 131 CONFIRMATION_PAGE_OPT_OUT_MORE_INFO_CLICKED, getContext()); 132 } 133 134 setInfoViewState(!mIsInfoViewExpanded); 135 }); 136 137 Button leftControlButton = 138 requireActivity().findViewById(R.id.leftControlButtonConfirmation); 139 leftControlButton.setOnClickListener( 140 view -> { 141 if (mTopicsOptIn) { 142 ConsentNotificationActivity.handleAction( 143 CONFIRMATION_PAGE_OPT_IN_SETTINGS_CLICKED, getContext()); 144 } else { 145 ConsentNotificationActivity.handleAction( 146 CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED, getContext()); 147 } 148 149 // go to settings activity 150 Intent intent = 151 new Intent(requireActivity(), AdServicesSettingsMainActivity.class); 152 intent.putExtra(FROM_NOTIFICATION_KEY, true); 153 startActivity(intent); 154 requireActivity().finish(); 155 }); 156 157 Button rightControlButton = 158 requireActivity().findViewById(R.id.rightControlButtonConfirmation); 159 rightControlButton.setOnClickListener( 160 view -> { 161 if (mTopicsOptIn) { 162 ConsentNotificationActivity.handleAction( 163 CONFIRMATION_PAGE_OPT_IN_GOT_IT_BUTTON_CLICKED, getContext()); 164 } else { 165 ConsentNotificationActivity.handleAction( 166 CONFIRMATION_PAGE_OPT_OUT_GOT_IT_BUTTON_CLICKED, getContext()); 167 } 168 169 // acknowledge and dismiss 170 requireActivity().finish(); 171 }); 172 173 ScrollView scrollView = 174 requireView().findViewById(R.id.consent_notification_fledge_measurement_view); 175 176 mScrollToBottomController = 177 new ScrollToBottomController( 178 scrollView, leftControlButton, rightControlButton, savedInstanceState); 179 mScrollToBottomController.bind(); 180 // check whether it can scroll vertically and update buttons after layout can be measured 181 scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom()); 182 } 183 setInfoViewState(boolean expanded)184 private void setInfoViewState(boolean expanded) { 185 View text = 186 requireActivity().findViewById(R.id.how_it_works_fledge_measurement_expanded_text); 187 TextView expander = 188 requireActivity().findViewById(R.id.how_it_works_fledge_measurement_expander); 189 if (expanded) { 190 mIsInfoViewExpanded = true; 191 text.setVisibility(View.VISIBLE); 192 expander.setCompoundDrawablesRelativeWithIntrinsicBounds( 193 0, 0, R.drawable.ic_minimize, 0); 194 } else { 195 mIsInfoViewExpanded = false; 196 text.setVisibility(View.GONE); 197 expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0); 198 } 199 } 200 201 /** 202 * Allows the positive, acceptance button to scroll the view. 203 * 204 * <p>When the positive button first appears it will show the text "More". When the user taps 205 * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the 206 * button text will be replaced with the acceptance text. Once the text has changed, the button 207 * will trigger the positive action no matter where the view is scrolled. 208 */ 209 private class ScrollToBottomController implements OnScrollChangeListener { 210 private static final String STATE_HAS_SCROLLED_TO_BOTTOM = "has_scrolled_to_bottom"; 211 private static final int SCROLL_DIRECTION_DOWN = 1; 212 private static final double SCROLL_MULTIPLIER = 0.8; 213 214 private final ScrollView mScrollContainer; 215 private final Button mLeftControlButton; 216 private final Button mRightControlButton; 217 218 private boolean mHasScrolledToBottom; 219 ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)220 ScrollToBottomController( 221 ScrollView scrollContainer, 222 Button leftControlButton, 223 Button rightControlButton, 224 @Nullable Bundle savedInstanceState) { 225 this.mScrollContainer = scrollContainer; 226 this.mLeftControlButton = leftControlButton; 227 this.mRightControlButton = rightControlButton; 228 mHasScrolledToBottom = 229 savedInstanceState != null 230 && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM) 231 && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM); 232 } 233 bind()234 public void bind() { 235 mScrollContainer.setOnScrollChangeListener(this); 236 mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked); 237 updateControlButtons(); 238 } 239 saveInstanceState(Bundle bundle)240 public void saveInstanceState(Bundle bundle) { 241 if (mHasScrolledToBottom) { 242 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM, true); 243 } 244 } 245 updateControlButtons()246 private void updateControlButtons() { 247 if (mHasScrolledToBottom) { 248 mLeftControlButton.setVisibility(View.VISIBLE); 249 mRightControlButton.setText( 250 R.string.notificationUI_confirmation_right_control_button_text); 251 } else { 252 mLeftControlButton.setVisibility(View.INVISIBLE); 253 mRightControlButton.setText(R.string.notificationUI_more_button_text); 254 } 255 } 256 onMoreOrAcceptClicked(View view)257 private void onMoreOrAcceptClicked(View view) { 258 if (mHasScrolledToBottom) { 259 // acknowledge and dismiss 260 requireActivity().finishAndRemoveTask(); 261 } else { 262 mScrollContainer.smoothScrollTo( 263 0, 264 mScrollContainer.getScrollY() 265 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER)); 266 } 267 } 268 269 @Override onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)270 public void onScrollChange( 271 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 272 updateButtonsIfHasScrolledToBottom(); 273 } 274 updateButtonsIfHasScrolledToBottom()275 void updateButtonsIfHasScrolledToBottom() { 276 if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) { 277 mHasScrolledToBottom = true; 278 updateControlButtons(); 279 } 280 } 281 } 282 } 283