1 /* 2 * Copyright (C) 2023 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 androidx.lifecycle.FlowLiveDataConversions.asLiveData; 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.hidden.apis.HiddenApiAccess.getDragSurface; 24 25 import static java.lang.annotation.RetentionPolicy.SOURCE; 26 27 import android.animation.ValueAnimator; 28 import android.car.Car; 29 import android.car.content.pm.CarPackageManager; 30 import android.car.drivingstate.CarUxRestrictionsManager; 31 import android.car.media.CarMediaManager; 32 import android.content.Intent; 33 import android.content.pm.LauncherApps; 34 import android.content.pm.PackageManager; 35 import android.media.session.MediaSessionManager; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.util.Log; 39 import android.view.DragEvent; 40 import android.view.SurfaceControl; 41 import android.view.View; 42 import android.view.ViewTreeObserver; 43 import android.widget.FrameLayout; 44 import android.widget.LinearLayout; 45 46 import androidx.annotation.IntDef; 47 import androidx.annotation.NonNull; 48 import androidx.annotation.Nullable; 49 import androidx.annotation.StringRes; 50 import androidx.annotation.VisibleForTesting; 51 import androidx.appcompat.app.AppCompatActivity; 52 import androidx.lifecycle.ViewModelProvider; 53 import androidx.recyclerview.widget.RecyclerView; 54 55 import com.android.car.carlauncher.datasources.AppOrderDataSource; 56 import com.android.car.carlauncher.datasources.AppOrderProtoDataSourceImpl; 57 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource; 58 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSourceImpl; 59 import com.android.car.carlauncher.datasources.LauncherActivitiesDataSource; 60 import com.android.car.carlauncher.datasources.LauncherActivitiesDataSourceImpl; 61 import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSource; 62 import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSourceImpl; 63 import com.android.car.carlauncher.datasources.UXRestrictionDataSource; 64 import com.android.car.carlauncher.datasources.UXRestrictionDataSourceImpl; 65 import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSource; 66 import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSourceImpl; 67 import com.android.car.carlauncher.datasources.restricted.TosDataSource; 68 import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl; 69 import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource; 70 import com.android.car.carlauncher.pagination.PageMeasurementHelper; 71 import com.android.car.carlauncher.pagination.PaginationController; 72 import com.android.car.carlauncher.recyclerview.AppGridAdapter; 73 import com.android.car.carlauncher.recyclerview.AppGridItemAnimator; 74 import com.android.car.carlauncher.recyclerview.AppGridLayoutManager; 75 import com.android.car.carlauncher.recyclerview.AppItemViewHolder; 76 import com.android.car.carlauncher.repositories.AppGridRepository; 77 import com.android.car.carlauncher.repositories.AppGridRepositoryImpl; 78 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory; 79 import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory; 80 import com.android.car.ui.core.CarUi; 81 import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup; 82 import com.android.car.ui.toolbar.MenuItem; 83 import com.android.car.ui.toolbar.NavButtonMode; 84 import com.android.car.ui.toolbar.ToolbarController; 85 86 import java.lang.annotation.Retention; 87 import java.util.Collections; 88 89 import kotlin.Unit; 90 import kotlinx.coroutines.CoroutineDispatcher; 91 import kotlinx.coroutines.Dispatchers; 92 93 /** 94 * Launcher activity that shows a grid of apps. 95 */ 96 public class AppGridActivity extends AppCompatActivity implements 97 AppGridPageSnapper.PageSnapListener, AppItemViewHolder.AppItemDragListener, 98 PaginationController.DimensionUpdateListener, 99 AppGridAdapter.AppGridAdapterListener { 100 private static final String TAG = "AppGridActivity"; 101 private static final boolean DEBUG_BUILD = false; 102 private static final String MODE_INTENT_EXTRA = "com.android.car.carlauncher.mode"; 103 private static CarUiShortcutsPopup sCarUiShortcutsPopup; 104 105 private boolean mShowAllApps = true; 106 private boolean mShowToolbar = true; 107 private Car mCar; 108 private Mode mMode; 109 private AppGridAdapter mAdapter; 110 private AppGridRecyclerView mRecyclerView; 111 private PageIndicator mPageIndicator; 112 private AppGridLayoutManager mLayoutManager; 113 private boolean mIsCurrentlyDragging; 114 private long mOffPageHoverBeforeScrollMs; 115 private Banner mBanner; 116 117 private AppGridDragController mAppGridDragController; 118 private PaginationController mPaginationController; 119 120 private int mNumOfRows; 121 private int mNumOfCols; 122 private int mAppGridMarginHorizontal; 123 private int mAppGridMarginVertical; 124 private int mAppGridWidth; 125 private int mAppGridHeight; 126 @PageOrientation 127 private int mPageOrientation; 128 129 private int mCurrentScrollOffset; 130 private int mCurrentScrollState; 131 private int mNextScrollDestination; 132 private AppGridPageSnapper.AppGridPageSnapCallback mSnapCallback; 133 private AppItemViewHolder.AppItemDragCallback mDragCallback; 134 private BackgroundAnimationHelper mBackgroundAnimationHelper; 135 136 private AppGridViewModel mAppGridViewModel; 137 138 @Retention(SOURCE) 139 @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES}) 140 @interface AppTypes {} 141 static final int APP_TYPE_LAUNCHABLES = 1; 142 static final int APP_TYPE_MEDIA_SERVICES = 2; 143 144 public enum Mode { 145 ALL_APPS(R.string.app_launcher_title_all_apps, 146 APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES, 147 true), 148 MEDIA_ONLY(R.string.app_launcher_title_media_only, 149 APP_TYPE_MEDIA_SERVICES, 150 true), 151 MEDIA_POPUP(R.string.app_launcher_title_media_only, 152 APP_TYPE_MEDIA_SERVICES, 153 false), 154 ; 155 @StringRes 156 public final int mTitleStringId; 157 @AppTypes 158 public final int mAppTypes; 159 public final boolean mOpenMediaCenter; 160 Mode(@tringRes int titleStringId, @AppTypes int appTypes, boolean openMediaCenter)161 Mode(@StringRes int titleStringId, @AppTypes int appTypes, 162 boolean openMediaCenter) { 163 mTitleStringId = titleStringId; 164 mAppTypes = appTypes; 165 mOpenMediaCenter = openMediaCenter; 166 } 167 } 168 169 /** 170 * Updates the state of the app grid components depending on the driving state. 171 */ handleDistractionOptimization(boolean requiresDistractionOptimization)172 private void handleDistractionOptimization(boolean requiresDistractionOptimization) { 173 mAdapter.setIsDistractionOptimizationRequired(requiresDistractionOptimization); 174 if (requiresDistractionOptimization) { 175 // if the user start driving while drag is in action, we cancel existing drag operations 176 if (mIsCurrentlyDragging) { 177 mIsCurrentlyDragging = false; 178 mLayoutManager.setShouldLayoutChildren(true); 179 mRecyclerView.cancelDragAndDrop(); 180 } 181 dismissShortcutPopup(); 182 } 183 } 184 185 @Override onCreate(@ullable Bundle savedInstanceState)186 protected void onCreate(@Nullable Bundle savedInstanceState) { 187 // TODO (b/267548246) deprecate toolbar and find another way to hide debug apps 188 mShowToolbar = false; 189 if (mShowToolbar) { 190 setTheme(R.style.Theme_Launcher_AppGridActivity); 191 } else { 192 setTheme(R.style.Theme_Launcher_AppGridActivity_NoToolbar); 193 } 194 super.onCreate(savedInstanceState); 195 196 mCar = Car.createCar(this); 197 setContentView(R.layout.app_grid_activity); 198 updateMode(); 199 initViewModel(); 200 201 if (mShowToolbar) { 202 ToolbarController toolbar = CarUi.requireToolbar(this); 203 204 toolbar.setNavButtonMode(NavButtonMode.CLOSE); 205 206 if (DEBUG_BUILD) { 207 toolbar.setMenuItems(Collections.singletonList(MenuItem.builder(this) 208 .setDisplayBehavior(MenuItem.DisplayBehavior.NEVER) 209 .setTitle(R.string.hide_debug_apps) 210 .setOnClickListener(i -> { 211 mShowAllApps = !mShowAllApps; 212 i.setTitle(mShowAllApps 213 ? R.string.hide_debug_apps 214 : R.string.show_debug_apps); 215 }) 216 .build())); 217 } 218 } 219 220 mSnapCallback = new AppGridPageSnapper.AppGridPageSnapCallback(this); 221 mDragCallback = new AppItemViewHolder.AppItemDragCallback(this); 222 223 mNumOfCols = getResources().getInteger(R.integer.car_app_selector_column_number); 224 mNumOfRows = getResources().getInteger(R.integer.car_app_selector_row_number); 225 mAppGridDragController = new AppGridDragController(); 226 mOffPageHoverBeforeScrollMs = getResources().getInteger( 227 R.integer.ms_off_page_hover_before_scroll); 228 229 mPageOrientation = getResources().getBoolean(R.bool.use_vertical_app_grid) 230 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL; 231 232 mRecyclerView = requireViewById(R.id.apps_grid); 233 mRecyclerView.setFocusable(false); 234 mLayoutManager = new AppGridLayoutManager(this, mNumOfCols, mNumOfRows, mPageOrientation); 235 mRecyclerView.setLayoutManager(mLayoutManager); 236 237 AppGridPageSnapper pageSnapper = new AppGridPageSnapper( 238 this, 239 mNumOfCols, 240 mNumOfRows, 241 mSnapCallback); 242 pageSnapper.attachToRecyclerView(mRecyclerView); 243 244 mRecyclerView.setItemAnimator(new AppGridItemAnimator()); 245 246 // hide the default scrollbar and replace it with a visual page indicator 247 mRecyclerView.setVerticalScrollBarEnabled(false); 248 mRecyclerView.setHorizontalScrollBarEnabled(false); 249 mRecyclerView.addOnScrollListener(new AppGridOnScrollListener()); 250 251 // TODO: (b/271637411) move this to be contained in a scroll controller 252 mPageIndicator = requireViewById(R.id.page_indicator); 253 FrameLayout pageIndicatorContainer = requireViewById(R.id.page_indicator_container); 254 mPageIndicator.setContainer(pageIndicatorContainer); 255 256 // recycler view is set to LTR to prevent layout manager from reassigning layout direction. 257 // instead, PageIndexinghelper will determine the grid index based on the system layout 258 // direction and provide LTR mapping at adapter level. 259 mRecyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); 260 pageIndicatorContainer.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); 261 262 // we create but do not attach the adapter to recyclerview until view tree layout is 263 // complete and the total size of the app grid is measureable. 264 mAdapter = new AppGridAdapter(this, mNumOfCols, mNumOfRows, 265 /* dragCallback */ mDragCallback, 266 /* snapCallback */ mSnapCallback, this, mMode); 267 268 mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 269 @Override 270 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 271 // scroll state will need to be updated after item has been dropped 272 mNextScrollDestination = mSnapCallback.getSnapPosition(); 273 updateScrollState(); 274 } 275 }); 276 mRecyclerView.setAdapter(mAdapter); 277 278 asLiveData(mAppGridViewModel.getAppList()).observe(this, 279 appItems -> { 280 mAdapter.setLauncherItems(appItems); 281 mNextScrollDestination = mSnapCallback.getSnapPosition(); 282 updateScrollState(); 283 }); 284 285 asLiveData(mAppGridViewModel.requiresDistractionOptimization()).observe(this, 286 uxRestrictions -> { 287 handleDistractionOptimization(uxRestrictions); 288 }); 289 290 // set drag listener and global layout listener, which will dynamically adjust app grid 291 // height and width depending on device screen size. 292 if (getResources().getBoolean(R.bool.config_allow_reordering)) { 293 mRecyclerView.setOnDragListener(new AppGridDragListener()); 294 } 295 296 // since some measurements for window size may not be available yet during onCreate or may 297 // later change, we add a listener that redraws the app grid when window size changes. 298 LinearLayout windowBackground = requireViewById(R.id.apps_grid_background); 299 windowBackground.setOrientation( 300 isHorizontal() ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); 301 PaginationController.DimensionUpdateCallback dimensionUpdateCallback = 302 new PaginationController.DimensionUpdateCallback(); 303 dimensionUpdateCallback.addListener(mRecyclerView); 304 dimensionUpdateCallback.addListener(mPageIndicator); 305 dimensionUpdateCallback.addListener(this); 306 mPaginationController = new PaginationController(windowBackground, dimensionUpdateCallback); 307 308 mBanner = requireViewById(R.id.tos_banner); 309 310 mBackgroundAnimationHelper = new BackgroundAnimationHelper(windowBackground, mBanner); 311 312 setupTosBanner(); 313 } 314 initViewModel()315 private void initViewModel() { 316 LauncherActivitiesDataSource launcherActivities = new LauncherActivitiesDataSourceImpl( 317 getSystemService(LauncherApps.class), 318 (broadcastReceiver, intentFilter) -> { 319 registerReceiver(broadcastReceiver, intentFilter); 320 return Unit.INSTANCE; 321 }, broadcastReceiver -> { 322 unregisterReceiver(broadcastReceiver); 323 return Unit.INSTANCE; 324 }, 325 android.os.Process.myUserHandle(), 326 getApplication().getResources(), 327 Dispatchers.getDefault() 328 ); 329 MediaTemplateAppsDataSource mediaTemplateApps = new MediaTemplateAppsDataSourceImpl( 330 getPackageManager(), 331 getApplication(), 332 Dispatchers.getDefault() 333 ); 334 335 DisabledAppsDataSource disabledApps = new DisabledAppsDataSourceImpl(getContentResolver(), 336 getPackageManager(), Dispatchers.getIO()); 337 TosDataSource tosApps = new TosDataSourceImpl(getContentResolver(), getPackageManager(), 338 Dispatchers.getIO()); 339 ControlCenterMirroringDataSource controlCenterMirroringDataSource = 340 new ControlCenterMirroringDataSourceImpl(getApplication().getResources(), 341 (intent, serviceConnection, flags) -> { 342 bindService(intent, serviceConnection, flags); 343 return Unit.INSTANCE; 344 }, 345 (serviceConnection) -> { 346 unbindService(serviceConnection); 347 return Unit.INSTANCE; 348 }, 349 getPackageManager(), 350 Dispatchers.getIO() 351 ); 352 UXRestrictionDataSource uxRestrictionDataSource = new UXRestrictionDataSourceImpl( 353 (CarUxRestrictionsManager) mCar.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE), 354 (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE), 355 getSystemService(MediaSessionManager.class), 356 getApplication().getResources(), 357 Dispatchers.getDefault() 358 ); 359 AppOrderDataSource appOrderDataSource = new AppOrderProtoDataSourceImpl( 360 new LauncherItemListSource(getFilesDir(), "order.data"), 361 Dispatchers.getIO() 362 ); 363 364 PackageManager packageManager = getPackageManager(); 365 AppLaunchProviderFactory launchProviderFactory = new AppLaunchProviderFactory( 366 (CarMediaManager) mCar.getCarManager(Car.CAR_MEDIA_SERVICE), 367 mMode.mOpenMediaCenter, 368 () -> { 369 finish(); 370 return Unit.INSTANCE; 371 }, 372 getPackageManager()); 373 AppShortcutsFactory appShortcutsFactory = new AppShortcutsFactory( 374 (CarMediaManager) mCar.getCarManager(Car.CAR_MEDIA_SERVICE), 375 Collections.emptySet(), 376 this::onShortcutsShow 377 ); 378 CoroutineDispatcher bgDispatcher = Dispatchers.getDefault(); 379 380 AppGridRepository repo = new AppGridRepositoryImpl(launcherActivities, mediaTemplateApps, 381 disabledApps, tosApps, controlCenterMirroringDataSource, uxRestrictionDataSource, 382 appOrderDataSource, packageManager, launchProviderFactory, appShortcutsFactory, 383 bgDispatcher); 384 385 mAppGridViewModel = new ViewModelProvider(this, 386 AppGridViewModel.Companion.provideFactory(repo, getApplication(), this, null)).get( 387 AppGridViewModel.class); 388 } 389 390 @Override onNewIntent(Intent intent)391 protected void onNewIntent(Intent intent) { 392 super.onNewIntent(intent); 393 setIntent(intent); 394 updateMode(); 395 } 396 397 @Override onDestroy()398 protected void onDestroy() { 399 if (mCar.isConnected()) { 400 mCar.disconnect(); 401 mCar = null; 402 } 403 super.onDestroy(); 404 } 405 updateMode()406 private void updateMode() { 407 mMode = parseMode(getIntent()); 408 setTitle(mMode.mTitleStringId); 409 if (mShowToolbar) { 410 CarUi.requireToolbar(this).setTitle(mMode.mTitleStringId); 411 } 412 } 413 414 @VisibleForTesting isHorizontal()415 boolean isHorizontal() { 416 return AppGridConstants.isHorizontal(mPageOrientation); 417 } 418 419 /** 420 * Note: This activity is exported, meaning that it might receive intents from any source. 421 * Intent data parsing must be extra careful. 422 */ 423 @NonNull parseMode(@ullable Intent intent)424 private Mode parseMode(@Nullable Intent intent) { 425 String mode = intent != null ? intent.getStringExtra(MODE_INTENT_EXTRA) : null; 426 try { 427 return mode != null ? Mode.valueOf(mode) : Mode.ALL_APPS; 428 } catch (IllegalArgumentException e) { 429 throw new IllegalArgumentException("Received invalid mode: " + mode, e); 430 } 431 } 432 433 @Override onResume()434 protected void onResume() { 435 super.onResume(); 436 updateScrollState(); 437 mAdapter.setLayoutDirection(getResources().getConfiguration().getLayoutDirection()); 438 mAppGridViewModel.updateMode(mMode); 439 } 440 441 @Override onDimensionsUpdated(PageMeasurementHelper.PageDimensions pageDimens, PageMeasurementHelper.GridDimensions gridDimens)442 public void onDimensionsUpdated(PageMeasurementHelper.PageDimensions pageDimens, 443 PageMeasurementHelper.GridDimensions gridDimens) { 444 // TODO(b/271637411): move this method into a scroll controller 445 mAppGridMarginHorizontal = pageDimens.marginHorizontalPx; 446 mAppGridMarginVertical = pageDimens.marginVerticalPx; 447 mAppGridWidth = gridDimens.gridWidthPx; 448 mAppGridHeight = gridDimens.gridHeightPx; 449 } 450 451 /** 452 * Updates the scroll state after receiving data changes, such as new apps being added or 453 * reordered, and when user returns to launcher onResume. 454 * 455 * Additionally, notify page indicator to handle resizing in case new app addition creates a 456 * new page or deleted a page. 457 */ updateScrollState()458 void updateScrollState() { 459 // TODO(b/271637411): move this method into a scroll controller 460 // to calculate how many pages we need to offset, we use the scroll offset anchor position 461 // as item count and map to the page which the anchor is on. 462 int offsetPageCount = mAdapter.getPageCount(mNextScrollDestination + 1) - 1; 463 mRecyclerView.suppressLayout(false); 464 mCurrentScrollOffset = offsetPageCount * (isHorizontal() 465 ? (mAppGridWidth + 2 * mAppGridMarginHorizontal) 466 : (mAppGridHeight + 2 * mAppGridMarginVertical)); 467 mLayoutManager.scrollToPositionWithOffset(/* position */ 468 offsetPageCount * mNumOfRows * mNumOfCols, /* offset */ 0); 469 470 mPageIndicator.updateOffset(mCurrentScrollOffset); 471 mPageIndicator.updatePageCount(mAdapter.getPageCount()); 472 } 473 474 @Override onPause()475 protected void onPause() { 476 dismissShortcutPopup(); 477 super.onPause(); 478 } 479 480 @Override onSnapToPosition(int position)481 public void onSnapToPosition(int position) { 482 mNextScrollDestination = position; 483 } 484 485 @Override onItemLongPressed(boolean isLongPressed)486 public void onItemLongPressed(boolean isLongPressed) { 487 // after the user long presses the app icon, scrolling should be disabled until long press 488 // is canceled as to allow MotionEvent to be interpreted as attempt to drag the app icon. 489 mRecyclerView.suppressLayout(isLongPressed); 490 } 491 492 @Override onItemSelected(int gridPositionFrom)493 public void onItemSelected(int gridPositionFrom) { 494 mIsCurrentlyDragging = true; 495 mLayoutManager.setShouldLayoutChildren(false); 496 mAdapter.setDragStartPoint(gridPositionFrom); 497 dismissShortcutPopup(); 498 } 499 500 @Override onItemDragged()501 public void onItemDragged() { 502 mAppGridDragController.cancelDelayedPageFling(); 503 } 504 505 @Override onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection)506 public void onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection) { 507 if (mAdapter.getOffsetBoundDirection(gridPosition) == exitDirection) { 508 mAppGridDragController.postDelayedPageFling(exitDirection); 509 } 510 } 511 512 @Override onItemDropped(int gridPositionFrom, int gridPositionTo)513 public void onItemDropped(int gridPositionFrom, int gridPositionTo) { 514 mLayoutManager.setShouldLayoutChildren(true); 515 mAdapter.moveAppItem(gridPositionFrom, gridPositionTo); 516 } 517 onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup)518 public void onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup) { 519 sCarUiShortcutsPopup = carUiShortcutsPopup; 520 } 521 dismissShortcutPopup()522 private void dismissShortcutPopup() { 523 // TODO (b/268563442): shortcut popup is set to be static since its 524 // sometimes recreated when taskview is present, find out why 525 if (sCarUiShortcutsPopup != null) { 526 sCarUiShortcutsPopup.dismiss(); 527 sCarUiShortcutsPopup = null; 528 } 529 } 530 531 @Override onAppPositionChanged(int newPosition, AppItem appItem)532 public void onAppPositionChanged(int newPosition, AppItem appItem) { 533 mAppGridViewModel.saveAppOrder(newPosition, appItem); 534 } 535 536 537 private class AppGridOnScrollListener extends RecyclerView.OnScrollListener { 538 @Override onScrolled(@onNull RecyclerView recyclerView, int dx, int dy)539 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 540 mCurrentScrollOffset = mCurrentScrollOffset + (isHorizontal() ? dx : dy); 541 mPageIndicator.updateOffset(mCurrentScrollOffset); 542 } 543 544 @Override onScrollStateChanged(@onNull RecyclerView recyclerView, int newState)545 public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 546 mCurrentScrollState = newState; 547 mSnapCallback.setScrollState(mCurrentScrollState); 548 switch (newState) { 549 case RecyclerView.SCROLL_STATE_DRAGGING: 550 if (!mIsCurrentlyDragging) { 551 mDragCallback.cancelDragTasks(); 552 } 553 dismissShortcutPopup(); 554 mPageIndicator.animateAppearance(); 555 break; 556 557 case RecyclerView.SCROLL_STATE_SETTLING: 558 mPageIndicator.animateAppearance(); 559 break; 560 561 case RecyclerView.SCROLL_STATE_IDLE: 562 if (mIsCurrentlyDragging) { 563 mLayoutManager.setShouldLayoutChildren(false); 564 } 565 mPageIndicator.animateFading(); 566 // in case the recyclerview was scrolled by rotary input, we need to handle 567 // focusing the correct element: either on the first or last element on page 568 mRecyclerView.maybeHandleRotaryFocus(); 569 } 570 } 571 } 572 573 private class AppGridDragController { 574 // TODO: (b/271320404) move DragController to separate directory called dragndrop and 575 // migrate logic this class and AppItemViewHolder there. 576 private final Handler mHandler; 577 AppGridDragController()578 AppGridDragController() { 579 mHandler = new Handler(getMainLooper()); 580 } 581 cancelDelayedPageFling()582 void cancelDelayedPageFling() { 583 mHandler.removeCallbacksAndMessages(null); 584 } 585 postDelayedPageFling(@ppItemBoundDirection int exitDirection)586 void postDelayedPageFling(@AppItemBoundDirection int exitDirection) { 587 boolean scrollToNextPage = isHorizontal() 588 ? exitDirection == AppItemBoundDirection.RIGHT 589 : exitDirection == AppItemBoundDirection.BOTTOM; 590 mHandler.removeCallbacksAndMessages(null); 591 mHandler.postDelayed(new Runnable() { 592 public void run() { 593 if (mCurrentScrollState == RecyclerView.SCROLL_STATE_IDLE) { 594 mAdapter.updatePageScrollDestination(scrollToNextPage); 595 mNextScrollDestination = mSnapCallback.getSnapPosition(); 596 597 mLayoutManager.setShouldLayoutChildren(true); 598 mRecyclerView.smoothScrollToPosition(mNextScrollDestination); 599 } 600 // another delayed scroll will be queued to enable the user to input multiple 601 // page scrolls by holding the recyclerview at the app grid margin 602 postDelayedPageFling(exitDirection); 603 } 604 }, mOffPageHoverBeforeScrollMs); 605 } 606 } 607 608 /** 609 * Private onDragListener for handling dispatching off page scroll event when user holds the app 610 * icon at the page margin. 611 */ 612 private class AppGridDragListener implements View.OnDragListener { 613 @Override onDrag(View v, DragEvent event)614 public boolean onDrag(View v, DragEvent event) { 615 int action = event.getAction(); 616 if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) { 617 mIsCurrentlyDragging = false; 618 mAppGridDragController.cancelDelayedPageFling(); 619 mDragCallback.resetCallbackState(); 620 mLayoutManager.setShouldLayoutChildren(true); 621 if (action == DragEvent.ACTION_DROP) { 622 return false; 623 } else { 624 animateDropEnded(getDragSurface(event)); 625 } 626 } 627 return true; 628 } 629 } 630 animateDropEnded(@ullable SurfaceControl dragSurface)631 private void animateDropEnded(@Nullable SurfaceControl dragSurface) { 632 if (dragSurface == null) { 633 Log.d(TAG, "animateDropEnded, dragSurface unavailable"); 634 return; 635 } 636 // update default animation for the drag shadow after user lifts their finger 637 SurfaceControl.Transaction txn = new SurfaceControl.Transaction(); 638 // set an animator to animate a delay before clearing the dragSurface 639 ValueAnimator delayedDismissAnimator = ValueAnimator.ofFloat(0f, 1f); 640 delayedDismissAnimator.setStartDelay( 641 getResources().getInteger(R.integer.ms_drop_animation_delay)); 642 delayedDismissAnimator.addUpdateListener( 643 new ValueAnimator.AnimatorUpdateListener() { 644 @Override 645 public void onAnimationUpdate(ValueAnimator animation) { 646 txn.setAlpha(dragSurface, 0); 647 txn.apply(); 648 } 649 }); 650 delayedDismissAnimator.start(); 651 } 652 setupTosBanner()653 private void setupTosBanner() { 654 asLiveData(mAppGridViewModel.getShouldShowTosBanner()).observe(AppGridActivity.this, 655 showBanner -> { 656 if (showBanner) { 657 mBanner.setVisibility(View.VISIBLE); 658 // Pre draw is required for animation to work. 659 mBanner.getViewTreeObserver().addOnPreDrawListener( 660 new ViewTreeObserver.OnPreDrawListener() { 661 @Override 662 public boolean onPreDraw() { 663 mBanner.getViewTreeObserver().removeOnPreDrawListener(this); 664 mBackgroundAnimationHelper.showBanner(); 665 return true; 666 } 667 }); 668 } else { 669 mBanner.setVisibility(View.GONE); 670 } 671 }); 672 mBanner.setFirstButtonOnClickListener(v -> { 673 Intent tosIntent = AppLauncherUtils.getIntentForTosAcceptanceFlow(v.getContext()); 674 AppLauncherUtils.launchApp(v.getContext(), tosIntent); 675 }); 676 mBanner.setSecondButtonOnClickListener( 677 v -> { 678 mBackgroundAnimationHelper.hideBanner(); 679 mAppGridViewModel.saveTosBannerDismissalTime(); 680 }); 681 } 682 683 } 684