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