• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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