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