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