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