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_MORE_BUTTON_CLICKED;
22 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_OPT_IN_CLICKED;
23 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_OPT_OUT_CLICKED;
24 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_SCROLLED;
25 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.LANDING_PAGE_SCROLLED_TO_BOTTOM;
26 
27 import android.content.Context;
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.View.OnScrollChangeListener;
34 import android.view.ViewGroup;
35 import android.widget.Button;
36 import android.widget.ScrollView;
37 import android.widget.TextView;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.annotation.RequiresApi;
42 import androidx.fragment.app.Fragment;
43 
44 import com.android.adservices.api.R;
45 import com.android.adservices.service.FlagsFactory;
46 import com.android.adservices.service.consent.AdServicesApiType;
47 import com.android.adservices.service.consent.ConsentManager;
48 import com.android.adservices.ui.UxUtil;
49 import com.android.adservices.ui.notifications.ConsentNotificationActivity;
50 
51 /** Fragment for the topics view of the AdServices Settings App. */
52 // TODO(b/269798827): Enable for R.
53 @RequiresApi(Build.VERSION_CODES.S)
54 public class ConsentNotificationGaV2Screen2Fragment extends Fragment {
55     public static final String IS_TOPICS_INFO_VIEW_EXPANDED_KEY = "is_topics_info_view_expanded";
56     private boolean mIsEUDevice;
57     private boolean mIsInfoViewExpanded = false;
58     private @Nullable ScrollToBottomController mScrollToBottomController;
59 
60     @Override
onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)61     public View onCreateView(
62             @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
63         return setupActivity(inflater, container);
64     }
65 
66     @Override
onViewCreated(@onNull View view, Bundle savedInstanceState)67     public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
68         setupListeners(savedInstanceState);
69 
70         ConsentNotificationActivity.handleAction(LANDING_PAGE_DISPLAYED, getContext());
71     }
72 
73     @Override
onSaveInstanceState(@onNull Bundle savedInstanceState)74     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
75         super.onSaveInstanceState(savedInstanceState);
76 
77         ConsentNotificationActivity.handleAction(LANDING_PAGE_DISMISSED, getContext());
78         if (mScrollToBottomController != null) {
79             mScrollToBottomController.saveInstanceState(savedInstanceState);
80         }
81         savedInstanceState.putBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, mIsInfoViewExpanded);
82     }
83 
setupActivity(LayoutInflater inflater, ViewGroup container)84     private View setupActivity(LayoutInflater inflater, ViewGroup container) {
85         mIsEUDevice = UxUtil.isEeaDevice(requireActivity());
86         return inflater.inflate(R.layout.consent_notification_screen_2_ga_v2_eu, container, false);
87     }
88 
setupListeners(Bundle savedInstanceState)89     private void setupListeners(Bundle savedInstanceState) {
90         TextView howItWorksExpander =
91                 requireActivity().findViewById(R.id.how_it_works_expander_screen_2);
92         if (savedInstanceState != null) {
93             setInfoViewState(
94                     savedInstanceState.getBoolean(IS_TOPICS_INFO_VIEW_EXPANDED_KEY, false));
95         }
96         howItWorksExpander.setOnClickListener(
97                 view -> {
98                     setInfoViewState(!mIsInfoViewExpanded);
99                     ConsentNotificationActivity.handleAction(
100                             LANDING_PAGE_ADDITIONAL_INFO_CLICKED, getContext());
101                 });
102 
103         ((TextView) requireActivity().findViewById(R.id.learn_more_from_privacy_policy))
104                 .setMovementMethod(LinkMovementMethod.getInstance());
105 
106         Button leftControlButton = requireActivity().findViewById(R.id.leftControlButton_screen_2);
107         leftControlButton.setOnClickListener(
108                 view -> {
109                     ConsentNotificationActivity.handleAction(
110                             LANDING_PAGE_OPT_OUT_CLICKED, getContext());
111 
112                     // opt-out confirmation activity
113                     ConsentManager.getInstance()
114                             .disable(requireContext(), AdServicesApiType.TOPICS);
115                     if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) {
116                         ConsentManager.getInstance()
117                                 .recordUserManualInteractionWithConsent(
118                                         ConsentManager.MANUAL_INTERACTIONS_RECORDED);
119                     }
120                     requireActivity().finishAndRemoveTask();
121                 });
122 
123         Button rightControlButton =
124                 requireActivity().findViewById(R.id.rightControlButton_screen_2);
125         ScrollView scrollView =
126                 requireView().findViewById(R.id.notification_fragment_scrollview_screen_2);
127 
128         mScrollToBottomController =
129                 new ScrollToBottomController(
130                         scrollView, leftControlButton, rightControlButton, savedInstanceState);
131         mScrollToBottomController.bind();
132         // check whether it can scroll vertically and update buttons after layout can be measured
133         scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom());
134     }
135 
setInfoViewState(boolean expanded)136     private void setInfoViewState(boolean expanded) {
137         View text = requireActivity().findViewById(R.id.how_it_works_expanded_text_screen_2);
138         TextView expander = requireActivity().findViewById(R.id.how_it_works_expander_screen_2);
139         if (expanded) {
140             mIsInfoViewExpanded = true;
141             text.setVisibility(View.VISIBLE);
142             expander.setCompoundDrawablesRelativeWithIntrinsicBounds(
143                     0, 0, R.drawable.ic_minimize, 0);
144         } else {
145             mIsInfoViewExpanded = false;
146             text.setVisibility(View.GONE);
147             expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0);
148         }
149     }
150 
151     /**
152      * Allows the positive, acceptance button to scroll the view.
153      *
154      * <p>When the positive button first appears it will show the text "More". When the user taps
155      * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the
156      * button text will be replaced with the acceptance text. Once the text has changed, the button
157      * will trigger the positive action no matter where the view is scrolled.
158      */
159     private class ScrollToBottomController implements OnScrollChangeListener {
160         private static final String STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE =
161                 "has_scrolled_to_bottom_2";
162         private static final int SCROLL_DIRECTION_DOWN = 1;
163         private static final double SCROLL_MULTIPLIER = 0.8;
164 
165         private final ScrollView mScrollContainer;
166         private final Button mLeftControlButton;
167         private final Button mRightControlButton;
168 
169         private boolean mHasScrolledToBottom;
170 
ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)171         ScrollToBottomController(
172                 ScrollView scrollContainer,
173                 Button leftControlButton,
174                 Button rightControlButton,
175                 @Nullable Bundle savedInstanceState) {
176             this.mScrollContainer = scrollContainer;
177             this.mLeftControlButton = leftControlButton;
178             this.mRightControlButton = rightControlButton;
179             mHasScrolledToBottom =
180                     savedInstanceState != null
181                             && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE)
182                             && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE);
183         }
184 
bind()185         public void bind() {
186             mScrollContainer.setOnScrollChangeListener(this);
187             mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked);
188             updateControlButtons();
189         }
190 
saveInstanceState(Bundle bundle)191         public void saveInstanceState(Bundle bundle) {
192             if (mHasScrolledToBottom) {
193                 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM_2ND_PAGE, true);
194             }
195         }
196 
updateControlButtons()197         private void updateControlButtons() {
198             if (mHasScrolledToBottom) {
199                 mLeftControlButton.setVisibility(View.VISIBLE);
200                 mRightControlButton.setText(
201                         mIsEUDevice
202                                 ? R.string.notificationUI_right_control_button_ga_text_eu_v2
203                                 : R.string.notificationUI_right_control_button_text);
204             } else {
205                 mLeftControlButton.setVisibility(View.INVISIBLE);
206                 mRightControlButton.setText(R.string.notificationUI_more_button_text);
207             }
208         }
209 
onMoreOrAcceptClicked(View view)210         private void onMoreOrAcceptClicked(View view) {
211             Context context = getContext();
212             if (context == null) {
213                 return;
214             }
215 
216             if (mHasScrolledToBottom) {
217                 ConsentNotificationActivity.handleAction(
218                         LANDING_PAGE_OPT_IN_CLICKED, getContext());
219 
220                 // opt-in to topics
221                 ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.TOPICS);
222                 if (FlagsFactory.getFlags().getRecordManualInteractionEnabled()) {
223                     ConsentManager.getInstance()
224                             .recordUserManualInteractionWithConsent(
225                                     ConsentManager.MANUAL_INTERACTIONS_RECORDED);
226                 }
227                 requireActivity().finishAndRemoveTask();
228             } else {
229                 ConsentNotificationActivity.handleAction(
230                         LANDING_PAGE_MORE_BUTTON_CLICKED, getContext());
231 
232                 mScrollContainer.smoothScrollTo(
233                         0,
234                         mScrollContainer.getScrollY()
235                                 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER));
236             }
237         }
238 
239         @Override
onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)240         public void onScrollChange(
241                 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
242             ConsentNotificationActivity.handleAction(LANDING_PAGE_SCROLLED, getContext());
243             updateButtonsIfHasScrolledToBottom();
244         }
245 
updateButtonsIfHasScrolledToBottom()246         void updateButtonsIfHasScrolledToBottom() {
247             if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) {
248                 ConsentNotificationActivity.handleAction(
249                         LANDING_PAGE_SCROLLED_TO_BOTTOM, getContext());
250                 mHasScrolledToBottom = true;
251                 updateControlButtons();
252             }
253         }
254     }
255 }
256