1 /*
2  * Copyright (C) 2024 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 
17 package com.android.systemui.car.qc;
18 
19 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
20 import static android.Manifest.permission.INTERNET;
21 
22 import android.annotation.Nullable;
23 import android.annotation.SuppressLint;
24 import android.app.ActivityManager;
25 import android.app.ActivityTaskManager;
26 import android.app.TaskStackListener;
27 import android.car.drivingstate.CarUxRestrictions;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageInfo;
32 import android.content.pm.PackageManager;
33 import android.content.res.Resources;
34 import android.net.ConnectivityManager;
35 import android.net.Network;
36 import android.net.NetworkCapabilities;
37 import android.os.Build;
38 import android.os.Handler;
39 import android.util.Log;
40 import android.view.LayoutInflater;
41 import android.view.MotionEvent;
42 import android.view.View;
43 import android.widget.Button;
44 import android.widget.LinearLayout;
45 import android.widget.PopupWindow;
46 import android.widget.TextView;
47 
48 import androidx.annotation.NonNull;
49 import androidx.annotation.VisibleForTesting;
50 
51 import com.android.car.datasubscription.DataSubscription;
52 import com.android.car.ui.utils.CarUxRestrictionsUtil;
53 import com.android.systemui.R;
54 import com.android.systemui.dagger.SysUISingleton;
55 import com.android.systemui.dagger.qualifiers.Background;
56 import com.android.systemui.dagger.qualifiers.Main;
57 import com.android.systemui.settings.UserTracker;
58 
59 import java.util.Arrays;
60 import java.util.HashSet;
61 import java.util.List;
62 import java.util.Set;
63 import java.util.concurrent.CountDownLatch;
64 import java.util.concurrent.Executor;
65 import java.util.concurrent.TimeUnit;
66 
67 import javax.inject.Inject;
68 
69 /**
70  * Controller to display the data subscription pop-up
71  */
72 @SysUISingleton
73 public class DataSubscriptionController implements DataSubscription.DataSubscriptionChangeListener {
74     private static final boolean DEBUG = Build.IS_DEBUGGABLE;
75     private static final String TAG = DataSubscriptionController.class.toString();
76     private static final String DATA_SUBSCRIPTION_ACTION =
77             "android.intent.action.DATA_SUBSCRIPTION";
78     // Timeout for network callback in ms
79     private static final int CALLBACK_TIMEOUT_MS = 1000;
80     private final Context mContext;
81     private DataSubscription mSubscription;
82     private final UserTracker mUserTracker;
83     private PopupWindow mPopupWindow;
84     private final View mPopupView;
85     private Button mExplorationButton;
86     private final Intent mIntent;
87     private ConnectivityManager mConnectivityManager;
88     private DataSubscriptionNetworkCallback mNetworkCallback;
89     private final Handler mMainHandler;
90     private final Executor mBackGroundExecutor;
91     private Set<String> mActivitiesBlocklist;
92     private Set<String> mPackagesBlocklist;
93     private CountDownLatch mLatch;
94     private boolean mIsNetworkCallbackRegistered;
95     private final TaskStackListener mTaskStackListener = new TaskStackListener() {
96         @SuppressLint("MissingPermission")
97         @Override
98         public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
99             if (mIsNetworkCallbackRegistered && mConnectivityManager != null) {
100                 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
101                 mIsNetworkCallbackRegistered = false;
102             }
103 
104             if (taskInfo.topActivity == null || mConnectivityManager == null) {
105                 return;
106             }
107             mTopPackage = taskInfo.topActivity.getPackageName();
108             if (mPackagesBlocklist.contains(mTopPackage)) {
109                 return;
110             }
111 
112             mTopActivity = taskInfo.topActivity.flattenToString();
113             if (mActivitiesBlocklist.contains(mTopActivity)) {
114                 return;
115             }
116 
117             PackageInfo info;
118             int userId = mUserTracker.getUserId();
119             try {
120                 info = mContext.getPackageManager().getPackageInfoAsUser(mTopPackage,
121                         PackageManager.GET_PERMISSIONS, userId);
122                 if (info != null) {
123                     String[] permissions = info.requestedPermissions;
124                     boolean appReqInternet = Arrays.asList(permissions).contains(
125                             ACCESS_NETWORK_STATE)
126                             && Arrays.asList(permissions).contains(INTERNET);
127                     if (!appReqInternet) {
128                         mActivitiesBlocklist.add(mTopActivity);
129                         return;
130                     }
131                 }
132 
133                 ApplicationInfo appInfo = mContext.getPackageManager().getApplicationInfoAsUser(
134                         mTopPackage, 0, mUserTracker.getUserId());
135                 int uid = appInfo.uid;
136                 mConnectivityManager.registerDefaultNetworkCallbackForUid(uid, mNetworkCallback,
137                         mMainHandler);
138                 mIsNetworkCallbackRegistered = true;
139                 // since we don't have the option of using the synchronous call of getting the
140                 // default network by UID, we need to set a timeout period to make sure the network
141                 // from the callback is updated correctly before deciding to display the message
142                 //TODO: b/336869328 use the synchronous call to update network status
143                 mLatch = new CountDownLatch(CALLBACK_TIMEOUT_MS);
144                 mBackGroundExecutor.execute(() -> {
145                     try {
146                         mLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
147                     } catch (InterruptedException e) {
148                         Log.e(TAG, "error updating network callback" + e);
149                     } finally {
150                         if (mNetworkCallback.mNetwork == null) {
151                             mNetworkCapabilities = null;
152                             updateShouldDisplayReactiveMsg();
153                             if (mShouldDisplayReactiveMsg) {
154                                 showPopUpWindow();
155                             }
156                         }
157                     }
158                 });
159             } catch (Exception e) {
160                 Log.e(TAG, mTopPackage + " not found : " + e);
161             }
162         }
163     };
164 
165     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener
166             mUxRestrictionsChangedListener =
167             new CarUxRestrictionsUtil.OnUxRestrictionsChangedListener() {
168                 @Override
169                 public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) {
170                     mIsDistractionOptimizationRequired =
171                             carUxRestrictions.isRequiresDistractionOptimization();
172                     if (mIsProactiveMsg) {
173                         if (mIsDistractionOptimizationRequired
174                                 && mPopupWindow != null
175                                 && mPopupWindow.isShowing()) {
176                             mPopupWindow.dismiss();
177                         }
178                     } else {
179                         if (mIsDistractionOptimizationRequired && mPopupWindow != null) {
180                             mExplorationButton.setVisibility(View.GONE);
181 
182                         } else {
183                             mExplorationButton.setVisibility(View.VISIBLE);
184                         }
185                         mPopupWindow.update();
186                     }
187                 }
188             };
189 
190     // Determines whether a proactive message was already displayed
191     private boolean mWasProactiveMsgDisplayed;
192     // Determines whether the current message being displayed is proactive or reactive
193     private boolean mIsProactiveMsg;
194     private boolean mIsDistractionOptimizationRequired;
195     private View mAnchorView;
196     private boolean mShouldDisplayProactiveMsg;
197 
198     private final int mPopUpTimeOut;
199     private boolean mShouldDisplayReactiveMsg;
200     private String mTopActivity;
201     private String mTopPackage;
202     private NetworkCapabilities mNetworkCapabilities;
203     private boolean mIsUxRestrictionsListenerRegistered;
204 
205     @SuppressLint("MissingPermission")
206     @Inject
DataSubscriptionController(Context context, UserTracker userTracker, @Main Handler mainHandler, @Background Executor backgroundExecutor)207     public DataSubscriptionController(Context context,
208             UserTracker userTracker,
209             @Main Handler mainHandler,
210             @Background Executor backgroundExecutor) {
211         mContext = context;
212         mSubscription = new DataSubscription(context);
213         mUserTracker = userTracker;
214         mMainHandler = mainHandler;
215         mBackGroundExecutor = backgroundExecutor;
216         mIntent = new Intent(DATA_SUBSCRIPTION_ACTION);
217         mIntent.setPackage(mContext.getString(
218                 R.string.connectivity_flow_app));
219         mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
220         LayoutInflater inflater = LayoutInflater.from(mContext);
221         mPopupView = inflater.inflate(R.layout.data_subscription_popup_window, null);
222         mPopUpTimeOut = mContext.getResources().getInteger(
223                 R.integer.data_subscription_pop_up_timeout);
224         int width = LinearLayout.LayoutParams.WRAP_CONTENT;
225         int height = LinearLayout.LayoutParams.WRAP_CONTENT;
226         boolean focusable = true;
227         mPopupWindow = new PopupWindow(mPopupView, width, height, focusable);
228         mPopupWindow.setTouchModal(false);
229         mPopupWindow.setOutsideTouchable(true);
230         mPopupView.setOnTouchListener(new View.OnTouchListener() {
231             @Override
232             public boolean onTouch(View v, MotionEvent event) {
233                 if (mPopupWindow != null) {
234                     mPopupWindow.dismiss();
235                     if (!mWasProactiveMsgDisplayed) {
236                         mWasProactiveMsgDisplayed = true;
237                     }
238                 }
239                 return true;
240             }
241         });
242 
243         mExplorationButton = mPopupView.findViewById(
244                 R.id.data_subscription_explore_options_button);
245         mExplorationButton.setOnClickListener(v -> {
246             mContext.startActivityAsUser(mIntent, mUserTracker.getUserHandle());
247             mPopupWindow.dismiss();
248         });
249         mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
250         mNetworkCallback = new DataSubscriptionNetworkCallback();
251         mActivitiesBlocklist = new HashSet<>();
252         mPackagesBlocklist = new HashSet<>();
253 
254         Resources res = mContext.getResources();
255         String[] blockActivities = res.getStringArray(
256                 R.array.config_dataSubscriptionBlockedActivitiesList);
257         mActivitiesBlocklist.addAll(List.of(blockActivities));
258         String[] blockComponents = res.getStringArray(
259                 R.array.config_dataSubscriptionBlockedPackagesList);
260         mPackagesBlocklist.addAll(List.of(blockComponents));
261         try {
262             ActivityTaskManager.getService().registerTaskStackListener(mTaskStackListener);
263         } catch (Exception e) {
264             Log.e(TAG, "error while registering TaskStackListener " + e);
265         }
266     }
267 
updateShouldDisplayProactiveMsg()268     private void updateShouldDisplayProactiveMsg() {
269         if (mIsDistractionOptimizationRequired) {
270             if (mPopupWindow != null && mPopupWindow.isShowing()) {
271                 mPopupWindow.dismiss();
272             }
273         } else {
274             // Determines whether a proactive message should be displayed
275             mShouldDisplayProactiveMsg = !mWasProactiveMsgDisplayed
276                     && mSubscription.isDataSubscriptionInactive();
277             if (mShouldDisplayProactiveMsg && mPopupWindow != null
278                     && !mPopupWindow.isShowing()) {
279                 mIsProactiveMsg = true;
280                 showPopUpWindow();
281             }
282         }
283     }
284 
updateShouldDisplayReactiveMsg()285     private void updateShouldDisplayReactiveMsg() {
286         if (mIsDistractionOptimizationRequired) {
287             mExplorationButton.setVisibility(View.GONE);
288 
289         } else {
290             mExplorationButton.setVisibility(View.VISIBLE);
291         }
292         if (!mPopupWindow.isShowing()) {
293             mShouldDisplayReactiveMsg = ((mNetworkCapabilities == null
294                     || (!isSuspendedNetwork() && !isValidNetwork()))
295                     && mSubscription.isDataSubscriptionInactive());
296             if (mShouldDisplayReactiveMsg) {
297                 mIsProactiveMsg = false;
298                 showPopUpWindow();
299                 mActivitiesBlocklist.add(mTopActivity);
300             } else {
301                 if (mPopupWindow != null && mPopupWindow.isShowing()) {
302                     mPopupWindow.dismiss();
303                 }
304             }
305         }
306     }
307 
308     @VisibleForTesting
showPopUpWindow()309     void showPopUpWindow() {
310         if (mAnchorView != null) {
311             mAnchorView.post(new Runnable() {
312                 @Override
313                 public void run() {
314                     TextView popUpPrompt = mPopupView.findViewById(R.id.popup_text_view);
315                     if (popUpPrompt != null) {
316                         if (mIsProactiveMsg) {
317                             popUpPrompt.setText(R.string.data_subscription_proactive_msg_prompt);
318                         } else {
319                             popUpPrompt.setText(R.string.data_subscription_reactive_msg_prompt);
320                         }
321                     }
322                     int xOffsetInPx = mContext.getResources().getDimensionPixelSize(
323                             R.dimen.car_quick_controls_entry_points_button_width);
324                     int yOffsetInPx = mContext.getResources().getDimensionPixelSize(
325                             R.dimen.car_quick_controls_panel_margin);
326                     mPopupWindow.showAsDropDown(mAnchorView, -xOffsetInPx / 2, yOffsetInPx);
327                     mAnchorView.getHandler().postDelayed(new Runnable() {
328 
329                         public void run() {
330                             mPopupWindow.dismiss();
331                             mWasProactiveMsgDisplayed = true;
332                             // after the proactive msg dismisses, it won't get displayed again hence
333                             // the msg from now on will just be reactive
334                             mIsProactiveMsg = false;
335                         }
336                     }, mPopUpTimeOut);
337                 }
338             });
339         }
340     }
341 
342     /** Set the anchor view. If null, unregisters active data subscription listeners */
setAnchorView(@ullable View view)343     public void setAnchorView(@Nullable View view) {
344         mAnchorView = view;
345         if (mAnchorView != null) {
346             mSubscription.addDataSubscriptionListener(this);
347             updateShouldDisplayProactiveMsg();
348             if (!mIsUxRestrictionsListenerRegistered) {
349                 CarUxRestrictionsUtil.getInstance(mContext).register(
350                         mUxRestrictionsChangedListener);
351                 mIsUxRestrictionsListenerRegistered = true;
352             }
353         } else {
354             mSubscription.removeDataSubscriptionListener();
355             if (mIsUxRestrictionsListenerRegistered) {
356                 CarUxRestrictionsUtil.getInstance(mContext).unregister(
357                         mUxRestrictionsChangedListener);
358                 mIsUxRestrictionsListenerRegistered = false;
359             }
360         }
361     }
362 
isValidNetwork()363     boolean isValidNetwork() {
364         return mNetworkCapabilities.hasCapability(
365                 NetworkCapabilities.NET_CAPABILITY_VALIDATED);
366     }
367 
isSuspendedNetwork()368     boolean isSuspendedNetwork() {
369         return !mNetworkCapabilities.hasCapability(
370                 NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
371     }
372 
373     @Override
onChange(int value)374     public void onChange(int value) {
375         updateShouldDisplayProactiveMsg();
376     }
377 
378     public class DataSubscriptionNetworkCallback extends ConnectivityManager.NetworkCallback {
379         Network mNetwork;
380 
381         @Override
onAvailable(@onNull Network network)382         public void onAvailable(@NonNull Network network) {
383             if (DEBUG) {
384                 Log.d(TAG, "onAvailable " + network);
385             }
386             mNetwork = network;
387             mLatch.countDown();
388         }
389 
390         @Override
onCapabilitiesChanged(@onNull Network network, @NonNull NetworkCapabilities networkCapabilities)391         public void onCapabilitiesChanged(@NonNull Network network,
392                 @NonNull NetworkCapabilities networkCapabilities) {
393             if (DEBUG) {
394                 Log.d(TAG, "onCapabilitiesChanged " + network);
395             }
396             mNetwork = network;
397             mNetworkCapabilities = networkCapabilities;
398             updateShouldDisplayReactiveMsg();
399             if (mShouldDisplayReactiveMsg) {
400                 showPopUpWindow();
401             }
402         }
403     }
404 
405     @VisibleForTesting
setPopupWindow(PopupWindow popupWindow)406     void setPopupWindow(PopupWindow popupWindow) {
407         mPopupWindow = popupWindow;
408     }
409 
410     @VisibleForTesting
setSubscription(DataSubscription dataSubscription)411     void setSubscription(DataSubscription dataSubscription) {
412         mSubscription = dataSubscription;
413     }
414 
415     @VisibleForTesting
getShouldDisplayProactiveMsg()416     boolean getShouldDisplayProactiveMsg() {
417         return mShouldDisplayProactiveMsg;
418     }
419 
420     @VisibleForTesting
setPackagesBlocklist(Set<String> list)421     void setPackagesBlocklist(Set<String> list) {
422         mPackagesBlocklist = list;
423     }
424 
425     @VisibleForTesting
setActivitiesBlocklist(Set<String> list)426     void setActivitiesBlocklist(Set<String> list) {
427         mActivitiesBlocklist = list;
428     }
429 
430     @VisibleForTesting
setConnectivityManager(ConnectivityManager connectivityManager)431     void setConnectivityManager(ConnectivityManager connectivityManager) {
432         mConnectivityManager = connectivityManager;
433     }
434 
435     @VisibleForTesting
getTaskStackListener()436     TaskStackListener getTaskStackListener() {
437         return mTaskStackListener;
438     }
439 
440     @VisibleForTesting
getShouldDisplayReactiveMsg()441     boolean getShouldDisplayReactiveMsg() {
442         return mShouldDisplayReactiveMsg;
443     }
444 
445     @VisibleForTesting
setNetworkCallback(DataSubscriptionNetworkCallback callback)446     void setNetworkCallback(DataSubscriptionNetworkCallback callback) {
447         mNetworkCallback = callback;
448     }
449 
450     @VisibleForTesting
setIsCallbackRegistered(boolean value)451     void setIsCallbackRegistered(boolean value) {
452         mIsNetworkCallbackRegistered = value;
453     }
454 
455     @VisibleForTesting
setIsProactiveMsg(boolean value)456     void setIsProactiveMsg(boolean value) {
457         mIsProactiveMsg = value;
458     }
459 
460     @VisibleForTesting
setExplorationButton(Button button)461     void setExplorationButton(Button button) {
462         mExplorationButton = button;
463     }
464 
465     @VisibleForTesting
setIsUxRestrictionsListenerRegistered(boolean value)466     void setIsUxRestrictionsListenerRegistered(boolean value) {
467         mIsUxRestrictionsListenerRegistered = value;
468     }
469 
470     @VisibleForTesting
setWasProactiveMsgDisplayed(boolean value)471     void setWasProactiveMsgDisplayed(boolean value) {
472         mWasProactiveMsgDisplayed = value;
473     }
474 }
475