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 final 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> blackList = mShowAllApps ? Collections.emptySet() : mHiddenApps;
237         LauncherAppsInfo appsInfo = AppLauncherUtils.getLauncherApps(blackList,
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 
364         requireViewById(android.R.id.content)
365                 .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
366     }
367 
368     /**
369      * Comparator for {@link UsageStats} that sorts the list by the "last time used" property
370      * in descending order.
371      */
372     private static class LastTimeUsedComparator implements Comparator<UsageStats> {
373         @Override
compare(UsageStats stat1, UsageStats stat2)374         public int compare(UsageStats stat1, UsageStats stat2) {
375             Long time1 = stat1.getLastTimeUsed();
376             Long time2 = stat2.getLastTimeUsed();
377             return time2.compareTo(time1);
378         }
379     }
380 
381     private class AppInstallUninstallReceiver extends BroadcastReceiver {
382         @Override
onReceive(Context context, Intent intent)383         public void onReceive(Context context, Intent intent) {
384             String packageName = intent.getData().getSchemeSpecificPart();
385 
386             if (TextUtils.isEmpty(packageName)) {
387                 Log.e(TAG, "System sent an empty app install/uninstall broadcast");
388                 return;
389             }
390 
391             updateAppsLists();
392         }
393     }
394 }
395