1/* 2 * Copyright (C) 2014 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 17package android.support.v7.app; 18 19import android.content.Context; 20import android.content.ContextWrapper; 21import android.content.res.TypedArray; 22import android.os.Build; 23import android.support.annotation.NonNull; 24import android.support.annotation.Nullable; 25import android.support.v4.util.ArrayMap; 26import android.support.v4.view.ViewCompat; 27import android.support.v7.appcompat.R; 28import android.support.v7.view.ContextThemeWrapper; 29import android.support.v7.widget.AppCompatAutoCompleteTextView; 30import android.support.v7.widget.AppCompatButton; 31import android.support.v7.widget.AppCompatCheckBox; 32import android.support.v7.widget.AppCompatCheckedTextView; 33import android.support.v7.widget.AppCompatEditText; 34import android.support.v7.widget.AppCompatImageButton; 35import android.support.v7.widget.AppCompatImageView; 36import android.support.v7.widget.AppCompatMultiAutoCompleteTextView; 37import android.support.v7.widget.AppCompatRadioButton; 38import android.support.v7.widget.AppCompatRatingBar; 39import android.support.v7.widget.AppCompatSeekBar; 40import android.support.v7.widget.AppCompatSpinner; 41import android.support.v7.widget.AppCompatTextView; 42import android.support.v7.widget.TintContextWrapper; 43import android.util.AttributeSet; 44import android.util.Log; 45import android.view.InflateException; 46import android.view.View; 47 48import java.lang.reflect.Constructor; 49import java.lang.reflect.InvocationTargetException; 50import java.lang.reflect.Method; 51import java.util.Map; 52 53/** 54 * This class is responsible for manually inflating our tinted widgets which are used on devices 55 * running {@link android.os.Build.VERSION_CODES#KITKAT KITKAT} or below. As such, this class 56 * should only be used when running on those devices. 57 * <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of 58 * the framework versions in layout inflation; the second is backport the {@code android:theme} 59 * functionality for any inflated widgets. This include theme inheritance from it's parent. 60 */ 61class AppCompatViewInflater { 62 63 private static final Class<?>[] sConstructorSignature = new Class[]{ 64 Context.class, AttributeSet.class}; 65 private static final int[] sOnClickAttrs = new int[]{android.R.attr.onClick}; 66 67 private static final String[] sClassPrefixList = { 68 "android.widget.", 69 "android.view.", 70 "android.webkit." 71 }; 72 73 private static final String LOG_TAG = "AppCompatViewInflater"; 74 75 private static final Map<String, Constructor<? extends View>> sConstructorMap 76 = new ArrayMap<>(); 77 78 private final Object[] mConstructorArgs = new Object[2]; 79 80 public final View createView(View parent, final String name, @NonNull Context context, 81 @NonNull AttributeSet attrs, boolean inheritContext, 82 boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { 83 final Context originalContext = context; 84 85 // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy 86 // by using the parent's context 87 if (inheritContext && parent != null) { 88 context = parent.getContext(); 89 } 90 if (readAndroidTheme || readAppTheme) { 91 // We then apply the theme on the context, if specified 92 context = themifyContext(context, attrs, readAndroidTheme, readAppTheme); 93 } 94 if (wrapContext) { 95 context = TintContextWrapper.wrap(context); 96 } 97 98 View view = null; 99 100 // We need to 'inject' our tint aware Views in place of the standard framework versions 101 switch (name) { 102 case "TextView": 103 view = new AppCompatTextView(context, attrs); 104 break; 105 case "ImageView": 106 view = new AppCompatImageView(context, attrs); 107 break; 108 case "Button": 109 view = new AppCompatButton(context, attrs); 110 break; 111 case "EditText": 112 view = new AppCompatEditText(context, attrs); 113 break; 114 case "Spinner": 115 view = new AppCompatSpinner(context, attrs); 116 break; 117 case "ImageButton": 118 view = new AppCompatImageButton(context, attrs); 119 break; 120 case "CheckBox": 121 view = new AppCompatCheckBox(context, attrs); 122 break; 123 case "RadioButton": 124 view = new AppCompatRadioButton(context, attrs); 125 break; 126 case "CheckedTextView": 127 view = new AppCompatCheckedTextView(context, attrs); 128 break; 129 case "AutoCompleteTextView": 130 view = new AppCompatAutoCompleteTextView(context, attrs); 131 break; 132 case "MultiAutoCompleteTextView": 133 view = new AppCompatMultiAutoCompleteTextView(context, attrs); 134 break; 135 case "RatingBar": 136 view = new AppCompatRatingBar(context, attrs); 137 break; 138 case "SeekBar": 139 view = new AppCompatSeekBar(context, attrs); 140 break; 141 } 142 143 if (view == null && originalContext != context) { 144 // If the original context does not equal our themed context, then we need to manually 145 // inflate it using the name so that android:theme takes effect. 146 view = createViewFromTag(context, name, attrs); 147 } 148 149 if (view != null) { 150 // If we have created a view, check it's android:onClick 151 checkOnClickListener(view, attrs); 152 } 153 154 return view; 155 } 156 157 private View createViewFromTag(Context context, String name, AttributeSet attrs) { 158 if (name.equals("view")) { 159 name = attrs.getAttributeValue(null, "class"); 160 } 161 162 try { 163 mConstructorArgs[0] = context; 164 mConstructorArgs[1] = attrs; 165 166 if (-1 == name.indexOf('.')) { 167 for (int i = 0; i < sClassPrefixList.length; i++) { 168 final View view = createView(context, name, sClassPrefixList[i]); 169 if (view != null) { 170 return view; 171 } 172 } 173 return null; 174 } else { 175 return createView(context, name, null); 176 } 177 } catch (Exception e) { 178 // We do not want to catch these, lets return null and let the actual LayoutInflater 179 // try 180 return null; 181 } finally { 182 // Don't retain references on context. 183 mConstructorArgs[0] = null; 184 mConstructorArgs[1] = null; 185 } 186 } 187 188 /** 189 * android:onClick doesn't handle views with a ContextWrapper context. This method 190 * backports new framework functionality to traverse the Context wrappers to find a 191 * suitable target. 192 */ 193 private void checkOnClickListener(View view, AttributeSet attrs) { 194 final Context context = view.getContext(); 195 196 if (!(context instanceof ContextWrapper) || 197 (Build.VERSION.SDK_INT >= 15 && !ViewCompat.hasOnClickListeners(view))) { 198 // Skip our compat functionality if: the Context isn't a ContextWrapper, or 199 // the view doesn't have an OnClickListener (we can only rely on this on API 15+ so 200 // always use our compat code on older devices) 201 return; 202 } 203 204 final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs); 205 final String handlerName = a.getString(0); 206 if (handlerName != null) { 207 view.setOnClickListener(new DeclaredOnClickListener(view, handlerName)); 208 } 209 a.recycle(); 210 } 211 212 private View createView(Context context, String name, String prefix) 213 throws ClassNotFoundException, InflateException { 214 Constructor<? extends View> constructor = sConstructorMap.get(name); 215 216 try { 217 if (constructor == null) { 218 // Class not found in the cache, see if it's real, and try to add it 219 Class<? extends View> clazz = context.getClassLoader().loadClass( 220 prefix != null ? (prefix + name) : name).asSubclass(View.class); 221 222 constructor = clazz.getConstructor(sConstructorSignature); 223 sConstructorMap.put(name, constructor); 224 } 225 constructor.setAccessible(true); 226 return constructor.newInstance(mConstructorArgs); 227 } catch (Exception e) { 228 // We do not want to catch these, lets return null and let the actual LayoutInflater 229 // try 230 return null; 231 } 232 } 233 234 /** 235 * Allows us to emulate the {@code android:theme} attribute for devices before L. 236 */ 237 private static Context themifyContext(Context context, AttributeSet attrs, 238 boolean useAndroidTheme, boolean useAppTheme) { 239 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0); 240 int themeId = 0; 241 if (useAndroidTheme) { 242 // First try reading android:theme if enabled 243 themeId = a.getResourceId(R.styleable.View_android_theme, 0); 244 } 245 if (useAppTheme && themeId == 0) { 246 // ...if that didn't work, try reading app:theme (for legacy reasons) if enabled 247 themeId = a.getResourceId(R.styleable.View_theme, 0); 248 249 if (themeId != 0) { 250 Log.i(LOG_TAG, "app:theme is now deprecated. " 251 + "Please move to using android:theme instead."); 252 } 253 } 254 a.recycle(); 255 256 if (themeId != 0 && (!(context instanceof ContextThemeWrapper) 257 || ((ContextThemeWrapper) context).getThemeResId() != themeId)) { 258 // If the context isn't a ContextThemeWrapper, or it is but does not have 259 // the same theme as we need, wrap it in a new wrapper 260 context = new ContextThemeWrapper(context, themeId); 261 } 262 return context; 263 } 264 265 /** 266 * An implementation of OnClickListener that attempts to lazily load a 267 * named click handling method from a parent or ancestor context. 268 */ 269 private static class DeclaredOnClickListener implements View.OnClickListener { 270 private final View mHostView; 271 private final String mMethodName; 272 273 private Method mResolvedMethod; 274 private Context mResolvedContext; 275 276 public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) { 277 mHostView = hostView; 278 mMethodName = methodName; 279 } 280 281 @Override 282 public void onClick(@NonNull View v) { 283 if (mResolvedMethod == null) { 284 resolveMethod(mHostView.getContext(), mMethodName); 285 } 286 287 try { 288 mResolvedMethod.invoke(mResolvedContext, v); 289 } catch (IllegalAccessException e) { 290 throw new IllegalStateException( 291 "Could not execute non-public method for android:onClick", e); 292 } catch (InvocationTargetException e) { 293 throw new IllegalStateException( 294 "Could not execute method for android:onClick", e); 295 } 296 } 297 298 @NonNull 299 private void resolveMethod(@Nullable Context context, @NonNull String name) { 300 while (context != null) { 301 try { 302 if (!context.isRestricted()) { 303 final Method method = context.getClass().getMethod(mMethodName, View.class); 304 if (method != null) { 305 mResolvedMethod = method; 306 mResolvedContext = context; 307 return; 308 } 309 } 310 } catch (NoSuchMethodException e) { 311 // Failed to find method, keep searching up the hierarchy. 312 } 313 314 if (context instanceof ContextWrapper) { 315 context = ((ContextWrapper) context).getBaseContext(); 316 } else { 317 // Can't search up the hierarchy, null out and fail. 318 context = null; 319 } 320 } 321 322 final int id = mHostView.getId(); 323 final String idText = id == View.NO_ID ? "" : " with id '" 324 + mHostView.getContext().getResources().getResourceEntryName(id) + "'"; 325 throw new IllegalStateException("Could not find method " + mMethodName 326 + "(View) in a parent or ancestor Context for android:onClick " 327 + "attribute defined on view " + mHostView.getClass() + idText); 328 } 329 } 330} 331