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 android.annotation.Nullable; 20 import android.app.Activity; 21 import android.app.usage.UsageStats; 22 import android.app.usage.UsageStatsManager; 23 import android.car.Car; 24 import android.car.CarNotConnectedException; 25 import android.car.content.pm.CarPackageManager; 26 import android.car.drivingstate.CarUxRestrictionsManager; 27 import android.content.BroadcastReceiver; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.ServiceConnection; 33 import android.content.pm.ActivityInfo; 34 import android.content.pm.LauncherApps; 35 import android.content.pm.PackageManager; 36 import android.graphics.drawable.Drawable; 37 import android.os.Bundle; 38 import android.os.IBinder; 39 import android.support.v7.widget.GridLayoutManager; 40 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; 41 import android.text.TextUtils; 42 import android.text.format.DateUtils; 43 import android.util.Log; 44 import android.view.View; 45 46 import java.util.ArrayList; 47 import java.util.Collections; 48 import java.util.Comparator; 49 import java.util.List; 50 51 import androidx.car.widget.PagedListView; 52 53 /** 54 * Launcher activity that shows a grid of apps. 55 */ 56 public final class AppGridActivity extends Activity { 57 58 private static final String TAG = "AppGridActivity"; 59 60 private int mColumnNumber; 61 private AppGridAdapter mGridAdapter; 62 private PackageManager mPackageManager; 63 private UsageStatsManager mUsageStatsManager; 64 private AppInstallUninstallReceiver mInstallUninstallReceiver; 65 private Car mCar; 66 private CarUxRestrictionsManager mCarUxRestrictionsManager; 67 private CarPackageManager mCarPackageManager; 68 69 private ServiceConnection mCarConnectionListener = new ServiceConnection() { 70 @Override 71 public void onServiceConnected(ComponentName name, IBinder service) { 72 try { 73 mCarUxRestrictionsManager = (CarUxRestrictionsManager) mCar.getCarManager( 74 Car.CAR_UX_RESTRICTION_SERVICE); 75 mGridAdapter.setIsDistractionOptimizationRequired( 76 mCarUxRestrictionsManager 77 .getCurrentCarUxRestrictions() 78 .isRequiresDistractionOptimization()); 79 mCarUxRestrictionsManager.registerListener( 80 restrictionInfo -> 81 mGridAdapter.setIsDistractionOptimizationRequired( 82 restrictionInfo.isRequiresDistractionOptimization())); 83 84 mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE); 85 mGridAdapter.setAllApps(getAllApps()); 86 mGridAdapter.setMostRecentApps(getMostRecentApps()); 87 88 } catch (CarNotConnectedException e) { 89 Log.e(TAG, "Car not connected in CarConnectionListener", e); 90 } 91 } 92 93 @Override 94 public void onServiceDisconnected(ComponentName name) { 95 mCarUxRestrictionsManager = null; 96 mCarPackageManager = null; 97 } 98 }; 99 100 @Override onCreate(@ullable Bundle savedInstanceState)101 protected void onCreate(@Nullable Bundle savedInstanceState) { 102 super.onCreate(savedInstanceState); 103 mColumnNumber = getResources().getInteger(R.integer.car_app_selector_column_number); 104 mPackageManager = getPackageManager(); 105 mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); 106 mCar = Car.createCar(this, mCarConnectionListener); 107 108 setContentView(R.layout.app_grid_activity); 109 110 findViewById(R.id.exit_button_container).setOnClickListener(v -> finish()); 111 112 findViewById(R.id.search_button_container).setOnClickListener((View view) -> { 113 Intent intent = new Intent(this, AppSearchActivity.class); 114 startActivity(intent); 115 }); 116 117 mGridAdapter = new AppGridAdapter(this); 118 PagedListView gridView = findViewById(R.id.apps_grid); 119 120 GridLayoutManager gridLayoutManager = new GridLayoutManager(this, mColumnNumber); 121 gridLayoutManager.setSpanSizeLookup(new SpanSizeLookup() { 122 @Override 123 public int getSpanSize(int position) { 124 return mGridAdapter.getSpanSizeLookup(position); 125 } 126 }); 127 gridView.getRecyclerView().setLayoutManager(gridLayoutManager); 128 129 gridView.setAdapter(mGridAdapter); 130 } 131 132 @Override onResume()133 protected void onResume() { 134 super.onResume(); 135 mGridAdapter.setAllApps(getAllApps()); 136 // using onResume() to refresh most recently used apps because we want to refresh even if 137 // the app being launched crashes/doesn't cover the entire screen. 138 mGridAdapter.setMostRecentApps(getMostRecentApps()); 139 } 140 141 @Override onStart()142 protected void onStart() { 143 super.onStart(); 144 // register broadcast receiver for package installation and uninstallation 145 mInstallUninstallReceiver = new AppInstallUninstallReceiver(); 146 IntentFilter filter = new IntentFilter(); 147 filter.addAction(Intent.ACTION_PACKAGE_ADDED); 148 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 149 filter.addAction(Intent.ACTION_PACKAGE_REPLACED); 150 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 151 filter.addDataScheme("package"); 152 registerReceiver(mInstallUninstallReceiver, filter); 153 154 // Connect to car service 155 mCar.connect(); 156 } 157 158 @Override onStop()159 protected void onStop() { 160 super.onPause(); 161 // disconnect from app install/uninstall receiver 162 if (mInstallUninstallReceiver != null) { 163 unregisterReceiver(mInstallUninstallReceiver); 164 mInstallUninstallReceiver = null; 165 } 166 // disconnect from car listeners 167 try { 168 if (mCarUxRestrictionsManager != null) { 169 mCarUxRestrictionsManager.unregisterListener(); 170 } 171 } catch (CarNotConnectedException e) { 172 Log.e(TAG, "Error unregistering listeners", e); 173 } 174 if (mCar != null) { 175 mCar.disconnect(); 176 } 177 } 178 getMostRecentApps()179 private List<AppMetaData> getMostRecentApps() { 180 ArrayList<AppMetaData> apps = new ArrayList<>(); 181 182 // get the usage stats starting from 1 year ago with a INTERVAL_YEARLY granularity 183 // returning entries like: 184 // "During 2017 App A is last used at 2017/12/15 18:03" 185 // "During 2017 App B is last used at 2017/6/15 10:00" 186 // "During 2018 App A is last used at 2018/1/1 15:12" 187 List<UsageStats> stats = 188 mUsageStatsManager.queryUsageStats( 189 UsageStatsManager.INTERVAL_YEARLY, 190 System.currentTimeMillis() - DateUtils.YEAR_IN_MILLIS, 191 System.currentTimeMillis()); 192 193 if (stats == null || stats.size() == 0) { 194 return apps; // empty list 195 } 196 197 Collections.sort(stats, new LastTimeUsedComparator()); 198 199 int currentIndex = 0; 200 int itemsAdded = 0; 201 int statsSize = stats.size(); 202 int itemCount = Math.min(mColumnNumber, statsSize); 203 while (itemsAdded < itemCount && currentIndex < statsSize) { 204 String packageName = stats.get(currentIndex).getPackageName(); 205 currentIndex++; 206 207 // do not include self 208 if (packageName.equals(getPackageName())) { 209 continue; 210 } 211 212 // do not include apps that don't support starting from launcher 213 Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); 214 if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { 215 continue; 216 } 217 218 try { 219 // try getting activity info from package name 220 Drawable icon = mPackageManager.getActivityIcon(intent); 221 ActivityInfo info = mPackageManager.getActivityInfo(intent.getComponent(), 0); 222 CharSequence displayName = info.loadLabel(mPackageManager); 223 if (icon == null || TextUtils.isEmpty(displayName)) { 224 continue; 225 } 226 boolean isDistractionOptimized = AppLauncherUtils.isActivityDistractionOptimized( 227 mCarPackageManager, packageName, intent.getComponent().getClassName()); 228 AppMetaData app = 229 new AppMetaData(displayName, packageName, icon, isDistractionOptimized); 230 231 // edge case: do not include duplicated entries 232 // e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00 233 if (apps.contains(app)) { 234 continue; 235 } 236 237 apps.add(app); 238 itemsAdded++; 239 } catch (PackageManager.NameNotFoundException e) { 240 // this should never happen 241 Log.e(TAG, "NameNotFoundException when getting app icon in AppGridActivity", e); 242 } 243 } 244 return apps; 245 } 246 247 /** 248 * Comparator for {@link UsageStats} that sorts the list by the "last time used" property 249 * in descending order. 250 */ 251 private static class LastTimeUsedComparator implements Comparator<UsageStats> { 252 @Override compare(UsageStats stat1, UsageStats stat2)253 public int compare(UsageStats stat1, UsageStats stat2) { 254 Long time1 = stat1.getLastTimeUsed(); 255 Long time2 = stat2.getLastTimeUsed(); 256 return time2.compareTo(time1); 257 } 258 } 259 getAllApps()260 private List<AppMetaData> getAllApps() { 261 return AppLauncherUtils.getAllLaunchableApps( 262 getSystemService(LauncherApps.class), mCarPackageManager, mPackageManager); 263 } 264 265 private class AppInstallUninstallReceiver extends BroadcastReceiver { 266 @Override onReceive(Context context, Intent intent)267 public void onReceive(Context context, Intent intent) { 268 String packageName = intent.getData().getSchemeSpecificPart(); 269 270 if (TextUtils.isEmpty(packageName)) { 271 Log.e(TAG, "System sent an empty app install/uninstall broadcast"); 272 return; 273 } 274 275 mGridAdapter.setAllApps(getAllApps()); 276 } 277 } 278 } 279