• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.FlagsConstants.KEY_EEA_PAS_UX_ENABLED;
19 import static com.android.adservices.service.consent.ConsentManager.MANUAL_INTERACTIONS_RECORDED;
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_OUT_MORE_INFO_CLICKED;
23 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED;
24 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_ADDITIONAL_INFO_2_CLICKED;
25 import static com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity.FROM_NOTIFICATION_KEY;
26 
27 import android.content.Intent;
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.ViewGroup;
34 import android.widget.Button;
35 import android.widget.ScrollView;
36 import android.widget.TextView;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 import androidx.annotation.RequiresApi;
41 import androidx.fragment.app.Fragment;
42 
43 import com.android.adservices.LoggerFactory;
44 import com.android.adservices.api.R;
45 import com.android.adservices.service.consent.AdServicesApiType;
46 import com.android.adservices.service.consent.ConsentManager;
47 import com.android.adservices.service.ui.data.UxStatesManager;
48 import com.android.adservices.ui.notifications.ConsentNotificationActivity;
49 import com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity;
50 
51 /**
52  * Fragment for the confirmation view after accepting or rejecting to be part of Privacy Sandbox
53  * Beta.
54  */
55 @RequiresApi(Build.VERSION_CODES.S)
56 public class ConsentNotificationPasFragment extends Fragment {
57     public static final String IS_RENOTIFY_KEY = "IS_RENOTIFY_KEY";
58 
59     /** This includes EEA devices and ROW AdID disabled devices */
60     public static final String IS_STRICT_CONSENT_BEHAVIOR = "IS_STRICT_CONSENT_BEHAVIOR";
61 
62     public static final String INFO_VIEW_EXPANDED_1 = "info_view_expanded_1";
63     public static final String INFO_VIEW_EXPANDED_2 = "info_view_expanded_2";
64     private boolean mIsInfoViewExpanded1 = false;
65     private boolean mIsInfoViewExpanded2 = false;
66     private boolean mIsStrictConsentBehavior;
67     private boolean mIsRenotify;
68     private boolean mIsFirstTimeRow;
69     private @Nullable ScrollToBottomController mScrollToBottomController;
70 
71     @Override
onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)72     public View onCreateView(
73             @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
74         View inflatedView;
75         mIsStrictConsentBehavior =
76                 requireActivity().getIntent().getBooleanExtra(IS_STRICT_CONSENT_BEHAVIOR, false);
77         mIsRenotify = requireActivity().getIntent().getBooleanExtra(IS_RENOTIFY_KEY, false);
78         mIsFirstTimeRow = false;
79         if (mIsRenotify) {
80             LoggerFactory.getUILogger().d("PAS renotify");
81             // renotify version
82             inflatedView =
83                     inflater.inflate(R.layout.consent_notification_screen_1_pas, container, false);
84             TextView title = inflatedView.findViewById(R.id.notification_title);
85             title.setText(R.string.notificationUI_pas_renotify_header_title);
86         } else if (mIsStrictConsentBehavior) {
87             LoggerFactory.getUILogger().d("PAS strict consent behavior");
88             // first-time version
89             inflatedView =
90                     inflater.inflate(R.layout.consent_notification_screen_1_pas, container, false);
91         } else {
92             // combined version
93             LoggerFactory.getUILogger().d("PAS combined version");
94             mIsFirstTimeRow = true;
95             inflatedView =
96                     inflater.inflate(
97                             R.layout.consent_notification_pas_first_time_row, container, false);
98         }
99         return inflatedView;
100     }
101 
102     @Override
onViewCreated(@onNull View view, Bundle savedInstanceState)103     public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
104         setupListeners(savedInstanceState);
105         ConsentManager consentManager = ConsentManager.getInstance();
106         boolean isNotRenotifyNoManualInteraction =
107                 !mIsRenotify
108                         && consentManager.getUserManualInteractionWithConsent()
109                                 != MANUAL_INTERACTIONS_RECORDED;
110         if (UxStatesManager.getInstance().getFlag(KEY_EEA_PAS_UX_ENABLED)) {
111             consentManager.recordPasNotificationOpened(true);
112             if (mIsStrictConsentBehavior && isNotRenotifyNoManualInteraction) {
113                 consentManager.enable(requireContext(), AdServicesApiType.FLEDGE);
114                 consentManager.enable(requireContext(), AdServicesApiType.MEASUREMENTS);
115             }
116         }
117         ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISPLAYED);
118     }
119 
120     @Override
onSaveInstanceState(@onNull Bundle savedInstanceState)121     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
122         super.onSaveInstanceState(savedInstanceState);
123         if (mScrollToBottomController != null) {
124             mScrollToBottomController.saveInstanceState(savedInstanceState);
125         }
126         ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISMISSED);
127     }
128 
setupListeners(Bundle savedInstanceState)129     private void setupListeners(Bundle savedInstanceState) {
130         TextView howItWorksExpander = requireActivity().findViewById(R.id.how_it_works_expander);
131         if (savedInstanceState != null) {
132             setInfoViewState1(savedInstanceState.getBoolean(INFO_VIEW_EXPANDED_1, false));
133         }
134         howItWorksExpander.setOnClickListener(
135                 view -> {
136                     ConsentNotificationActivity.handleAction(
137                             CONFIRMATION_PAGE_OPT_OUT_MORE_INFO_CLICKED);
138 
139                     setInfoViewState1(!mIsInfoViewExpanded1);
140                 });
141         ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy1))
142                 .setMovementMethod(LinkMovementMethod.getInstance());
143 
144         if (!mIsFirstTimeRow) {
145             TextView howItWorksExpander2 =
146                     requireActivity().findViewById(R.id.how_it_works_expander2);
147             if (savedInstanceState != null) {
148                 setInfoViewState2(savedInstanceState.getBoolean(INFO_VIEW_EXPANDED_2, false));
149             }
150             howItWorksExpander2.setOnClickListener(
151                     view -> {
152                         ConsentNotificationActivity.handleAction(
153                                 LANDING_PAGE_ADDITIONAL_INFO_2_CLICKED);
154                         setInfoViewState2(!mIsInfoViewExpanded2);
155                     });
156             ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy2))
157                     .setMovementMethod(LinkMovementMethod.getInstance());
158             // initialize hyperlink in 1st section
159             ((TextView) requireActivity().findViewById(R.id.notificationUI_pas_app_body2_part2))
160                     .setMovementMethod(LinkMovementMethod.getInstance());
161         } else {
162             ((TextView)
163                             requireActivity()
164                                     .findViewById(R.id.notificationUI_pas_combined_dropdown_body6))
165                     .setMovementMethod(LinkMovementMethod.getInstance());
166         }
167 
168         Button leftControlButton = requireActivity().findViewById(R.id.leftControlButton);
169         leftControlButton.setOnClickListener(
170                 view -> {
171                     ConsentNotificationActivity.handleAction(
172                             CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED);
173 
174                     // go to settings activity
175                     Intent intent =
176                             new Intent(requireActivity(), AdServicesSettingsMainActivity.class);
177                     // users should be able to go back to notification if clicked manage settings
178                     intent.putExtra(FROM_NOTIFICATION_KEY, true);
179                     startActivity(intent);
180                     requireActivity().finish();
181                 });
182 
183         Button rightControlButton = requireActivity().findViewById(R.id.rightControlButton);
184         ScrollView scrollView = requireView().findViewById(R.id.notification_fragment_scrollview);
185         mScrollToBottomController =
186                 new ScrollToBottomController(
187                         scrollView, leftControlButton, rightControlButton, savedInstanceState);
188         mScrollToBottomController.bind();
189         // check whether it can scroll vertically and update buttons after layout can be measured
190         scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom());
191     }
192 
setInfoViewState1(boolean expanded)193     private void setInfoViewState1(boolean expanded) {
194         View text = requireActivity().findViewById(R.id.how_it_works_expanded_text);
195         TextView expander = requireActivity().findViewById(R.id.how_it_works_expander);
196         mIsInfoViewExpanded1 = infoViewChanger(expanded, text, expander);
197     }
198 
setInfoViewState2(boolean expanded)199     private void setInfoViewState2(boolean expanded) {
200         View text = requireActivity().findViewById(R.id.how_it_works_expanded_text2);
201         TextView expander = requireActivity().findViewById(R.id.how_it_works_expander2);
202         mIsInfoViewExpanded2 = infoViewChanger(expanded, text, expander);
203     }
204 
205     // returns the state of the info view
infoViewChanger(boolean expanded, View text, TextView expander)206     private boolean infoViewChanger(boolean expanded, View text, TextView expander) {
207         if (expanded) {
208             text.setVisibility(View.VISIBLE);
209             expander.setCompoundDrawablesRelativeWithIntrinsicBounds(
210                     0, 0, R.drawable.ic_minimize, 0);
211         } else {
212             text.setVisibility(View.GONE);
213             expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0);
214         }
215         return expanded;
216     }
217 
218     // helper method to start topics consent notification screen for EU users
startTopicsConsentNotificationFragment()219     private void startTopicsConsentNotificationFragment() {
220         requireActivity()
221                 .getSupportFragmentManager()
222                 .beginTransaction()
223                 .replace(
224                         R.id.fragment_container_view,
225                         ConsentNotificationGaV2Screen2Fragment.class,
226                         null)
227                 .setReorderingAllowed(true)
228                 .addToBackStack(null)
229                 .commit();
230     }
231 
232     /**
233      * Allows the positive, acceptance button to scroll the view.
234      *
235      * <p>When the positive button first appears it will show the text "More". When the user taps
236      * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the
237      * button text will be replaced with the acceptance text. Once the text has changed, the button
238      * will trigger the positive action no matter where the view is scrolled.
239      */
240     private class ScrollToBottomController implements View.OnScrollChangeListener {
241         private static final String STATE_HAS_SCROLLED_TO_BOTTOM = "has_scrolled_to_bottom";
242         private static final int SCROLL_DIRECTION_DOWN = 1;
243         private static final double SCROLL_MULTIPLIER = 0.8;
244 
245         private final ScrollView mScrollContainer;
246         private final Button mLeftControlButton;
247         private final Button mRightControlButton;
248 
249         private boolean mHasScrolledToBottom;
250 
ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)251         ScrollToBottomController(
252                 ScrollView scrollContainer,
253                 Button leftControlButton,
254                 Button rightControlButton,
255                 @Nullable Bundle savedInstanceState) {
256             this.mScrollContainer = scrollContainer;
257             this.mLeftControlButton = leftControlButton;
258             this.mRightControlButton = rightControlButton;
259             mHasScrolledToBottom =
260                     savedInstanceState != null
261                             && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM)
262                             && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM);
263         }
264 
bind()265         public void bind() {
266             mScrollContainer.setOnScrollChangeListener(this);
267             mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked);
268             updateControlButtons();
269         }
270 
saveInstanceState(Bundle bundle)271         public void saveInstanceState(Bundle bundle) {
272             if (mHasScrolledToBottom) {
273                 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM, true);
274             }
275         }
276 
updateControlButtons()277         private void updateControlButtons() {
278             if (mHasScrolledToBottom) {
279                 mLeftControlButton.setVisibility(View.VISIBLE);
280                 mRightControlButton.setText(
281                         R.string.notificationUI_confirmation_right_control_button_text);
282             } else {
283                 mLeftControlButton.setVisibility(View.INVISIBLE);
284                 mRightControlButton.setText(R.string.notificationUI_more_button_text);
285             }
286         }
287 
onMoreOrAcceptClicked(View view)288         private void onMoreOrAcceptClicked(View view) {
289             if (mHasScrolledToBottom) {
290                 // screen 2
291                 if (!mIsRenotify && mIsStrictConsentBehavior) {
292                     startTopicsConsentNotificationFragment();
293                 } else {
294                     requireActivity().finishAndRemoveTask();
295                 }
296             } else {
297                 mScrollContainer.smoothScrollTo(
298                         0,
299                         mScrollContainer.getScrollY()
300                                 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER));
301             }
302         }
303 
304         @Override
onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)305         public void onScrollChange(
306                 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
307             updateButtonsIfHasScrolledToBottom();
308         }
309 
updateButtonsIfHasScrolledToBottom()310         void updateButtonsIfHasScrolledToBottom() {
311             if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) {
312                 mHasScrolledToBottom = true;
313                 updateControlButtons();
314             }
315         }
316     }
317 }
318