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