1 /* 2 * Copyright (C) 2019 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.ui.paintbooth; 18 19 import static com.android.car.ui.paintbooth.PaintBoothApplication.SHARED_PREFERENCES_FILE; 20 import static com.android.car.ui.paintbooth.PaintBoothApplication.SHARED_PREFERENCES_SHARED_LIB_DENYLIST; 21 22 import android.app.Activity; 23 import android.app.ActivityManager; 24 import android.app.Service; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.os.Bundle; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.Button; 33 import android.widget.CompoundButton; 34 import android.widget.CompoundButton.OnCheckedChangeListener; 35 import android.widget.Switch; 36 import android.widget.Toast; 37 38 import androidx.annotation.NonNull; 39 import androidx.core.util.Supplier; 40 import androidx.recyclerview.widget.RecyclerView; 41 42 import com.android.car.ui.FocusArea; 43 import com.android.car.ui.baselayout.Insets; 44 import com.android.car.ui.baselayout.InsetsChangedListener; 45 import com.android.car.ui.core.CarUi; 46 import com.android.car.ui.paintbooth.appstyledview.AppStyledViewSampleActivity; 47 import com.android.car.ui.paintbooth.caruirecyclerview.CarUiListItemActivity; 48 import com.android.car.ui.paintbooth.caruirecyclerview.CarUiRecyclerViewActivity; 49 import com.android.car.ui.paintbooth.caruirecyclerview.GridCarUiRecyclerViewActivity; 50 import com.android.car.ui.paintbooth.currentactivity.CurrentActivityService; 51 import com.android.car.ui.paintbooth.dialogs.DialogsActivity; 52 import com.android.car.ui.paintbooth.overlays.OverlayActivity; 53 import com.android.car.ui.paintbooth.preferences.PreferenceActivity; 54 import com.android.car.ui.paintbooth.preferences.SplitPreferenceActivity; 55 import com.android.car.ui.paintbooth.toolbar.NoCarUiToolbarActivity; 56 import com.android.car.ui.paintbooth.toolbar.ToolbarActivity; 57 import com.android.car.ui.paintbooth.widescreenime.WideScreenImeActivity; 58 import com.android.car.ui.paintbooth.widescreenime.WideScreenTestView; 59 import com.android.car.ui.paintbooth.widgets.WidgetActivity; 60 import com.android.car.ui.recyclerview.CarUiRecyclerView; 61 import com.android.car.ui.toolbar.ToolbarController; 62 63 import java.lang.reflect.Method; 64 import java.util.Arrays; 65 import java.util.Collections; 66 import java.util.List; 67 68 /** 69 * Paint booth app 70 */ 71 public class MainActivity extends Activity implements InsetsChangedListener { 72 73 public static final String STOP_SERVICE = "com.android.car.ui.paintbooth.StopService"; 74 75 /** 76 * List of all sample activities. 77 */ 78 private final List<ListElement> mActivities = Arrays.asList( 79 new ServiceElement("Show foreground activities", CurrentActivityService.class), 80 new ServiceElement("Simulate Screen Bounds", VisibleBoundsSimulator.class), 81 new SwitchElement("Enable shared library", this::isSharedLibEnabled, 82 this::onSharedLibSwitchChanged), 83 new ActivityElement("Dialogs sample", DialogsActivity.class), 84 new ActivityElement("App Styled View Modal", AppStyledViewSampleActivity.class), 85 new ActivityElement("List sample", CarUiRecyclerViewActivity.class), 86 new ActivityElement("Grid sample", GridCarUiRecyclerViewActivity.class), 87 new ActivityElement("Preferences sample", PreferenceActivity.class), 88 new ActivityElement("Split preferences sample", SplitPreferenceActivity.class), 89 new ActivityElement("Overlays", OverlayActivity.class), 90 new ActivityElement("Toolbar sample", ToolbarActivity.class), 91 new ActivityElement("No CarUiToolbar sample", NoCarUiToolbarActivity.class), 92 new ActivityElement("Widget sample", WidgetActivity.class), 93 new ActivityElement("Wide Screen IME", WideScreenImeActivity.class), 94 new ActivityElement("Wide Screen View IME", WideScreenTestView.class), 95 new ActivityElement("ListItem sample", CarUiListItemActivity.class)); 96 97 private abstract static class ViewHolder extends RecyclerView.ViewHolder { 98 ViewHolder(@onNull View itemView)99 ViewHolder(@NonNull View itemView) { 100 super(itemView); 101 } 102 bind(ListElement element)103 public abstract void bind(ListElement element); 104 } 105 106 private class ActivityViewHolder extends ViewHolder { 107 private final Button mButton; 108 ActivityViewHolder(@onNull View itemView)109 ActivityViewHolder(@NonNull View itemView) { 110 super(itemView); 111 mButton = itemView.requireViewById(R.id.button); 112 } 113 114 @Override bind(ListElement e)115 public void bind(ListElement e) { 116 if (!(e instanceof ActivityElement)) { 117 throw new IllegalArgumentException("Expected an ActivityElement"); 118 } 119 ActivityElement element = (ActivityElement) e; 120 mButton.setText(element.getText()); 121 mButton.setOnClickListener(v -> 122 startActivity(new Intent(itemView.getContext(), element.getActivity()))); 123 } 124 } 125 126 private static class SwitchViewHolder extends ViewHolder { 127 private final Switch mSwitch; 128 SwitchViewHolder(@onNull View itemView)129 SwitchViewHolder(@NonNull View itemView) { 130 super(itemView); 131 mSwitch = itemView.requireViewById(R.id.button); 132 } 133 134 @Override bind(ListElement e)135 public void bind(ListElement e) { 136 if (!(e instanceof SwitchElement)) { 137 throw new IllegalArgumentException("Expected an ActivityElement"); 138 } 139 SwitchElement element = (SwitchElement) e; 140 mSwitch.setChecked(element.isChecked()); 141 mSwitch.setText(element.getText()); 142 mSwitch.setOnCheckedChangeListener(element.getOnCheckedChangedListener()); 143 } 144 } 145 146 private final RecyclerView.Adapter<ViewHolder> mAdapter = 147 new RecyclerView.Adapter<ViewHolder>() { 148 @NonNull 149 @Override 150 public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 151 LayoutInflater inflater = LayoutInflater.from(MainActivity.this); 152 if (viewType == ListElement.TYPE_ACTIVITY) { 153 return new ActivityViewHolder( 154 inflater.inflate(R.layout.list_item, parent, false)); 155 } else if (viewType == ListElement.TYPE_SWITCH) { 156 return new SwitchViewHolder( 157 inflater.inflate(R.layout.list_item_switch, parent, false)); 158 } else { 159 throw new IllegalArgumentException("Unknown viewType: " + viewType); 160 } 161 } 162 163 @Override 164 public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 165 holder.bind(mActivities.get(position)); 166 } 167 168 @Override 169 public int getItemCount() { 170 return mActivities.size(); 171 } 172 173 @Override 174 public int getItemViewType(int position) { 175 return mActivities.get(position).getType(); 176 } 177 }; 178 179 @Override onCreate(Bundle savedInstanceState)180 protected void onCreate(Bundle savedInstanceState) { 181 super.onCreate(savedInstanceState); 182 setContentView(R.layout.car_ui_recycler_view_activity); 183 184 ToolbarController toolbar = CarUi.requireToolbar(this); 185 toolbar.setLogo(R.drawable.ic_launcher); 186 toolbar.setTitle(getTitle()); 187 188 CarUiRecyclerView prv = findViewById(R.id.list); 189 prv.setAdapter(mAdapter); 190 191 initLeakCanary(); 192 } 193 initLeakCanary()194 private void initLeakCanary() { 195 // This sets LeakCanary to report errors after a single leak instead of 5, and to ask for 196 // permission to use storage, which it needs to work. 197 // 198 // Equivalent to this non-reflection code: 199 // 200 // Config config = LeakCanary.INSTANCE.getConfig(); 201 // LeakCanary.INSTANCE.setConfig(config.copy(config.getDumpHeap(), 202 // config.getDumpHeapWhenDebugging(), 203 // 1, 204 // config.getReferenceMatchers(), 205 // config.getObjectInspectors(), 206 // config.getOnHeapAnalyzedListener(), 207 // config.getMetatadaExtractor(), 208 // config.getComputeRetainedHeapSize(), 209 // config.getMaxStoredHeapDumps(), 210 // true, 211 // config.getUseExperimentalLeakFinders())); 212 try { 213 Class<?> canaryClass = Class.forName("leakcanary.LeakCanary"); 214 try { 215 Class<?> onHeapAnalyzedListenerClass = 216 Class.forName("leakcanary.OnHeapAnalyzedListener"); 217 Class<?> metadataExtractorClass = Class.forName("shark.MetadataExtractor"); 218 Method getConfig = canaryClass.getMethod("getConfig"); 219 Class<?> configClass = getConfig.getReturnType(); 220 Method setConfig = canaryClass.getMethod("setConfig", configClass); 221 Method copy = configClass.getMethod("copy", boolean.class, boolean.class, 222 int.class, List.class, List.class, onHeapAnalyzedListenerClass, 223 metadataExtractorClass, boolean.class, int.class, boolean.class, 224 boolean.class); 225 226 Object canary = canaryClass.getField("INSTANCE").get(null); 227 Object currentConfig = getConfig.invoke(canary); 228 229 Boolean dumpHeap = (Boolean) configClass 230 .getMethod("getDumpHeap").invoke(currentConfig); 231 Boolean dumpHeapWhenDebugging = (Boolean) configClass 232 .getMethod("getDumpHeapWhenDebugging").invoke(currentConfig); 233 List<?> referenceMatchers = (List<?>) configClass 234 .getMethod("getReferenceMatchers").invoke(currentConfig); 235 List<?> objectInspectors = (List<?>) configClass 236 .getMethod("getObjectInspectors").invoke(currentConfig); 237 Object onHeapAnalyzedListener = configClass 238 .getMethod("getOnHeapAnalyzedListener").invoke(currentConfig); 239 // Yes, LeakCanary misspelled metadata 240 Object metadataExtractor = configClass 241 .getMethod("getMetatadaExtractor").invoke(currentConfig); 242 Boolean computeRetainedHeapSize = (Boolean) configClass 243 .getMethod("getComputeRetainedHeapSize").invoke(currentConfig); 244 Integer maxStoredHeapDumps = (Integer) configClass 245 .getMethod("getMaxStoredHeapDumps").invoke(currentConfig); 246 Boolean useExperimentalLeakFinders = (Boolean) configClass 247 .getMethod("getUseExperimentalLeakFinders").invoke(currentConfig); 248 249 setConfig.invoke(canary, copy.invoke(currentConfig, 250 dumpHeap, 251 dumpHeapWhenDebugging, 252 1, 253 referenceMatchers, 254 objectInspectors, 255 onHeapAnalyzedListener, 256 metadataExtractor, 257 computeRetainedHeapSize, 258 maxStoredHeapDumps, 259 true, 260 useExperimentalLeakFinders)); 261 262 } catch (ReflectiveOperationException e) { 263 Log.e("paintbooth", "Error initializing LeakCanary", e); 264 Toast.makeText(this, "Error initializing LeakCanary", Toast.LENGTH_LONG).show(); 265 } 266 } catch (ClassNotFoundException e) { 267 // LeakCanary is not used in this build, do nothing. 268 } 269 } 270 isServiceRunning(Class<? extends Service> serviceClazz)271 private boolean isServiceRunning(Class<? extends Service> serviceClazz) { 272 ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); 273 for (ActivityManager.RunningServiceInfo service : manager.getRunningServices( 274 Integer.MAX_VALUE)) { 275 if (serviceClazz.getName().equals(service.service.getClassName())) { 276 return true; 277 } 278 } 279 return false; 280 } 281 setServiceRunning(Class<? extends Service> serviceClass, boolean running)282 private void setServiceRunning(Class<? extends Service> serviceClass, boolean running) { 283 Intent intent = new Intent(this, serviceClass); 284 if (!running) { 285 intent.setAction(STOP_SERVICE); 286 } 287 startForegroundService(intent); 288 } 289 isSharedLibEnabled()290 private boolean isSharedLibEnabled() { 291 return getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE) 292 .getStringSet(SHARED_PREFERENCES_SHARED_LIB_DENYLIST, null) == null; 293 } 294 onSharedLibSwitchChanged(CompoundButton unused, boolean checked)295 private void onSharedLibSwitchChanged(CompoundButton unused, boolean checked) { 296 getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE) 297 .edit() 298 .putStringSet(SHARED_PREFERENCES_SHARED_LIB_DENYLIST, 299 checked ? null : Collections.singleton("com.chassis.car.ui.sharedlibrary")) 300 .apply(); 301 Toast.makeText(this, "Relaunch PaintBooth to see effects", Toast.LENGTH_SHORT).show(); 302 } 303 304 @Override onCarUiInsetsChanged(@onNull Insets insets)305 public void onCarUiInsetsChanged(@NonNull Insets insets) { 306 FocusArea focusArea = requireViewById(R.id.focus_area); 307 focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom()); 308 focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom()); 309 requireViewById(R.id.list) 310 .setPadding(0, insets.getTop(), 0, insets.getBottom()); 311 requireViewById(android.R.id.content) 312 .setPadding(insets.getLeft(), 0, insets.getRight(), 0); 313 } 314 315 private abstract static class ListElement { 316 static final int TYPE_ACTIVITY = 0; 317 static final int TYPE_SWITCH = 1; 318 319 private final String mText; 320 ListElement(String text)321 ListElement(String text) { 322 mText = text; 323 } 324 getText()325 String getText() { 326 return mText; 327 } 328 getType()329 abstract int getType(); 330 } 331 332 private static class ActivityElement extends ListElement { 333 private final Class<? extends Activity> mActivityClass; 334 ActivityElement(String text, Class<? extends Activity> activityClass)335 ActivityElement(String text, Class<? extends Activity> activityClass) { 336 super(text); 337 mActivityClass = activityClass; 338 } 339 getActivity()340 Class<? extends Activity> getActivity() { 341 return mActivityClass; 342 } 343 344 @Override getType()345 int getType() { 346 return TYPE_ACTIVITY; 347 } 348 } 349 350 private static class SwitchElement extends ListElement { 351 private final Supplier<Boolean> mIsCheckedSupplier; 352 private final OnCheckedChangeListener mOnCheckedChanged; 353 SwitchElement(String text, Supplier<Boolean> isCheckedSupplier, OnCheckedChangeListener onCheckedChanged)354 private SwitchElement(String text, Supplier<Boolean> isCheckedSupplier, 355 OnCheckedChangeListener onCheckedChanged) { 356 super(text); 357 mIsCheckedSupplier = isCheckedSupplier; 358 mOnCheckedChanged = onCheckedChanged; 359 } 360 isChecked()361 public boolean isChecked() { 362 return mIsCheckedSupplier.get(); 363 } 364 getOnCheckedChangedListener()365 public OnCheckedChangeListener getOnCheckedChangedListener() { 366 return mOnCheckedChanged; 367 } 368 369 @Override getType()370 int getType() { 371 return TYPE_SWITCH; 372 } 373 } 374 375 private class ServiceElement extends SwitchElement { ServiceElement(String text, Class<? extends Service> serviceClass)376 ServiceElement(String text, Class<? extends Service> serviceClass) { 377 super(text, 378 () -> isServiceRunning(serviceClass), 379 (v, checked) -> setServiceRunning(serviceClass, checked)); 380 } 381 } 382 } 383