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