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