• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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