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