1 /* 2 * Copyright (C) 2018 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.car.carlauncher; 18 19 import static android.content.Intent.URI_INTENT_SCHEME; 20 21 import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection; 22 import static com.android.car.carlauncher.AppGridConstants.PageOrientation; 23 import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_LAUNCHABLES; 24 import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_MEDIA_SERVICES; 25 26 import android.animation.ValueAnimator; 27 import android.app.AlertDialog; 28 import android.app.usage.UsageStats; 29 import android.app.usage.UsageStatsManager; 30 import android.car.Car; 31 import android.car.CarNotConnectedException; 32 import android.car.content.pm.CarPackageManager; 33 import android.car.drivingstate.CarUxRestrictions; 34 import android.car.drivingstate.CarUxRestrictionsManager; 35 import android.car.media.CarMediaManager; 36 import android.content.BroadcastReceiver; 37 import android.content.ComponentName; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.IntentFilter; 41 import android.content.ServiceConnection; 42 import android.content.pm.LauncherApps; 43 import android.content.pm.PackageManager; 44 import android.os.Build; 45 import android.os.Bundle; 46 import android.os.Handler; 47 import android.os.IBinder; 48 import android.os.Looper; 49 import android.os.Message; 50 import android.os.Messenger; 51 import android.os.RemoteException; 52 import android.text.TextUtils; 53 import android.text.format.DateUtils; 54 import android.util.Log; 55 import android.view.DragEvent; 56 import android.view.SurfaceControl; 57 import android.view.View; 58 import android.widget.FrameLayout; 59 import android.widget.LinearLayout; 60 import android.widget.Toast; 61 62 import androidx.annotation.NonNull; 63 import androidx.annotation.Nullable; 64 import androidx.annotation.StringRes; 65 import androidx.annotation.VisibleForTesting; 66 import androidx.appcompat.app.AppCompatActivity; 67 import androidx.lifecycle.Observer; 68 import androidx.lifecycle.ViewModelProvider; 69 import androidx.recyclerview.widget.RecyclerView; 70 71 import com.android.car.carlauncher.AppLauncherUtils.LauncherAppsInfo; 72 import com.android.car.carlauncher.pagination.PageMeasurementHelper; 73 import com.android.car.carlauncher.pagination.PaginationController; 74 import com.android.car.carlauncher.recyclerview.AppGridAdapter; 75 import com.android.car.carlauncher.recyclerview.AppGridItemAnimator; 76 import com.android.car.carlauncher.recyclerview.AppGridLayoutManager; 77 import com.android.car.carlauncher.recyclerview.AppItemViewHolder; 78 import com.android.car.ui.AlertDialogBuilder; 79 import com.android.car.ui.FocusArea; 80 import com.android.car.ui.baselayout.Insets; 81 import com.android.car.ui.baselayout.InsetsChangedListener; 82 import com.android.car.ui.core.CarUi; 83 import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup; 84 import com.android.car.ui.toolbar.MenuItem; 85 import com.android.car.ui.toolbar.NavButtonMode; 86 import com.android.car.ui.toolbar.ToolbarController; 87 88 import java.net.URISyntaxException; 89 import java.util.ArrayList; 90 import java.util.Arrays; 91 import java.util.Collections; 92 import java.util.Comparator; 93 import java.util.HashSet; 94 import java.util.List; 95 import java.util.Set; 96 import java.util.concurrent.ExecutorService; 97 import java.util.concurrent.Executors; 98 99 /** 100 * Launcher activity that shows a grid of apps. 101 */ 102 public class AppGridActivity extends AppCompatActivity implements InsetsChangedListener, 103 AppGridPageSnapper.PageSnapListener, AppItemViewHolder.AppItemDragListener, 104 AppLauncherUtils.ShortcutsListener, PaginationController.DimensionUpdateListener { 105 private static final String TAG = "AppGridActivity"; 106 private static final String MODE_INTENT_EXTRA = "com.android.car.carlauncher.mode"; 107 private static CarUiShortcutsPopup sCarUiShortcutsPopup; 108 109 private boolean mShowAllApps = true; 110 private boolean mShowToolbar = true; 111 private final Set<String> mHiddenApps = new HashSet<>(); 112 private PackageManager mPackageManager; 113 private UsageStatsManager mUsageStatsManager; 114 private AppInstallUninstallReceiver mInstallUninstallReceiver; 115 private Car mCar; 116 private CarUxRestrictionsManager mCarUxRestrictionsManager; 117 private CarPackageManager mCarPackageManager; 118 private CarMediaManager mCarMediaManager; 119 private Mode mMode; 120 private AlertDialog mStopAppAlertDialog; 121 private LauncherAppsInfo mAppsInfo; 122 private LauncherViewModel mLauncherModel; 123 private AppGridAdapter mAdapter; 124 private AppGridRecyclerView mRecyclerView; 125 private PageIndicator mPageIndicator; 126 private AppGridLayoutManager mLayoutManager; 127 private boolean mIsCurrentlyDragging; 128 private long mOffPageHoverBeforeScrollMs; 129 130 private AppGridDragController mAppGridDragController; 131 private PaginationController mPaginationController; 132 133 private int mNumOfRows; 134 private int mNumOfCols; 135 private int mAppGridMarginHorizontal; 136 private int mAppGridMarginVertical; 137 private int mAppGridWidth; 138 private int mAppGridHeight; 139 @PageOrientation 140 private int mPageOrientation; 141 142 private int mCurrentScrollOffset; 143 private int mCurrentScrollState; 144 private int mNextScrollDestination; 145 private RecyclerView.ItemDecoration mPageMarginDecorator; 146 private AppGridPageSnapper.AppGridPageSnapCallback mSnapCallback; 147 private AppItemViewHolder.AppItemDragCallback mDragCallback; 148 149 private Messenger mMirroringService; 150 private Messenger mMessenger; 151 private String mMirroringPackageName; 152 private Intent mMirroringIntentRedirect; 153 154 /** 155 * enum to define the state of display area possible. 156 * CONTROL_BAR state is when only control bar is visible. 157 * FULL state is when display area hosting default apps cover the screen fully. 158 * DEFAULT state where maps are shown above DA for default apps. 159 */ 160 public enum CAR_LAUNCHER_STATE { 161 CONTROL_BAR, DEFAULT, FULL 162 } 163 164 public enum Mode { 165 ALL_APPS(R.string.app_launcher_title_all_apps, 166 APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES, 167 true), 168 MEDIA_ONLY(R.string.app_launcher_title_media_only, 169 APP_TYPE_MEDIA_SERVICES, 170 true), 171 MEDIA_POPUP(R.string.app_launcher_title_media_only, 172 APP_TYPE_MEDIA_SERVICES, 173 false), 174 ; 175 public final @StringRes int mTitleStringId; 176 public final @AppLauncherUtils.AppTypes int mAppTypes; 177 public final boolean mOpenMediaCenter; 178 Mode(@tringRes int titleStringId, @AppLauncherUtils.AppTypes int appTypes, boolean openMediaCenter)179 Mode(@StringRes int titleStringId, @AppLauncherUtils.AppTypes int appTypes, 180 boolean openMediaCenter) { 181 mTitleStringId = titleStringId; 182 mAppTypes = appTypes; 183 mOpenMediaCenter = openMediaCenter; 184 } 185 } 186 187 private ServiceConnection mCarConnectionListener = new ServiceConnection() { 188 @Override 189 public void onServiceConnected(ComponentName name, IBinder service) { 190 try { 191 mCarUxRestrictionsManager = (CarUxRestrictionsManager) mCar.getCarManager( 192 Car.CAR_UX_RESTRICTION_SERVICE); 193 CarUxRestrictions carUxRestrictions = mCarUxRestrictionsManager 194 .getCurrentCarUxRestrictions(); 195 boolean isDistractionOptimizationRequired; 196 if (carUxRestrictions == null) { 197 Log.v(TAG, "No CarUxRestrictions on display"); 198 isDistractionOptimizationRequired = false; 199 } else { 200 isDistractionOptimizationRequired = carUxRestrictions 201 .isRequiresDistractionOptimization(); 202 } 203 mAdapter.setIsDistractionOptimizationRequired(isDistractionOptimizationRequired); 204 mAdapter.setMode(mMode); 205 // set listener to update the app grid components and apply interaction restrictions 206 // when driving state changes 207 mCarUxRestrictionsManager.registerListener(restrictionInfo -> { 208 handleDistractionOptimization(/* requiresDistractionOptimization */ 209 restrictionInfo.isRequiresDistractionOptimization()); 210 }); 211 mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE); 212 mCarMediaManager = (CarMediaManager) mCar.getCarManager(Car.CAR_MEDIA_SERVICE); 213 initializeLauncherModel(); 214 } catch (CarNotConnectedException e) { 215 Log.e(TAG, "Car not connected in CarConnectionListener", e); 216 } 217 } 218 219 @Override 220 public void onServiceDisconnected(ComponentName name) { 221 mCarUxRestrictionsManager = null; 222 mCarPackageManager = null; 223 } 224 }; 225 226 /** 227 * Updates the state of the app grid components depending on the driving state. 228 */ handleDistractionOptimization(boolean requiresDistractionOptimization)229 private void handleDistractionOptimization(boolean requiresDistractionOptimization) { 230 mAdapter.setIsDistractionOptimizationRequired(requiresDistractionOptimization); 231 if (requiresDistractionOptimization) { 232 // if the user start driving while drag is in action, we cancel existing drag operations 233 if (mIsCurrentlyDragging) { 234 mIsCurrentlyDragging = false; 235 mLayoutManager.setShouldLayoutChildren(true); 236 mRecyclerView.cancelDragAndDrop(); 237 } 238 dismissForceStopMenus(); 239 } 240 } 241 initializeLauncherModel()242 private void initializeLauncherModel() { 243 ExecutorService fetchOrderExecutorService = Executors.newSingleThreadExecutor(); 244 fetchOrderExecutorService.execute(() -> { 245 //If the order file is deleted, we need to reset the flag 246 if (!mLauncherModel.doesFileExist() && mLauncherModel.isCustomized()) { 247 mLauncherModel.setCustomized(false); 248 mLauncherModel.setAppOrderRead(false); 249 } 250 mLauncherModel.updateAppsOrder(); 251 fetchOrderExecutorService.shutdown(); 252 }); 253 ExecutorService alphabetizeExecutorService = Executors.newSingleThreadExecutor(); 254 alphabetizeExecutorService.execute(() -> { 255 Set<String> appsToHide = mShowAllApps ? Collections.emptySet() : mHiddenApps; 256 mAppsInfo = AppLauncherUtils.getLauncherApps(getApplicationContext(), 257 appsToHide, 258 mMode.mAppTypes, 259 mMode.mOpenMediaCenter, 260 getSystemService(LauncherApps.class), 261 mCarPackageManager, 262 mPackageManager, 263 mCarMediaManager, 264 AppGridActivity.this, 265 mMirroringPackageName, 266 mMirroringIntentRedirect); 267 mLauncherModel.generateAlphabetizedAppOrder(mAppsInfo); 268 alphabetizeExecutorService.shutdown(); 269 }); 270 } 271 272 @Override onCreate(@ullable Bundle savedInstanceState)273 protected void onCreate(@Nullable Bundle savedInstanceState) { 274 // TODO (b/267548246) deprecate toolbar and find another way to hide debug apps 275 mShowToolbar = false; 276 if (mShowToolbar) { 277 setTheme(R.style.Theme_Launcher_AppGridActivity); 278 } else { 279 setTheme(R.style.Theme_Launcher_AppGridActivity_NoToolbar); 280 } 281 super.onCreate(savedInstanceState); 282 283 mPackageManager = getPackageManager(); 284 mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); 285 mLauncherModel = new ViewModelProvider(AppGridActivity.this, 286 new LauncherViewModelFactory(getFilesDir())).get( 287 LauncherViewModel.class); 288 mLauncherModel.getCurrentLauncher().observe( 289 AppGridActivity.this, new Observer<List<LauncherItem>>() { 290 @Override 291 public void onChanged(List<LauncherItem> launcherItems) { 292 mAdapter.setLauncherItems(launcherItems); 293 mNextScrollDestination = mSnapCallback.getSnapPosition(); 294 updateScrollState(); 295 if (mMode == Mode.ALL_APPS) { 296 mLauncherModel.maybeSaveAppsOrder(); 297 } 298 } 299 } 300 ); 301 mCar = Car.createCar(this, mCarConnectionListener); 302 mHiddenApps.addAll(Arrays.asList(getResources().getStringArray(R.array.hidden_apps))); 303 setContentView(R.layout.app_grid_activity); 304 updateMode(); 305 306 ServiceConnection mirroringConnectionListener = new ServiceConnection() { 307 @Override 308 public void onServiceConnected(ComponentName name, IBinder service) { 309 Log.d(TAG, "Mirroring service connected"); 310 mMirroringService = new Messenger(service); 311 mMessenger = new Messenger(new IncomingHandler(Looper.getMainLooper())); 312 Message msg = Message.obtain(null, getResources() 313 .getInteger(R.integer.config_msg_register_mirroring_pkg_code)); 314 msg.replyTo = mMessenger; 315 try { 316 mMirroringService.send(msg); 317 } catch (RemoteException e) { 318 Log.d(TAG, "Exception sending message to mirroring service: " + e); 319 } 320 } 321 322 @Override 323 public void onServiceDisconnected(ComponentName name) { 324 Log.d(TAG, "Mirroring service disconnected"); 325 mMirroringPackageName = null; 326 mMirroringIntentRedirect = null; 327 } 328 }; 329 330 // Bind to service that will inform about apps that are being mirrored 331 try { 332 Intent intent = new Intent(); 333 intent.setComponent(new ComponentName( 334 getString(R.string.config_msg_mirroring_service_pkg_name), 335 getString(R.string.config_msg_mirroring_service_class_name))); 336 if (mPackageManager.resolveService(intent, /* flags = */ 0) != null) { 337 bindService(intent, mirroringConnectionListener, 338 BIND_AUTO_CREATE | BIND_IMPORTANT); 339 } 340 } catch (SecurityException e) { 341 Log.e(TAG, "Error binding to mirroring service: " + e); 342 } 343 344 if (mShowToolbar) { 345 ToolbarController toolbar = CarUi.requireToolbar(this); 346 347 toolbar.setNavButtonMode(NavButtonMode.CLOSE); 348 349 if (Build.IS_DEBUGGABLE) { 350 toolbar.setMenuItems(Collections.singletonList(MenuItem.builder(this) 351 .setDisplayBehavior(MenuItem.DisplayBehavior.NEVER) 352 .setTitle(R.string.hide_debug_apps) 353 .setOnClickListener(i -> { 354 mShowAllApps = !mShowAllApps; 355 i.setTitle(mShowAllApps 356 ? R.string.hide_debug_apps 357 : R.string.show_debug_apps); 358 }) 359 .build())); 360 } 361 } 362 363 mSnapCallback = new AppGridPageSnapper.AppGridPageSnapCallback(this); 364 mDragCallback = new AppItemViewHolder.AppItemDragCallback(this); 365 366 mNumOfCols = getResources().getInteger(R.integer.car_app_selector_column_number); 367 mNumOfRows = getResources().getInteger(R.integer.car_app_selector_row_number); 368 mAppGridDragController = new AppGridDragController(); 369 mOffPageHoverBeforeScrollMs = getResources().getInteger( 370 R.integer.ms_off_page_hover_before_scroll); 371 372 mPageOrientation = getResources().getBoolean(R.bool.use_vertical_app_grid) 373 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL; 374 375 mRecyclerView = requireViewById(R.id.apps_grid); 376 mRecyclerView.setFocusable(false); 377 mLayoutManager = new AppGridLayoutManager(this, mNumOfCols, mNumOfRows, mPageOrientation); 378 mRecyclerView.setLayoutManager(mLayoutManager); 379 380 AppGridPageSnapper pageSnapper = new AppGridPageSnapper( 381 this, 382 mNumOfCols, 383 mNumOfRows, 384 mSnapCallback); 385 pageSnapper.attachToRecyclerView(mRecyclerView); 386 387 mRecyclerView.setItemAnimator(new AppGridItemAnimator()); 388 389 // hide the default scrollbar and replace it with a visual page indicator 390 mRecyclerView.setVerticalScrollBarEnabled(false); 391 mRecyclerView.setHorizontalScrollBarEnabled(false); 392 mRecyclerView.addOnScrollListener(new AppGridOnScrollListener()); 393 394 // TODO: (b/271637411) move this to be contained in a scroll controller 395 mPageIndicator = requireViewById(R.id.page_indicator); 396 FrameLayout pageIndicatorContainer = requireViewById(R.id.page_indicator_container); 397 mPageIndicator.setContainer(pageIndicatorContainer); 398 399 // recycler view is set to LTR to prevent layout manager from reassigning layout direction. 400 // instead, PageIndexinghelper will determine the grid index based on the system layout 401 // direction and provide LTR mapping at adapter level. 402 mRecyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); 403 pageIndicatorContainer.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); 404 405 // we create but do not attach the adapter to recyclerview until view tree layout is 406 // complete and the total size of the app grid is measureable. 407 mAdapter = new AppGridAdapter(this, mNumOfCols, mNumOfRows, 408 /* dataModel */ mLauncherModel, /* dragCallback */ mDragCallback, 409 /* snapCallback */ mSnapCallback); 410 mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 411 @Override 412 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 413 // scroll state will need to be updated after item has been dropped 414 mNextScrollDestination = mSnapCallback.getSnapPosition(); 415 updateScrollState(); 416 } 417 }); 418 mRecyclerView.setAdapter(mAdapter); 419 420 // set drag listener and global layout listener, which will dynamically adjust app grid 421 // height and width depending on device screen size. 422 if (getResources().getBoolean(R.bool.config_allow_reordering)) { 423 mRecyclerView.setOnDragListener(new AppGridDragListener()); 424 } 425 426 // since some measurements for window size may not be available yet during onCreate or may 427 // later change, we add a listener that redraws the app grid when window size changes. 428 LinearLayout windowBackground = requireViewById(R.id.apps_grid_background); 429 windowBackground.setOrientation( 430 isHorizontal() ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); 431 PaginationController.DimensionUpdateCallback dimensionUpdateCallback = 432 new PaginationController.DimensionUpdateCallback(); 433 dimensionUpdateCallback.addListener(mRecyclerView); 434 dimensionUpdateCallback.addListener(mPageIndicator); 435 dimensionUpdateCallback.addListener(this); 436 mPaginationController = new PaginationController(windowBackground, dimensionUpdateCallback); 437 } 438 439 @Override onNewIntent(Intent intent)440 protected void onNewIntent(Intent intent) { 441 super.onNewIntent(intent); 442 setIntent(intent); 443 updateMode(); 444 if (mCar.isConnected()) { 445 initializeLauncherModel(); 446 } 447 } 448 449 @Override onDestroy()450 protected void onDestroy() { 451 if (mCar != null && mCar.isConnected()) { 452 mCar.disconnect(); 453 mCar = null; 454 } 455 456 if (mMirroringService != null) { 457 Message msg = Message.obtain(null, 458 getResources().getInteger(R.integer.config_msg_unregister_mirroring_pkg_code)); 459 msg.replyTo = mMessenger; 460 try { 461 mMirroringService.send(msg); 462 } catch (RemoteException e) { 463 Log.d(TAG, "Exception sending message to mirroring service: " + e); 464 } 465 } 466 467 super.onDestroy(); 468 } 469 updateMode()470 private void updateMode() { 471 mMode = parseMode(getIntent()); 472 setTitle(mMode.mTitleStringId); 473 if (mShowToolbar) { 474 CarUi.requireToolbar(this).setTitle(mMode.mTitleStringId); 475 } 476 } 477 478 @VisibleForTesting isHorizontal()479 boolean isHorizontal() { 480 return AppGridConstants.isHorizontal(mPageOrientation); 481 } 482 483 /** 484 * Note: This activity is exported, meaning that it might receive intents from any source. 485 * Intent data parsing must be extra careful. 486 */ 487 @NonNull parseMode(@ullable Intent intent)488 private Mode parseMode(@Nullable Intent intent) { 489 String mode = intent != null ? intent.getStringExtra(MODE_INTENT_EXTRA) : null; 490 try { 491 return mode != null ? Mode.valueOf(mode) : Mode.ALL_APPS; 492 } catch (IllegalArgumentException e) { 493 throw new IllegalArgumentException("Received invalid mode: " + mode, e); 494 } 495 } 496 497 @Override onResume()498 protected void onResume() { 499 super.onResume(); 500 updateScrollState(); 501 mAdapter.setLayoutDirection(getResources().getConfiguration().getLayoutDirection()); 502 } 503 504 @Override onDimensionsUpdated(PageMeasurementHelper.PageDimensions pageDimens, PageMeasurementHelper.GridDimensions gridDimens)505 public void onDimensionsUpdated(PageMeasurementHelper.PageDimensions pageDimens, 506 PageMeasurementHelper.GridDimensions gridDimens) { 507 // TODO(b/271637411): move this method into a scroll controller 508 mAppGridMarginHorizontal = pageDimens.marginHorizontalPx; 509 mAppGridMarginVertical = pageDimens.marginVerticalPx; 510 mAppGridWidth = gridDimens.gridWidthPx; 511 mAppGridHeight = gridDimens.gridHeightPx; 512 } 513 514 /** 515 * Updates the scroll state after receiving data changes, such as new apps being added or 516 * reordered, and when user returns to launcher onResume. 517 * 518 * Additionally, notify page indicator to handle resizing in case new app addition creates a 519 * new page or deleted a page. 520 */ updateScrollState()521 void updateScrollState() { 522 // TODO(b/271637411): move this method into a scroll controller 523 // to calculate how many pages we need to offset, we use the scroll offset anchor position 524 // as item count and map to the page which the anchor is on. 525 int offsetPageCount = mAdapter.getPageCount(mNextScrollDestination + 1) - 1; 526 mRecyclerView.suppressLayout(false); 527 mCurrentScrollOffset = offsetPageCount * (isHorizontal() 528 ? (mAppGridWidth + 2 * mAppGridMarginHorizontal) 529 : (mAppGridHeight + 2 * mAppGridMarginVertical)); 530 mLayoutManager.scrollToPositionWithOffset(/* position */ 531 offsetPageCount * mNumOfRows * mNumOfCols, /* offset */ 0); 532 533 mPageIndicator.updateOffset(mCurrentScrollOffset); 534 mPageIndicator.updatePageCount(mAdapter.getPageCount()); 535 } 536 537 @Override onStart()538 protected void onStart() { 539 super.onStart(); 540 // register broadcast receiver for package installation and uninstallation 541 mInstallUninstallReceiver = new AppInstallUninstallReceiver(); 542 IntentFilter filter = new IntentFilter(); 543 filter.addAction(Intent.ACTION_PACKAGE_ADDED); 544 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 545 filter.addAction(Intent.ACTION_PACKAGE_REPLACED); 546 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 547 filter.addDataScheme("package"); 548 registerReceiver(mInstallUninstallReceiver, filter); 549 550 // Connect to car service 551 mCar.connect(); 552 } 553 554 @Override onStop()555 protected void onStop() { 556 super.onStop(); 557 // disconnect from app install/uninstall receiver 558 if (mInstallUninstallReceiver != null) { 559 unregisterReceiver(mInstallUninstallReceiver); 560 mInstallUninstallReceiver = null; 561 } 562 // disconnect from car listeners 563 try { 564 if (mCarUxRestrictionsManager != null) { 565 mCarUxRestrictionsManager.unregisterListener(); 566 } 567 } catch (CarNotConnectedException e) { 568 Log.e(TAG, "Error unregistering listeners", e); 569 } 570 if (mCar != null) { 571 mCar.disconnect(); 572 } 573 } 574 575 @Override onPause()576 protected void onPause() { 577 dismissForceStopMenus(); 578 super.onPause(); 579 } 580 581 @Override onSnapToPosition(int position)582 public void onSnapToPosition(int position) { 583 mNextScrollDestination = position; 584 } 585 586 @Override onItemLongPressed(boolean isLongPressed)587 public void onItemLongPressed(boolean isLongPressed) { 588 // after the user long presses the app icon, scrolling should be disabled until long press 589 // is canceled as to allow MotionEvent to be interpreted as attempt to drag the app icon. 590 mRecyclerView.suppressLayout(isLongPressed); 591 } 592 593 @Override onItemSelected(int gridPositionFrom)594 public void onItemSelected(int gridPositionFrom) { 595 mIsCurrentlyDragging = true; 596 mLayoutManager.setShouldLayoutChildren(false); 597 mAdapter.setDragStartPoint(gridPositionFrom); 598 dismissShortcutPopup(); 599 } 600 601 @Override onItemDragged()602 public void onItemDragged() { 603 mAppGridDragController.cancelDelayedPageFling(); 604 } 605 606 @Override onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection)607 public void onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection) { 608 if (mAdapter.getOffsetBoundDirection(gridPosition) == exitDirection) { 609 mAppGridDragController.postDelayedPageFling(exitDirection); 610 } 611 } 612 613 @Override onItemDropped(int gridPositionFrom, int gridPositionTo)614 public void onItemDropped(int gridPositionFrom, int gridPositionTo) { 615 mLayoutManager.setShouldLayoutChildren(true); 616 mAdapter.moveAppItem(gridPositionFrom, gridPositionTo); 617 } 618 619 /** 620 * Note that in order to obtain usage stats from the previous boot, 621 * the device must have gone through a clean shut down process. 622 */ getMostRecentApps(LauncherAppsInfo appsInfo)623 private List<AppMetaData> getMostRecentApps(LauncherAppsInfo appsInfo) { 624 ArrayList<AppMetaData> apps = new ArrayList<>(); 625 if (appsInfo.isEmpty()) { 626 return apps; 627 } 628 629 // get the usage stats starting from 1 year ago with a INTERVAL_YEARLY granularity 630 // returning entries like: 631 // "During 2017 App A is last used at 2017/12/15 18:03" 632 // "During 2017 App B is last used at 2017/6/15 10:00" 633 // "During 2018 App A is last used at 2018/1/1 15:12" 634 List<UsageStats> stats = 635 mUsageStatsManager.queryUsageStats( 636 UsageStatsManager.INTERVAL_YEARLY, 637 System.currentTimeMillis() - DateUtils.YEAR_IN_MILLIS, 638 System.currentTimeMillis()); 639 640 if (stats == null || stats.size() == 0) { 641 return apps; // empty list 642 } 643 644 stats.sort(new LastTimeUsedComparator()); 645 646 int currentIndex = 0; 647 int itemsAdded = 0; 648 int statsSize = stats.size(); 649 int itemCount = Math.min(mNumOfCols, statsSize); 650 while (itemsAdded < itemCount && currentIndex < statsSize) { 651 UsageStats usageStats = stats.get(currentIndex); 652 String packageName = usageStats.mPackageName; 653 currentIndex++; 654 655 // do not include self 656 if (packageName.equals(getPackageName())) { 657 continue; 658 } 659 660 // TODO(b/136222320): UsageStats is obtained per package, but a package may contain 661 // multiple media services. We need to find a way to get the usage stats per service. 662 ComponentName componentName = AppLauncherUtils.getMediaSource(mPackageManager, 663 packageName); 664 // Exempt media services from background and launcher checks 665 if (!appsInfo.isMediaService(componentName)) { 666 // do not include apps that only ran in the background 667 if (usageStats.getTotalTimeInForeground() == 0) { 668 continue; 669 } 670 671 // do not include apps that don't support starting from launcher 672 Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); 673 if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { 674 continue; 675 } 676 } 677 678 AppMetaData app = appsInfo.getAppMetaData(componentName); 679 // Prevent duplicated entries 680 // e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00 681 if (app != null && !apps.contains(app)) { 682 apps.add(app); 683 itemsAdded++; 684 } 685 } 686 return apps; 687 } 688 689 @Override onCarUiInsetsChanged(Insets insets)690 public void onCarUiInsetsChanged(Insets insets) { 691 requireViewById(R.id.apps_grid) 692 .setPadding(0, insets.getTop(), 0, insets.getBottom()); 693 FocusArea focusArea = requireViewById(R.id.focus_area); 694 focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom()); 695 focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom()); 696 697 requireViewById(android.R.id.content) 698 .setPadding(insets.getLeft(), 0, insets.getRight(), 0); 699 } 700 701 @Override onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup)702 public void onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup) { 703 sCarUiShortcutsPopup = carUiShortcutsPopup; 704 } 705 706 @Override onShortcutsItemClick(String packageName, CharSequence displayName, boolean allowStopApp)707 public void onShortcutsItemClick(String packageName, CharSequence displayName, 708 boolean allowStopApp) { 709 AlertDialogBuilder builder = new AlertDialogBuilder(this) 710 .setTitle(R.string.app_launcher_stop_app_dialog_title); 711 712 if (allowStopApp) { 713 builder.setMessage(R.string.app_launcher_stop_app_dialog_text) 714 .setPositiveButton(android.R.string.ok, 715 (d, w) -> AppLauncherUtils.forceStop(packageName, AppGridActivity.this, 716 displayName, mCarMediaManager, mAppsInfo.getMediaServices(), 717 this)) 718 .setNegativeButton(android.R.string.cancel, /* onClickListener= */ null); 719 } else { 720 builder.setMessage(R.string.app_launcher_stop_app_cant_stop_text) 721 .setNeutralButton(android.R.string.ok, /* onClickListener= */ null); 722 } 723 mStopAppAlertDialog = builder.show(); 724 } 725 726 @Override onStopAppSuccess(String message)727 public void onStopAppSuccess(String message) { 728 Toast.makeText(this, message, Toast.LENGTH_LONG).show(); 729 } 730 dismissShortcutPopup()731 private void dismissShortcutPopup() { 732 // TODO (b/268563442): shortcut popup is set to be static since its 733 // sometimes recreated when taskview is present, find out why 734 if (sCarUiShortcutsPopup != null) { 735 sCarUiShortcutsPopup.dismiss(); 736 sCarUiShortcutsPopup = null; 737 } 738 } 739 dismissForceStopMenus()740 private void dismissForceStopMenus() { 741 if (sCarUiShortcutsPopup != null) { 742 sCarUiShortcutsPopup.dismissImmediate(); 743 sCarUiShortcutsPopup = null; 744 } 745 if (mStopAppAlertDialog != null) { 746 mStopAppAlertDialog.dismiss(); 747 } 748 } 749 750 /** 751 * Comparator for {@link UsageStats} that sorts the list by the "last time used" property 752 * in descending order. 753 */ 754 private static class LastTimeUsedComparator implements Comparator<UsageStats> { 755 @Override compare(UsageStats stat1, UsageStats stat2)756 public int compare(UsageStats stat1, UsageStats stat2) { 757 Long time1 = stat1.getLastTimeUsed(); 758 Long time2 = stat2.getLastTimeUsed(); 759 return time2.compareTo(time1); 760 } 761 } 762 763 private class AppInstallUninstallReceiver extends BroadcastReceiver { 764 @Override onReceive(Context context, Intent intent)765 public void onReceive(Context context, Intent intent) { 766 String packageName = intent.getData().getSchemeSpecificPart(); 767 if (TextUtils.isEmpty(packageName)) { 768 Log.e(TAG, "System sent an empty app install/uninstall broadcast"); 769 return; 770 } 771 // TODO b/256684061: find better way to get AppInfo from package name. 772 initializeLauncherModel(); 773 } 774 } 775 776 private class AppGridOnScrollListener extends RecyclerView.OnScrollListener { 777 @Override onScrolled(@onNull RecyclerView recyclerView, int dx, int dy)778 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 779 mCurrentScrollOffset = mCurrentScrollOffset + (isHorizontal() ? dx : dy); 780 mPageIndicator.updateOffset(mCurrentScrollOffset); 781 } 782 783 @Override onScrollStateChanged(@onNull RecyclerView recyclerView, int newState)784 public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 785 mCurrentScrollState = newState; 786 mSnapCallback.setScrollState(mCurrentScrollState); 787 switch (newState) { 788 case RecyclerView.SCROLL_STATE_DRAGGING: 789 if (!mIsCurrentlyDragging) { 790 mDragCallback.cancelDragTasks(); 791 } 792 dismissShortcutPopup(); 793 mPageIndicator.animateAppearance(); 794 break; 795 796 case RecyclerView.SCROLL_STATE_SETTLING: 797 mPageIndicator.animateAppearance(); 798 break; 799 800 case RecyclerView.SCROLL_STATE_IDLE: 801 if (mIsCurrentlyDragging) { 802 mLayoutManager.setShouldLayoutChildren(false); 803 } 804 mPageIndicator.animateFading(); 805 // in case the recyclerview was scrolled by rotary input, we need to handle 806 // focusing the correct element: either on the first or last element on page 807 mRecyclerView.maybeHandleRotaryFocus(); 808 } 809 } 810 } 811 812 private class AppGridDragController { 813 // TODO: (b/271320404) move DragController to separate directory called dragndrop and 814 // migrate logic this class and AppItemViewHolder there. 815 private final Handler mHandler; 816 AppGridDragController()817 AppGridDragController() { 818 mHandler = new Handler(getMainLooper()); 819 } 820 cancelDelayedPageFling()821 void cancelDelayedPageFling() { 822 mHandler.removeCallbacksAndMessages(null); 823 } 824 postDelayedPageFling(@ppItemBoundDirection int exitDirection)825 void postDelayedPageFling(@AppItemBoundDirection int exitDirection) { 826 boolean scrollToNextPage = isHorizontal() 827 ? exitDirection == AppItemBoundDirection.RIGHT 828 : exitDirection == AppItemBoundDirection.BOTTOM; 829 mHandler.removeCallbacksAndMessages(null); 830 mHandler.postDelayed(new Runnable() { 831 public void run() { 832 if (mCurrentScrollState == RecyclerView.SCROLL_STATE_IDLE) { 833 mAdapter.updatePageScrollDestination(scrollToNextPage); 834 mNextScrollDestination = mSnapCallback.getSnapPosition(); 835 836 mLayoutManager.setShouldLayoutChildren(true); 837 mRecyclerView.smoothScrollToPosition(mNextScrollDestination); 838 } 839 // another delayed scroll will be queued to enable the user to input multiple 840 // page scrolls by holding the recyclerview at the app grid margin 841 postDelayedPageFling(exitDirection); 842 } 843 }, mOffPageHoverBeforeScrollMs); 844 } 845 } 846 847 /** 848 * Private onDragListener for handling dispatching off page scroll event when user holds the app 849 * icon at the page margin. 850 */ 851 private class AppGridDragListener implements View.OnDragListener { 852 @Override onDrag(View v, DragEvent event)853 public boolean onDrag(View v, DragEvent event) { 854 int action = event.getAction(); 855 if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) { 856 mIsCurrentlyDragging = false; 857 mAppGridDragController.cancelDelayedPageFling(); 858 mDragCallback.resetCallbackState(); 859 mLayoutManager.setShouldLayoutChildren(true); 860 if (action == DragEvent.ACTION_DROP) { 861 return false; 862 } else { 863 animateDropEnded(event.getDragSurface()); 864 } 865 } 866 return true; 867 } 868 } 869 animateDropEnded(@ullable SurfaceControl dragSurface)870 private void animateDropEnded(@Nullable SurfaceControl dragSurface) { 871 if (dragSurface == null) { 872 return; 873 } 874 // update default animation for the drag shadow after user lifts their finger 875 SurfaceControl.Transaction txn = new SurfaceControl.Transaction(); 876 // set an animator to animate a delay before clearing the dragSurface 877 ValueAnimator delayedDismissAnimator = ValueAnimator.ofFloat(0f, 1f); 878 delayedDismissAnimator.setStartDelay( 879 getResources().getInteger(R.integer.ms_drop_animation_delay)); 880 delayedDismissAnimator.addUpdateListener( 881 new ValueAnimator.AnimatorUpdateListener() { 882 @Override 883 public void onAnimationUpdate(ValueAnimator animation) { 884 txn.setAlpha(dragSurface, 0); 885 txn.apply(); 886 } 887 }); 888 delayedDismissAnimator.start(); 889 } 890 891 @VisibleForTesting setCarUxRestrictionsManager(CarUxRestrictionsManager carUxRestrictionsManager)892 void setCarUxRestrictionsManager(CarUxRestrictionsManager carUxRestrictionsManager) { 893 mCarUxRestrictionsManager = carUxRestrictionsManager; 894 } 895 896 @VisibleForTesting setPageIndicator(PageIndicator pageIndicator)897 void setPageIndicator(PageIndicator pageIndicator) { 898 mPageIndicator = pageIndicator; 899 } 900 901 class IncomingHandler extends Handler { 902 903 int mSendMirroringPkgCode = getResources() 904 .getInteger(R.integer.config_msg_send_mirroring_pkg_code); 905 String mMirroringPkgNameKey = getString(R.string.config_msg_mirroring_pkg_name_key); 906 String mMirroringRedirectUriKey = getString(R.string.config_msg_mirroring_redirect_uri_key); 907 IncomingHandler(Looper looper)908 IncomingHandler(Looper looper) { 909 super(looper); 910 } 911 912 @Override handleMessage(Message msg)913 public void handleMessage(Message msg) { 914 Log.d(TAG, "Message received: " + msg); 915 if (msg.what 916 == mSendMirroringPkgCode) { 917 Bundle bundle = (Bundle) msg.obj; 918 mMirroringPackageName = 919 bundle.getString(mMirroringPkgNameKey); 920 Log.d(TAG, "message received with package name = " + mMirroringPackageName); 921 try { 922 mMirroringIntentRedirect = Intent.parseUri( 923 bundle.getString(mMirroringRedirectUriKey), 924 URI_INTENT_SCHEME); 925 Log.d(TAG, "intent is: " + mMirroringIntentRedirect); 926 mLauncherModel.updateMirroringItem(mMirroringPackageName, 927 mMirroringIntentRedirect); 928 } catch (URISyntaxException e) { 929 Log.d(TAG, "Error parsing mirroring redirect intent " + e); 930 } 931 } else { 932 super.handleMessage(msg); 933 } 934 } 935 } 936 } 937