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