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.multidisplay.launcher;
18 
19 import static com.android.car.multidisplay.launcher.PinnedAppListViewModel.PINNED_APPS_KEY;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.app.ActivityOptions;
24 import android.app.AlertDialog;
25 import android.app.Application;
26 import android.content.Intent;
27 import android.content.SharedPreferences;
28 import android.content.res.Configuration;
29 import android.hardware.display.DisplayManager;
30 import android.os.Bundle;
31 import android.view.Display;
32 import android.view.MenuInflater;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.view.ViewAnimationUtils;
36 import android.view.inputmethod.InputMethodManager;
37 import android.widget.AdapterView;
38 import android.widget.AdapterView.OnItemSelectedListener;
39 import android.widget.ArrayAdapter;
40 import android.widget.CheckBox;
41 import android.widget.GridView;
42 import android.widget.ImageButton;
43 import android.widget.PopupMenu;
44 import android.widget.Spinner;
45 
46 import androidx.fragment.app.FragmentActivity;
47 import androidx.fragment.app.FragmentManager;
48 import androidx.lifecycle.ViewModelProvider;
49 import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory;
50 
51 import com.android.car.multidisplay.R;
52 
53 import com.google.android.material.circularreveal.cardview.CircularRevealCardView;
54 import com.google.android.material.floatingactionbutton.FloatingActionButton;
55 
56 import java.util.ArrayList;
57 import java.util.HashSet;
58 import java.util.Set;
59 
60 /**
61  * Main launcher activity. It's launch mode is configured as "singleTop" to allow showing on
62  * multiple displays and to ensure a single instance per each display.
63  */
64 public class LauncherActivity extends FragmentActivity implements AppPickedCallback,
65         PopupMenu.OnMenuItemClickListener {
66 
67     private Spinner mDisplaySpinner;
68     private ArrayAdapter<DisplayItem> mDisplayAdapter;
69     private int mSelectedDisplayId = Display.INVALID_DISPLAY;
70     private View mRootView;
71     private View mScrimView;
72     private View mAppDrawerHeader;
73     private AppListAdapter mAppListAdapter;
74     private AppListAdapter mPinnedAppListAdapter;
75     private CircularRevealCardView mAppDrawerView;
76     private FloatingActionButton mFab;
77     private CheckBox mNewInstanceCheckBox;
78 
79     private boolean mAppDrawerShown;
80 
81     @Override
onCreate(Bundle savedInstanceState)82     protected void onCreate(Bundle savedInstanceState) {
83         super.onCreate(savedInstanceState);
84         setContentView(R.layout.activity_main);
85 
86         mRootView = findViewById(R.id.RootView);
87         mScrimView = findViewById(R.id.Scrim);
88         mAppDrawerView = findViewById(R.id.FloatingSheet);
89 
90         // get system insets and apply padding accordingly to the content view
91         mRootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
92                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
93         mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
94             mRootView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
95             mAppDrawerHeader = findViewById(R.id.FloatingSheetHeader);
96             mAppDrawerHeader.setPadding(0, insets.getSystemWindowInsetTop(), 0, 0);
97             return insets.consumeSystemWindowInsets();
98         });
99 
100         mFab = findViewById(R.id.FloatingActionButton);
101         mFab.setOnClickListener((View v) -> {
102             showAppDrawer(true);
103         });
104 
105         mScrimView.setOnClickListener((View v) -> {
106             showAppDrawer(false);
107         });
108 
109         mDisplaySpinner = findViewById(R.id.spinner);
110         mDisplaySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
111             @Override
112             public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
113                 mSelectedDisplayId = mDisplayAdapter.getItem(i).mId;
114             }
115 
116             @Override
117             public void onNothingSelected(AdapterView<?> adapterView) {
118                 mSelectedDisplayId = Display.INVALID_DISPLAY;
119             }
120         });
121         mDisplayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,
122                 new ArrayList<DisplayItem>());
123         mDisplayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
124         mDisplaySpinner.setAdapter(mDisplayAdapter);
125 
126         final ViewModelProvider viewModelProvider = new ViewModelProvider(getViewModelStore(),
127                 new AndroidViewModelFactory((Application) getApplicationContext()));
128 
129         mPinnedAppListAdapter = new AppListAdapter(this);
130         final GridView pinnedAppGridView = findViewById(R.id.pinned_app_grid);
131         pinnedAppGridView.setAdapter(mPinnedAppListAdapter);
132         pinnedAppGridView.setOnItemClickListener((adapterView, view, position, id) -> {
133             final AppEntry entry = mPinnedAppListAdapter.getItem(position);
134             launch(entry.getLaunchIntent());
135         });
136         final PinnedAppListViewModel pinnedAppListViewModel =
137                 viewModelProvider.get(PinnedAppListViewModel.class);
138         pinnedAppListViewModel.getPinnedAppList().observe(this, data -> {
139             mPinnedAppListAdapter.setData(data);
140         });
141 
142         mAppListAdapter = new AppListAdapter(this);
143         final GridView appGridView = findViewById(R.id.app_grid);
144         appGridView.setAdapter(mAppListAdapter);
145         appGridView.setOnItemClickListener((adapterView, view, position, id) -> {
146             final AppEntry entry = mAppListAdapter.getItem(position);
147             launch(entry.getLaunchIntent());
148         });
149         final AppListViewModel appListViewModel = viewModelProvider.get(AppListViewModel.class);
150         appListViewModel.getAppList().observe(this, data -> {
151             mAppListAdapter.setData(data);
152         });
153 
154         findViewById(R.id.RefreshButton).setOnClickListener(this::refreshDisplayPicker);
155         mNewInstanceCheckBox = findViewById(R.id.NewInstanceCheckBox);
156 
157         ImageButton optionsButton = findViewById(R.id.OptionsButton);
158         optionsButton.setOnClickListener((View v) -> {
159             PopupMenu popup = new PopupMenu(this, v);
160             popup.setOnMenuItemClickListener(this);
161             MenuInflater inflater = popup.getMenuInflater();
162             inflater.inflate(R.menu.context_menu, popup.getMenu());
163             popup.show();
164         });
165     }
166 
167     @Override
onMenuItemClick(MenuItem item)168     public boolean onMenuItemClick(MenuItem item) {
169         // Respond to picking one of the popup menu items.
170         switch (item.getItemId()) {
171             case R.id.add_app_shortcut:
172                 FragmentManager fm = getSupportFragmentManager();
173                 PinnedAppPickerDialog pickerDialogFragment =
174                         PinnedAppPickerDialog.newInstance(mAppListAdapter, this);
175                 pickerDialogFragment.show(fm, "fragment_app_picker");
176                 return true;
177             case R.id.set_wallpaper:
178                 Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER);
179                 startActivity(Intent.createChooser(intent, getString(R.string.set_wallpaper)));
180                 return true;
181             default:
182                 return true;
183         }
184     }
185 
186     @Override
onConfigurationChanged(Configuration newConfig)187     public void onConfigurationChanged(Configuration newConfig) {
188         super.onConfigurationChanged(newConfig);
189         showAppDrawer(false);
190     }
191 
onBackPressed()192     public void onBackPressed() {
193         // If the app drawer was shown - hide it. Otherwise, not doing anything since we don't want
194         // to close the launcher.
195         showAppDrawer(false);
196     }
197 
onNewIntent(Intent intent)198     public void onNewIntent(Intent intent) {
199         super.onNewIntent(intent);
200 
201         if (Intent.ACTION_MAIN.equals(intent.getAction())) {
202             // Hide keyboard.
203             final View v = getWindow().peekDecorView();
204             if (v != null && v.getWindowToken() != null) {
205                 getSystemService(InputMethodManager.class).hideSoftInputFromWindow(
206                         v.getWindowToken(), 0);
207             }
208         }
209 
210         // A new intent will bring the launcher to top. Hide the app drawer to reset the state.
211         showAppDrawer(false);
212     }
213 
launch(Intent launchIntent)214     void launch(Intent launchIntent) {
215         launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
216         if (mNewInstanceCheckBox.isChecked()) {
217             launchIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
218         }
219         final ActivityOptions options = ActivityOptions.makeBasic();
220         if (mSelectedDisplayId != Display.INVALID_DISPLAY) {
221             options.setLaunchDisplayId(mSelectedDisplayId);
222         }
223         try {
224             startActivity(launchIntent, options.toBundle());
225         } catch (Exception e) {
226             final AlertDialog.Builder builder =
227                     new AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert);
228             builder.setTitle(R.string.couldnt_launch)
229                     .setMessage(e.getLocalizedMessage())
230                     .setIcon(android.R.drawable.ic_dialog_alert)
231                     .show();
232         }
233     }
234 
refreshDisplayPicker()235     private void refreshDisplayPicker() {
236         refreshDisplayPicker(mAppDrawerView);
237     }
238 
refreshDisplayPicker(View view)239     private void refreshDisplayPicker(View view) {
240         final int currentDisplayId = view.getDisplay().getDisplayId();
241         final DisplayManager dm = getSystemService(DisplayManager.class);
242         mDisplayAdapter.setNotifyOnChange(false);
243         mDisplayAdapter.clear();
244         mDisplayAdapter.add(new DisplayItem(Display.INVALID_DISPLAY, "Do not specify display"));
245 
246         for (Display display : dm.getDisplays()) {
247             final int id = display.getDisplayId();
248             final boolean isDisplayPrivate = (display.getFlags() & Display.FLAG_PRIVATE) != 0;
249             final boolean isCurrentDisplay = id == currentDisplayId;
250             final StringBuilder sb = new StringBuilder();
251             sb.append(id).append(": ").append(display.getName());
252             if (isDisplayPrivate) {
253                 sb.append(" (private)");
254             }
255             if (isCurrentDisplay) {
256                 sb.append(" [Current display]");
257             }
258             mDisplayAdapter.add(new DisplayItem(id, sb.toString()));
259         }
260 
261         mDisplayAdapter.notifyDataSetChanged();
262     }
263 
264     /**
265      * Store the picked app to persistent pinned list and update the loader.
266      */
267     @Override
onAppPicked(AppEntry appEntry)268     public void onAppPicked(AppEntry appEntry) {
269         final SharedPreferences sp = getSharedPreferences(PINNED_APPS_KEY, 0);
270         Set<String> pinnedApps = sp.getStringSet(PINNED_APPS_KEY, null);
271         if (pinnedApps == null) {
272             pinnedApps = new HashSet<String>();
273         } else {
274             // Always need to create a new object to make sure that the changes are persisted.
275             pinnedApps = new HashSet<String>(pinnedApps);
276         }
277         pinnedApps.add(appEntry.getComponentName().flattenToString());
278 
279         final SharedPreferences.Editor editor = sp.edit();
280         editor.putStringSet(PINNED_APPS_KEY, pinnedApps);
281         editor.apply();
282     }
283 
284     /**
285      * Show/hide app drawer card with animation.
286      */
showAppDrawer(boolean show)287     private void showAppDrawer(boolean show) {
288         if (show == mAppDrawerShown) {
289             return;
290         }
291 
292         final Animator animator = revealAnimator(mAppDrawerView, show);
293         if (show) {
294             mAppDrawerShown = true;
295             mAppDrawerView.setVisibility(View.VISIBLE);
296             mScrimView.setVisibility(View.VISIBLE);
297             mFab.setVisibility(View.INVISIBLE);
298             refreshDisplayPicker();
299         } else {
300             mAppDrawerShown = false;
301             mScrimView.setVisibility(View.INVISIBLE);
302             animator.addListener(new AnimatorListenerAdapter() {
303                 @Override
304                 public void onAnimationEnd(Animator animation) {
305                     super.onAnimationEnd(animation);
306                     mAppDrawerView.setVisibility(View.INVISIBLE);
307                     mFab.setVisibility(View.VISIBLE);
308                 }
309             });
310         }
311         animator.start();
312     }
313 
314     /**
315      * Create reveal/hide animator for app list card.
316      */
revealAnimator(View view, boolean open)317     private Animator revealAnimator(View view, boolean open) {
318         final int radius = (int) Math.hypot((double) view.getWidth(), (double) view.getHeight());
319         return ViewAnimationUtils.createCircularReveal(view, view.getRight(), view.getBottom(),
320                 open ? 0 : radius, open ? radius : 0);
321     }
322 
323     private static class DisplayItem {
324         final int mId;
325         final String mDescription;
326 
DisplayItem(int displayId, String description)327         DisplayItem(int displayId, String description) {
328             mId = displayId;
329             mDescription = description;
330         }
331 
332         @Override
toString()333         public String toString() {
334             return mDescription;
335         }
336     }
337 }
338