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.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DISMISS_NOTIFICATION_FAILURE;
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX;
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_IN_GOT_IT_BUTTON_CLICKED;
23 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_IN_MORE_INFO_CLICKED;
24 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_IN_SETTINGS_CLICKED;
25 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_GOT_IT_BUTTON_CLICKED;
26 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_MORE_INFO_CLICKED;
27 import static com.android.adservices.ui.notifications.ConsentNotificationActivity.NotificationFragmentEnum.CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED;
28 import static com.android.adservices.ui.notifications.ConsentNotificationTrigger.NOTIFICATION_ID;
29 import static com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity.FROM_NOTIFICATION_KEY;
30 
31 import android.content.Intent;
32 import android.os.Build;
33 import android.os.Bundle;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.View.OnScrollChangeListener;
37 import android.view.ViewGroup;
38 import android.widget.Button;
39 import android.widget.ScrollView;
40 import android.widget.TextView;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.RequiresApi;
45 import androidx.core.app.NotificationManagerCompat;
46 import androidx.fragment.app.Fragment;
47 
48 import com.android.adservices.LogUtil;
49 import com.android.adservices.api.R;
50 import com.android.adservices.errorlogging.ErrorLogUtil;
51 import com.android.adservices.service.consent.AdServicesApiConsent;
52 import com.android.adservices.service.consent.AdServicesApiType;
53 import com.android.adservices.service.consent.ConsentManager;
54 import com.android.adservices.ui.notifications.ConsentNotificationActivity;
55 import com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity;
56 
57 /**
58  * Fragment for the confirmation view after accepting or rejecting to be part of Privacy Sandbox
59  * Beta.
60  */
61 // TODO(b/269798827): Enable for R.
62 // TODO(b/274955086): add logging for more button and scrolling
63 @RequiresApi(Build.VERSION_CODES.S)
64 public class ConsentNotificationConfirmationGaFragment extends Fragment {
65     public static final String IS_FlEDGE_MEASUREMENT_INFO_VIEW_EXPANDED_KEY =
66             "is_fledge_measurement_info_view_expanded";
67     private boolean mIsInfoViewExpanded = false;
68     private @Nullable ScrollToBottomController mScrollToBottomController;
69 
70     private boolean mTopicsOptIn;
71 
72     @Override
onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)73     public View onCreateView(
74             @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
75         AdServicesApiConsent topicsConsent =
76                 ConsentManager.getInstance().getConsent(AdServicesApiType.TOPICS);
77         mTopicsOptIn = topicsConsent != null ? topicsConsent.isGiven() : false;
78 
79         dismissNotificationIfNeeded();
80 
81         ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.FLEDGE);
82         ConsentManager.getInstance().enable(requireContext(), AdServicesApiType.MEASUREMENTS);
83         return inflater.inflate(
84                 R.layout.consent_notification_fledge_measurement_fragment_eu, container, false);
85     }
86 
dismissNotificationIfNeeded()87     private void dismissNotificationIfNeeded() {
88         try {
89             NotificationManagerCompat notificationManager =
90                     NotificationManagerCompat.from(requireContext());
91             notificationManager.cancel(NOTIFICATION_ID);
92         } catch (Exception e) {
93             LogUtil.e(e.toString());
94             ErrorLogUtil.e(e,
95                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DISMISS_NOTIFICATION_FAILURE,
96                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
97         }
98     }
99 
100     @Override
onViewCreated(@onNull View view, Bundle savedInstanceState)101     public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
102         setupListeners(savedInstanceState);
103 
104         ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISPLAYED, getContext());
105     }
106 
107     @Override
onSaveInstanceState(@onNull Bundle savedInstanceState)108     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
109         super.onSaveInstanceState(savedInstanceState);
110         if (mScrollToBottomController != null) {
111             mScrollToBottomController.saveInstanceState(savedInstanceState);
112         }
113         ConsentNotificationActivity.handleAction(CONFIRMATION_PAGE_DISMISSED, getContext());
114     }
115 
setupListeners(Bundle savedInstanceState)116     private void setupListeners(Bundle savedInstanceState) {
117         TextView howItWorksExpander =
118                 requireActivity().findViewById(R.id.how_it_works_fledge_measurement_expander);
119         if (savedInstanceState != null) {
120             setInfoViewState(
121                     savedInstanceState.getBoolean(
122                             IS_FlEDGE_MEASUREMENT_INFO_VIEW_EXPANDED_KEY, false));
123         }
124         howItWorksExpander.setOnClickListener(
125                 view -> {
126                     if (mTopicsOptIn) {
127                         ConsentNotificationActivity.handleAction(
128                                 CONFIRMATION_PAGE_OPT_IN_MORE_INFO_CLICKED, getContext());
129                     } else {
130                         ConsentNotificationActivity.handleAction(
131                                 CONFIRMATION_PAGE_OPT_OUT_MORE_INFO_CLICKED, getContext());
132                     }
133 
134                     setInfoViewState(!mIsInfoViewExpanded);
135                 });
136 
137         Button leftControlButton =
138                 requireActivity().findViewById(R.id.leftControlButtonConfirmation);
139         leftControlButton.setOnClickListener(
140                 view -> {
141                     if (mTopicsOptIn) {
142                         ConsentNotificationActivity.handleAction(
143                                 CONFIRMATION_PAGE_OPT_IN_SETTINGS_CLICKED, getContext());
144                     } else {
145                         ConsentNotificationActivity.handleAction(
146                                 CONFIRMATION_PAGE_OPT_OUT_SETTINGS_CLICKED, getContext());
147                     }
148 
149                     // go to settings activity
150                     Intent intent =
151                             new Intent(requireActivity(), AdServicesSettingsMainActivity.class);
152                     intent.putExtra(FROM_NOTIFICATION_KEY, true);
153                     startActivity(intent);
154                     requireActivity().finish();
155                 });
156 
157         Button rightControlButton =
158                 requireActivity().findViewById(R.id.rightControlButtonConfirmation);
159         rightControlButton.setOnClickListener(
160                 view -> {
161                     if (mTopicsOptIn) {
162                         ConsentNotificationActivity.handleAction(
163                                 CONFIRMATION_PAGE_OPT_IN_GOT_IT_BUTTON_CLICKED, getContext());
164                     } else {
165                         ConsentNotificationActivity.handleAction(
166                                 CONFIRMATION_PAGE_OPT_OUT_GOT_IT_BUTTON_CLICKED, getContext());
167                     }
168 
169                     // acknowledge and dismiss
170                     requireActivity().finish();
171                 });
172 
173         ScrollView scrollView =
174                 requireView().findViewById(R.id.consent_notification_fledge_measurement_view);
175 
176         mScrollToBottomController =
177                 new ScrollToBottomController(
178                         scrollView, leftControlButton, rightControlButton, savedInstanceState);
179         mScrollToBottomController.bind();
180         // check whether it can scroll vertically and update buttons after layout can be measured
181         scrollView.post(() -> mScrollToBottomController.updateButtonsIfHasScrolledToBottom());
182     }
183 
setInfoViewState(boolean expanded)184     private void setInfoViewState(boolean expanded) {
185         View text =
186                 requireActivity().findViewById(R.id.how_it_works_fledge_measurement_expanded_text);
187         TextView expander =
188                 requireActivity().findViewById(R.id.how_it_works_fledge_measurement_expander);
189         if (expanded) {
190             mIsInfoViewExpanded = true;
191             text.setVisibility(View.VISIBLE);
192             expander.setCompoundDrawablesRelativeWithIntrinsicBounds(
193                     0, 0, R.drawable.ic_minimize, 0);
194         } else {
195             mIsInfoViewExpanded = false;
196             text.setVisibility(View.GONE);
197             expander.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_expand, 0);
198         }
199     }
200 
201     /**
202      * Allows the positive, acceptance button to scroll the view.
203      *
204      * <p>When the positive button first appears it will show the text "More". When the user taps
205      * the button, the view will scroll to the bottom. Once the view has scrolled to the bottom, the
206      * button text will be replaced with the acceptance text. Once the text has changed, the button
207      * will trigger the positive action no matter where the view is scrolled.
208      */
209     private class ScrollToBottomController implements OnScrollChangeListener {
210         private static final String STATE_HAS_SCROLLED_TO_BOTTOM = "has_scrolled_to_bottom";
211         private static final int SCROLL_DIRECTION_DOWN = 1;
212         private static final double SCROLL_MULTIPLIER = 0.8;
213 
214         private final ScrollView mScrollContainer;
215         private final Button mLeftControlButton;
216         private final Button mRightControlButton;
217 
218         private boolean mHasScrolledToBottom;
219 
ScrollToBottomController( ScrollView scrollContainer, Button leftControlButton, Button rightControlButton, @Nullable Bundle savedInstanceState)220         ScrollToBottomController(
221                 ScrollView scrollContainer,
222                 Button leftControlButton,
223                 Button rightControlButton,
224                 @Nullable Bundle savedInstanceState) {
225             this.mScrollContainer = scrollContainer;
226             this.mLeftControlButton = leftControlButton;
227             this.mRightControlButton = rightControlButton;
228             mHasScrolledToBottom =
229                     savedInstanceState != null
230                             && savedInstanceState.containsKey(STATE_HAS_SCROLLED_TO_BOTTOM)
231                             && savedInstanceState.getBoolean(STATE_HAS_SCROLLED_TO_BOTTOM);
232         }
233 
bind()234         public void bind() {
235             mScrollContainer.setOnScrollChangeListener(this);
236             mRightControlButton.setOnClickListener(this::onMoreOrAcceptClicked);
237             updateControlButtons();
238         }
239 
saveInstanceState(Bundle bundle)240         public void saveInstanceState(Bundle bundle) {
241             if (mHasScrolledToBottom) {
242                 bundle.putBoolean(STATE_HAS_SCROLLED_TO_BOTTOM, true);
243             }
244         }
245 
updateControlButtons()246         private void updateControlButtons() {
247             if (mHasScrolledToBottom) {
248                 mLeftControlButton.setVisibility(View.VISIBLE);
249                 mRightControlButton.setText(
250                         R.string.notificationUI_confirmation_right_control_button_text);
251             } else {
252                 mLeftControlButton.setVisibility(View.INVISIBLE);
253                 mRightControlButton.setText(R.string.notificationUI_more_button_text);
254             }
255         }
256 
onMoreOrAcceptClicked(View view)257         private void onMoreOrAcceptClicked(View view) {
258             if (mHasScrolledToBottom) {
259                 // acknowledge and dismiss
260                 requireActivity().finishAndRemoveTask();
261             } else {
262                 mScrollContainer.smoothScrollTo(
263                         0,
264                         mScrollContainer.getScrollY()
265                                 + (int) (mScrollContainer.getHeight() * SCROLL_MULTIPLIER));
266             }
267         }
268 
269         @Override
onScrollChange( View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY)270         public void onScrollChange(
271                 View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
272             updateButtonsIfHasScrolledToBottom();
273         }
274 
updateButtonsIfHasScrolledToBottom()275         void updateButtonsIfHasScrolledToBottom() {
276             if (!mScrollContainer.canScrollVertically(SCROLL_DIRECTION_DOWN)) {
277                 mHasScrolledToBottom = true;
278                 updateControlButtons();
279             }
280         }
281     }
282 }
283