1 /* 2 * Copyright (C) 2015 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.layoutlib.bridge.bars; 18 19 import com.android.ide.common.rendering.api.LayoutLog; 20 import com.android.ide.common.rendering.api.LayoutlibCallback; 21 import com.android.ide.common.rendering.api.RenderResources; 22 import com.android.ide.common.rendering.api.ResourceValue; 23 import com.android.ide.common.rendering.api.SessionParams; 24 import com.android.ide.common.rendering.api.StyleResourceValue; 25 import com.android.layoutlib.bridge.Bridge; 26 import com.android.layoutlib.bridge.android.BridgeContext; 27 import com.android.layoutlib.bridge.impl.ResourceHelper; 28 import com.android.resources.ResourceType; 29 import com.android.tools.layoutlib.annotations.NotNull; 30 31 import android.annotation.NonNull; 32 import android.annotation.Nullable; 33 import android.content.Context; 34 import android.graphics.drawable.Drawable; 35 import android.view.ContextThemeWrapper; 36 import android.view.LayoutInflater; 37 import android.view.Menu; 38 import android.view.MenuInflater; 39 import android.view.View; 40 import android.widget.FrameLayout; 41 42 import java.lang.reflect.Field; 43 import java.lang.reflect.InvocationTargetException; 44 import java.lang.reflect.Method; 45 import java.util.List; 46 47 import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; 48 import static com.android.resources.ResourceType.MENU; 49 50 51 /** 52 * Assumes that the AppCompat library is present in the project's classpath and creates an 53 * actionbar around it. 54 */ 55 public class AppCompatActionBar extends BridgeActionBar { 56 57 private Object mWindowDecorActionBar; 58 private static final String[] WINDOW_ACTION_BAR_CLASS_NAMES = { 59 "android.support.v7.internal.app.WindowDecorActionBar", 60 "android.support.v7.app.WindowDecorActionBar", // This is used on v23.1.1 and later. 61 "androidx.app.WindowDecorActionBar" // User from v27 62 }; 63 64 private Class<?> mWindowActionBarClass; 65 66 /** 67 * Inflate the action bar and attach it to {@code parentView} 68 */ AppCompatActionBar(@onNull BridgeContext context, @NonNull SessionParams params)69 public AppCompatActionBar(@NonNull BridgeContext context, @NonNull SessionParams params) { 70 super(context, params); 71 int contentRootId = context.getProjectResourceValue(ResourceType.ID, 72 "action_bar_activity_content", 0); 73 View contentView = getDecorContent().findViewById(contentRootId); 74 if (contentView != null) { 75 assert contentView instanceof FrameLayout; 76 setContentRoot((FrameLayout) contentView); 77 } else { 78 // Something went wrong. Create a new FrameLayout in the enclosing layout. 79 FrameLayout contentRoot = new FrameLayout(context); 80 setMatchParent(contentRoot); 81 if (mEnclosingLayout != null) { 82 mEnclosingLayout.addView(contentRoot); 83 } 84 setContentRoot(contentRoot); 85 } 86 try { 87 Class[] constructorParams = {View.class}; 88 Object[] constructorArgs = {getDecorContent()}; 89 LayoutlibCallback callback = params.getLayoutlibCallback(); 90 91 // Find the correct WindowActionBar class 92 String actionBarClass = null; 93 for (int i = WINDOW_ACTION_BAR_CLASS_NAMES.length - 1; i >= 0; i--) { 94 actionBarClass = WINDOW_ACTION_BAR_CLASS_NAMES[i]; 95 try { 96 callback.findClass(actionBarClass); 97 98 break; 99 } catch (ClassNotFoundException ignore) { 100 } 101 } 102 103 mWindowDecorActionBar = callback.loadView(actionBarClass, 104 constructorParams, constructorArgs); 105 mWindowActionBarClass = mWindowDecorActionBar == null ? null : 106 mWindowDecorActionBar.getClass(); 107 inflateMenus(); 108 setupActionBar(); 109 } catch (Exception e) { 110 Bridge.getLog().warning(LayoutLog.TAG_BROKEN, 111 "Failed to load AppCompat ActionBar with unknown error.", e); 112 } 113 } 114 115 @Override getLayoutResource(BridgeContext context)116 protected ResourceValue getLayoutResource(BridgeContext context) { 117 // We always assume that the app has requested the action bar. 118 return context.getRenderResources().getProjectResource(ResourceType.LAYOUT, 119 "abc_screen_toolbar"); 120 } 121 122 @Override getInflater(BridgeContext context)123 protected LayoutInflater getInflater(BridgeContext context) { 124 // Other than the resource resolution part, the code has been taken from the support 125 // library. see code from line 269 onwards in 126 // https://android.googlesource.com/platform/frameworks/support/+/android-5.1.0_r1/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java 127 Context themedContext = context; 128 RenderResources resources = context.getRenderResources(); 129 ResourceValue actionBarTheme = resources.findItemInTheme("actionBarTheme", false); 130 if (actionBarTheme != null) { 131 // resolve it, if needed. 132 actionBarTheme = resources.resolveResValue(actionBarTheme); 133 } 134 if (actionBarTheme instanceof StyleResourceValue) { 135 int styleId = context.getDynamicIdByStyle(((StyleResourceValue) actionBarTheme)); 136 if (styleId != 0) { 137 themedContext = new ContextThemeWrapper(context, styleId); 138 } 139 } 140 return LayoutInflater.from(themedContext); 141 } 142 143 @Override setTitle(CharSequence title)144 protected void setTitle(CharSequence title) { 145 if (title != null && mWindowDecorActionBar != null) { 146 Method setTitle = getMethod(mWindowActionBarClass, "setTitle", CharSequence.class); 147 invoke(setTitle, mWindowDecorActionBar, title); 148 } 149 } 150 151 @Override setSubtitle(CharSequence subtitle)152 protected void setSubtitle(CharSequence subtitle) { 153 if (subtitle != null && mWindowDecorActionBar != null) { 154 Method setSubtitle = getMethod(mWindowActionBarClass, "setSubtitle", CharSequence.class); 155 invoke(setSubtitle, mWindowDecorActionBar, subtitle); 156 } 157 } 158 159 @Override setIcon(String icon)160 protected void setIcon(String icon) { 161 // Do this only if the action bar doesn't already have an icon. 162 if (icon != null && !icon.isEmpty() && mWindowDecorActionBar != null) { 163 if (invoke(getMethod(mWindowActionBarClass, "hasIcon"), mWindowDecorActionBar) 164 == Boolean.TRUE) { 165 Drawable iconDrawable = getDrawable(icon, false); 166 if (iconDrawable != null) { 167 Method setIcon = getMethod(mWindowActionBarClass, "setIcon", Drawable.class); 168 invoke(setIcon, mWindowDecorActionBar, iconDrawable); 169 } 170 } 171 } 172 } 173 174 @Override setHomeAsUp(boolean homeAsUp)175 protected void setHomeAsUp(boolean homeAsUp) { 176 if (mWindowDecorActionBar != null) { 177 Method setHomeAsUp = getMethod(mWindowActionBarClass, 178 "setDefaultDisplayHomeAsUpEnabled", boolean.class); 179 invoke(setHomeAsUp, mWindowDecorActionBar, homeAsUp); 180 } 181 } 182 inflateMenus()183 private void inflateMenus() { 184 List<String> menuNames = getCallBack().getMenuIdNames(); 185 if (menuNames.isEmpty()) { 186 return; 187 } 188 189 if (menuNames.size() > 1) { 190 // Supporting multiple menus means that we would need to instantiate our own supportlib 191 // MenuInflater instances using reflection 192 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 193 "Support Toolbar does not currently support multiple menus in the preview.", 194 null, null, null); 195 } 196 197 String name = menuNames.get(0); 198 int id; 199 if (name.startsWith(ANDROID_NS_NAME_PREFIX)) { 200 // Framework menu. 201 name = name.substring(ANDROID_NS_NAME_PREFIX.length()); 202 id = mBridgeContext.getFrameworkResourceValue(MENU, name, -1); 203 } else { 204 // Project menu. 205 id = mBridgeContext.getProjectResourceValue(MENU, name, -1); 206 } 207 if (id < 1) { 208 return; 209 } 210 // Get toolbar decorator 211 Object mDecorToolbar = getFieldValue(mWindowDecorActionBar, "mDecorToolbar"); 212 if (mDecorToolbar == null) { 213 return; 214 } 215 216 Class<?> mDecorToolbarClass = mDecorToolbar.getClass(); 217 Context themedContext = (Context)invoke( 218 getMethod(mWindowActionBarClass, "getThemedContext"), 219 mWindowDecorActionBar); 220 MenuInflater inflater = new MenuInflater(themedContext); 221 Menu menuBuilder = (Menu)invoke(getMethod(mDecorToolbarClass, "getMenu"), mDecorToolbar); 222 inflater.inflate(id, menuBuilder); 223 224 // Set the actual menu 225 invoke(findMethod(mDecorToolbarClass, "setMenu"), mDecorToolbar, menuBuilder, null); 226 } 227 228 @Override createMenuPopup()229 public void createMenuPopup() { 230 // it's hard to add menus to appcompat's actionbar, since it'll use a lot of reflection. 231 // so we skip it for now. 232 } 233 234 @Nullable getMethod(Class<?> owner, String name, Class<?>... parameterTypes)235 private static Method getMethod(Class<?> owner, String name, Class<?>... parameterTypes) { 236 try { 237 return owner == null ? null : owner.getMethod(name, parameterTypes); 238 } catch (NoSuchMethodException e) { 239 e.printStackTrace(); 240 } 241 return null; 242 } 243 244 /** 245 * Same as getMethod but doesn't require the parameterTypes. This allows us to call methods 246 * without having to get all the types for the parameters when we do not need them 247 */ 248 @Nullable findMethod(@ullable Class<?> owner, @NotNull String name)249 private static Method findMethod(@Nullable Class<?> owner, @NotNull String name) { 250 if (owner == null) { 251 return null; 252 } 253 for (Method method : owner.getMethods()) { 254 if (name.equals(method.getName())) { 255 return method; 256 } 257 } 258 259 return null; 260 } 261 262 @Nullable getFieldValue(@ullable Object instance, @NotNull String name)263 private static Object getFieldValue(@Nullable Object instance, @NotNull String name) { 264 if (instance == null) { 265 return null; 266 } 267 268 Class<?> instanceClass = instance.getClass(); 269 try { 270 Field field = instanceClass.getDeclaredField(name); 271 boolean accesible = field.isAccessible(); 272 if (!accesible) { 273 field.setAccessible(true); 274 } 275 try { 276 return field.get(instance); 277 } finally { 278 field.setAccessible(accesible); 279 } 280 } catch (NoSuchFieldException | IllegalAccessException e) { 281 e.printStackTrace(); 282 } 283 return null; 284 } 285 286 @Nullable invoke(@ullable Method method, Object owner, Object... args)287 private static Object invoke(@Nullable Method method, Object owner, Object... args) { 288 try { 289 return method == null ? null : method.invoke(owner, args); 290 } catch (InvocationTargetException | IllegalAccessException e) { 291 e.printStackTrace(); 292 } 293 return null; 294 } 295 296 // TODO: this is duplicated from FrameworkActionBarWrapper$WindowActionBarWrapper 297 @Nullable getDrawable(@onNull String name, boolean isFramework)298 private Drawable getDrawable(@NonNull String name, boolean isFramework) { 299 RenderResources res = mBridgeContext.getRenderResources(); 300 ResourceValue value = res.findResValue(name, isFramework); 301 value = res.resolveResValue(value); 302 if (value != null) { 303 return ResourceHelper.getDrawable(value, mBridgeContext); 304 } 305 return null; 306 } 307 } 308