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