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