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