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 com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_LAUNCHABLES; 20 import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_MEDIA_SERVICES; 21 import static com.android.car.carlauncher.displayarea.CarDisplayAreaOrganizer.FOREGROUND_DISPLAY_AREA_ROOT; 22 23 import android.app.Activity; 24 import android.app.usage.UsageStats; 25 import android.app.usage.UsageStatsManager; 26 import android.car.Car; 27 import android.car.CarNotConnectedException; 28 import android.car.content.pm.CarPackageManager; 29 import android.car.drivingstate.CarUxRestrictionsManager; 30 import android.car.media.CarMediaManager; 31 import android.content.BroadcastReceiver; 32 import android.content.ComponentName; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.IntentFilter; 36 import android.content.ServiceConnection; 37 import android.content.pm.LauncherApps; 38 import android.content.pm.PackageManager; 39 import android.os.Build; 40 import android.os.Bundle; 41 import android.os.IBinder; 42 import android.text.TextUtils; 43 import android.text.format.DateUtils; 44 import android.util.Log; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.annotation.StringRes; 49 import androidx.recyclerview.widget.GridLayoutManager; 50 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup; 51 52 import com.android.car.carlauncher.AppLauncherUtils.LauncherAppsInfo; 53 import com.android.car.carlauncher.displayarea.CarDisplayAreaController; 54 import com.android.car.ui.FocusArea; 55 import com.android.car.ui.baselayout.Insets; 56 import com.android.car.ui.baselayout.InsetsChangedListener; 57 import com.android.car.ui.core.CarUi; 58 import com.android.car.ui.recyclerview.CarUiRecyclerView; 59 import com.android.car.ui.toolbar.MenuItem; 60 import com.android.car.ui.toolbar.NavButtonMode; 61 import com.android.car.ui.toolbar.ToolbarController; 62 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.Collections; 66 import java.util.Comparator; 67 import java.util.HashSet; 68 import java.util.List; 69 import java.util.Set; 70 71 /** 72 * Launcher activity that shows a grid of apps. 73 */ 74 public class AppGridActivity extends Activity implements InsetsChangedListener { 75 private static final String TAG = "AppGridActivity"; 76 private static final String MODE_INTENT_EXTRA = "com.android.car.carlauncher.mode"; 77 78 private int mColumnNumber; 79 private boolean mShowAllApps = true; 80 private final Set<String> mHiddenApps = new HashSet<>(); 81 private final Set<String> mCustomMediaComponents = new HashSet<>(); 82 private AppGridAdapter mGridAdapter; 83 private PackageManager mPackageManager; 84 private UsageStatsManager mUsageStatsManager; 85 private AppInstallUninstallReceiver mInstallUninstallReceiver; 86 private Car mCar; 87 private CarUxRestrictionsManager mCarUxRestrictionsManager; 88 private CarPackageManager mCarPackageManager; 89 private CarMediaManager mCarMediaManager; 90 private Mode mMode; 91 92 /** 93 * enum to define the state of display area possible. 94 * CONTROL_BAR state is when only control bar is visible. 95 * FULL state is when display area hosting default apps cover the screen fully. 96 * DEFAULT state where maps are shown above DA for default apps. 97 */ 98 public enum CAR_LAUNCHER_STATE { 99 CONTROL_BAR, DEFAULT, FULL 100 } 101 102 private enum Mode { 103 ALL_APPS(R.string.app_launcher_title_all_apps, 104 APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES, 105 true), 106 MEDIA_ONLY(R.string.app_launcher_title_media_only, 107 APP_TYPE_MEDIA_SERVICES, 108 true), 109 MEDIA_POPUP(R.string.app_launcher_title_media_only, 110 APP_TYPE_MEDIA_SERVICES, 111 false), 112 ; 113 public final @StringRes int mTitleStringId; 114 public final @AppLauncherUtils.AppTypes int mAppTypes; 115 public final boolean mOpenMediaCenter; 116 Mode(@tringRes int titleStringId, @AppLauncherUtils.AppTypes int appTypes, boolean openMediaCenter)117 Mode(@StringRes int titleStringId, @AppLauncherUtils.AppTypes int appTypes, 118 boolean openMediaCenter) { 119 mTitleStringId = titleStringId; 120 mAppTypes = appTypes; 121 mOpenMediaCenter = openMediaCenter; 122 } 123 } 124 125 private ServiceConnection mCarConnectionListener = new ServiceConnection() { 126 @Override 127 public void onServiceConnected(ComponentName name, IBinder service) { 128 try { 129 mCarUxRestrictionsManager = (CarUxRestrictionsManager) mCar.getCarManager( 130 Car.CAR_UX_RESTRICTION_SERVICE); 131 mGridAdapter.setIsDistractionOptimizationRequired( 132 mCarUxRestrictionsManager 133 .getCurrentCarUxRestrictions() 134 .isRequiresDistractionOptimization()); 135 mCarUxRestrictionsManager.registerListener( 136 restrictionInfo -> 137 mGridAdapter.setIsDistractionOptimizationRequired( 138 restrictionInfo.isRequiresDistractionOptimization())); 139 140 mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE); 141 mCarMediaManager = (CarMediaManager) mCar.getCarManager(Car.CAR_MEDIA_SERVICE); 142 updateAppsLists(); 143 } catch (CarNotConnectedException e) { 144 Log.e(TAG, "Car not connected in CarConnectionListener", e); 145 } 146 } 147 148 @Override 149 public void onServiceDisconnected(ComponentName name) { 150 mCarUxRestrictionsManager = null; 151 mCarPackageManager = null; 152 } 153 }; 154 155 @Override onCreate(@ullable Bundle savedInstanceState)156 protected void onCreate(@Nullable Bundle savedInstanceState) { 157 super.onCreate(savedInstanceState); 158 159 mColumnNumber = getResources().getInteger(R.integer.car_app_selector_column_number); 160 mPackageManager = getPackageManager(); 161 mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); 162 mCar = Car.createCar(this, mCarConnectionListener); 163 mHiddenApps.addAll(Arrays.asList(getResources().getStringArray(R.array.hidden_apps))); 164 mCustomMediaComponents.addAll( 165 Arrays.asList(getResources().getStringArray(R.array.custom_media_packages))); 166 167 setContentView(R.layout.app_grid_activity); 168 169 updateMode(); 170 171 ToolbarController toolbar = CarUi.requireToolbar(this); 172 173 // Check if a custom policy builder is defined. 174 if (CarLauncherUtils.isCustomDisplayPolicyDefined(this)) { 175 CarDisplayAreaController carDisplayAreaController = 176 CarDisplayAreaController.getInstance(); 177 carDisplayAreaController.showTitleBar(FOREGROUND_DISPLAY_AREA_ROOT, this); 178 } else { 179 toolbar.setNavButtonMode(NavButtonMode.CLOSE); 180 } 181 182 if (Build.IS_DEBUGGABLE) { 183 toolbar.setMenuItems(Collections.singletonList(MenuItem.builder(this) 184 .setDisplayBehavior(MenuItem.DisplayBehavior.NEVER) 185 .setTitle(R.string.hide_debug_apps) 186 .setOnClickListener(i -> { 187 mShowAllApps = !mShowAllApps; 188 i.setTitle(mShowAllApps 189 ? R.string.hide_debug_apps 190 : R.string.show_debug_apps); 191 updateAppsLists(); 192 }) 193 .build())); 194 } 195 196 mGridAdapter = new AppGridAdapter(this); 197 CarUiRecyclerView gridView = requireViewById(R.id.apps_grid); 198 199 GridLayoutManager gridLayoutManager = new GridLayoutManager(this, mColumnNumber); 200 gridLayoutManager.setSpanSizeLookup(new SpanSizeLookup() { 201 @Override 202 public int getSpanSize(int position) { 203 return mGridAdapter.getSpanSizeLookup(position); 204 } 205 }); 206 gridView.setLayoutManager(gridLayoutManager); 207 gridView.setAdapter(mGridAdapter); 208 } 209 210 @Override onNewIntent(Intent intent)211 protected void onNewIntent(Intent intent) { 212 super.onNewIntent(intent); 213 setIntent(intent); 214 updateMode(); 215 } 216 217 @Override onDestroy()218 protected void onDestroy() { 219 if (mCar != null && mCar.isConnected()) { 220 mCar.disconnect(); 221 mCar = null; 222 } 223 super.onDestroy(); 224 } 225 updateMode()226 private void updateMode() { 227 mMode = parseMode(getIntent()); 228 setTitle(mMode.mTitleStringId); 229 CarUi.requireToolbar(this).setTitle(mMode.mTitleStringId); 230 } 231 232 /** 233 * Note: This activity is exported, meaning that it might receive intents from any source. 234 * Intent data parsing must be extra careful. 235 */ 236 @NonNull parseMode(@ullable Intent intent)237 private Mode parseMode(@Nullable Intent intent) { 238 String mode = intent != null ? intent.getStringExtra(MODE_INTENT_EXTRA) : null; 239 try { 240 return mode != null ? Mode.valueOf(mode) : Mode.ALL_APPS; 241 } catch (IllegalArgumentException e) { 242 throw new IllegalArgumentException("Received invalid mode: " + mode, e); 243 } 244 } 245 246 @Override onResume()247 protected void onResume() { 248 super.onResume(); 249 250 // Using onResume() to refresh most recently used apps because we want to refresh even if 251 // the app being launched crashes/doesn't cover the entire screen. 252 updateAppsLists(); 253 } 254 255 /** Updates the list of all apps, and the list of the most recently used ones. */ updateAppsLists()256 private void updateAppsLists() { 257 Set<String> appsToHide = mShowAllApps ? Collections.emptySet() : mHiddenApps; 258 LauncherAppsInfo appsInfo = AppLauncherUtils.getLauncherApps(appsToHide, 259 mCustomMediaComponents, 260 mMode.mAppTypes, 261 mMode.mOpenMediaCenter, 262 getSystemService(LauncherApps.class), 263 mCarPackageManager, 264 mPackageManager, 265 new AppLauncherUtils.VideoAppPredicate(mPackageManager), 266 mCarMediaManager); 267 mGridAdapter.setAllApps(appsInfo.getLaunchableComponentsList()); 268 mGridAdapter.setMostRecentApps(getMostRecentApps(appsInfo)); 269 } 270 271 @Override onStart()272 protected void onStart() { 273 super.onStart(); 274 // register broadcast receiver for package installation and uninstallation 275 mInstallUninstallReceiver = new AppInstallUninstallReceiver(); 276 IntentFilter filter = new IntentFilter(); 277 filter.addAction(Intent.ACTION_PACKAGE_ADDED); 278 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 279 filter.addAction(Intent.ACTION_PACKAGE_REPLACED); 280 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 281 filter.addDataScheme("package"); 282 registerReceiver(mInstallUninstallReceiver, filter); 283 284 // Connect to car service 285 mCar.connect(); 286 } 287 288 @Override onStop()289 protected void onStop() { 290 super.onStop(); 291 // disconnect from app install/uninstall receiver 292 if (mInstallUninstallReceiver != null) { 293 unregisterReceiver(mInstallUninstallReceiver); 294 mInstallUninstallReceiver = null; 295 } 296 // disconnect from car listeners 297 try { 298 if (mCarUxRestrictionsManager != null) { 299 mCarUxRestrictionsManager.unregisterListener(); 300 } 301 } catch (CarNotConnectedException e) { 302 Log.e(TAG, "Error unregistering listeners", e); 303 } 304 if (mCar != null) { 305 mCar.disconnect(); 306 } 307 } 308 309 /** 310 * Note that in order to obtain usage stats from the previous boot, 311 * the device must have gone through a clean shut down process. 312 */ getMostRecentApps(LauncherAppsInfo appsInfo)313 private List<AppMetaData> getMostRecentApps(LauncherAppsInfo appsInfo) { 314 ArrayList<AppMetaData> apps = new ArrayList<>(); 315 if (appsInfo.isEmpty()) { 316 return apps; 317 } 318 319 // get the usage stats starting from 1 year ago with a INTERVAL_YEARLY granularity 320 // returning entries like: 321 // "During 2017 App A is last used at 2017/12/15 18:03" 322 // "During 2017 App B is last used at 2017/6/15 10:00" 323 // "During 2018 App A is last used at 2018/1/1 15:12" 324 List<UsageStats> stats = 325 mUsageStatsManager.queryUsageStats( 326 UsageStatsManager.INTERVAL_YEARLY, 327 System.currentTimeMillis() - DateUtils.YEAR_IN_MILLIS, 328 System.currentTimeMillis()); 329 330 if (stats == null || stats.size() == 0) { 331 return apps; // empty list 332 } 333 334 stats.sort(new LastTimeUsedComparator()); 335 336 int currentIndex = 0; 337 int itemsAdded = 0; 338 int statsSize = stats.size(); 339 int itemCount = Math.min(mColumnNumber, statsSize); 340 while (itemsAdded < itemCount && currentIndex < statsSize) { 341 UsageStats usageStats = stats.get(currentIndex); 342 String packageName = usageStats.mPackageName; 343 currentIndex++; 344 345 // do not include self 346 if (packageName.equals(getPackageName())) { 347 continue; 348 } 349 350 // TODO(b/136222320): UsageStats is obtained per package, but a package may contain 351 // multiple media services. We need to find a way to get the usage stats per service. 352 ComponentName componentName = AppLauncherUtils.getMediaSource(mPackageManager, 353 packageName); 354 // Exempt media services from background and launcher checks 355 if (!appsInfo.isMediaService(componentName)) { 356 // do not include apps that only ran in the background 357 if (usageStats.getTotalTimeInForeground() == 0) { 358 continue; 359 } 360 361 // do not include apps that don't support starting from launcher 362 Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); 363 if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { 364 continue; 365 } 366 } 367 368 AppMetaData app = appsInfo.getAppMetaData(componentName); 369 // Prevent duplicated entries 370 // e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00 371 if (app != null && !apps.contains(app)) { 372 apps.add(app); 373 itemsAdded++; 374 } 375 } 376 return apps; 377 } 378 379 @Override onCarUiInsetsChanged(Insets insets)380 public void onCarUiInsetsChanged(Insets insets) { 381 requireViewById(R.id.apps_grid) 382 .setPadding(0, insets.getTop(), 0, insets.getBottom()); 383 FocusArea focusArea = requireViewById(R.id.focus_area); 384 focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom()); 385 focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom()); 386 387 requireViewById(android.R.id.content) 388 .setPadding(insets.getLeft(), 0, insets.getRight(), 0); 389 } 390 391 /** 392 * Comparator for {@link UsageStats} that sorts the list by the "last time used" property 393 * in descending order. 394 */ 395 private static class LastTimeUsedComparator implements Comparator<UsageStats> { 396 @Override compare(UsageStats stat1, UsageStats stat2)397 public int compare(UsageStats stat1, UsageStats stat2) { 398 Long time1 = stat1.getLastTimeUsed(); 399 Long time2 = stat2.getLastTimeUsed(); 400 return time2.compareTo(time1); 401 } 402 } 403 404 private class AppInstallUninstallReceiver extends BroadcastReceiver { 405 @Override onReceive(Context context, Intent intent)406 public void onReceive(Context context, Intent intent) { 407 String packageName = intent.getData().getSchemeSpecificPart(); 408 409 if (TextUtils.isEmpty(packageName)) { 410 Log.e(TAG, "System sent an empty app install/uninstall broadcast"); 411 return; 412 } 413 414 updateAppsLists(); 415 } 416 } 417 } 418