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