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