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 
17 package androidx.appcompat.view;
18 
19 import android.content.Context;
20 import android.content.ContextWrapper;
21 import android.content.res.AssetManager;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.os.Build;
25 import android.view.LayoutInflater;
26 
27 import androidx.annotation.RequiresApi;
28 import androidx.annotation.StyleRes;
29 import androidx.appcompat.R;
30 
31 /**
32  * A context wrapper that allows you to modify or replace the theme of the wrapped context.
33  */
34 public class ContextThemeWrapper extends ContextWrapper {
35     /**
36      * Lazily-populated configuration object representing an empty, default configuration.
37      */
38     private static Configuration sEmptyConfig;
39 
40     private int mThemeResource;
41     private Resources.Theme mTheme;
42     private LayoutInflater mInflater;
43     private Configuration mOverrideConfiguration;
44     private Resources mResources;
45 
46     /**
47      * Creates a new context wrapper with no theme and no base context.
48      * <p class="note">
49      * <strong>Note:</strong> A base context <strong>must</strong> be attached
50      * using {@link #attachBaseContext(Context)} before calling any other
51      * method on the newly constructed context wrapper.
52      */
ContextThemeWrapper()53     public ContextThemeWrapper() {
54         super(null);
55     }
56 
57     /**
58      * Creates a new context wrapper with the specified theme.
59      * <p>
60      * The specified theme will be applied on top of the base context's theme.
61      * Any attributes not explicitly defined in the theme identified by
62      * <var>themeResId</var> will retain their original values.
63      *
64      * @param base the base context
65      * @param themeResId the resource ID of the theme to be applied on top of
66      *                   the base context's theme
67      */
ContextThemeWrapper(Context base, @StyleRes int themeResId)68     public ContextThemeWrapper(Context base, @StyleRes int themeResId) {
69         super(base);
70         mThemeResource = themeResId;
71     }
72 
73     /**
74      * Creates a new context wrapper with the specified theme.
75      * <p>
76      * Unlike {@link #ContextThemeWrapper(Context, int)}, the theme passed to
77      * this constructor will completely replace the base context's theme.
78      *
79      * @param base the base context
80      * @param theme the theme against which resources should be inflated
81      */
ContextThemeWrapper(Context base, Resources.Theme theme)82     public ContextThemeWrapper(Context base, Resources.Theme theme) {
83         super(base);
84         mTheme = theme;
85     }
86 
87     @Override
attachBaseContext(Context newBase)88     protected void attachBaseContext(Context newBase) {
89         super.attachBaseContext(newBase);
90     }
91 
92     /**
93      * Call to set an "override configuration" on this context -- this is
94      * a configuration that replies one or more values of the standard
95      * configuration that is applied to the context.  See
96      * {@link Context#createConfigurationContext(Configuration)} for more
97      * information.
98      *
99      * <p>This method can only be called once, and must be called before any
100      * calls to {@link #getResources()} or {@link #getAssets()} are made.
101      */
applyOverrideConfiguration(Configuration overrideConfiguration)102     public void applyOverrideConfiguration(Configuration overrideConfiguration) {
103         if (mResources != null) {
104             throw new IllegalStateException(
105                     "getResources() or getAssets() has already been called");
106         }
107         if (mOverrideConfiguration != null) {
108             throw new IllegalStateException("Override configuration has already been set");
109         }
110         mOverrideConfiguration = new Configuration(overrideConfiguration);
111     }
112 
113     @Override
getResources()114     public Resources getResources() {
115         return getResourcesInternal();
116     }
117 
getResourcesInternal()118     private Resources getResourcesInternal() {
119         if (mResources == null) {
120             if (mOverrideConfiguration == null
121                     || (Build.VERSION.SDK_INT >= 26
122                     && isEmptyConfiguration(mOverrideConfiguration))) {
123                 // If we're not applying any overrides, use the base context's resources. On API
124                 // 26+, this will avoid pulling in resources that share a backing implementation
125                 // with the application context.
126                 mResources = super.getResources();
127             } else {
128                 final Context resContext =
129                         createConfigurationContext(mOverrideConfiguration);
130                 mResources = resContext.getResources();
131             }
132         }
133         return mResources;
134     }
135 
136     @Override
setTheme(int resid)137     public void setTheme(int resid) {
138         if (mThemeResource != resid) {
139             mThemeResource = resid;
140             initializeTheme();
141         }
142     }
143 
144     /**
145      * Returns the resource ID of the theme that is to be applied on top of the base context's
146      * theme.
147      */
getThemeResId()148     public int getThemeResId() {
149         return mThemeResource;
150     }
151 
152     @Override
getTheme()153     public Resources.Theme getTheme() {
154         if (mTheme != null) {
155             return mTheme;
156         }
157 
158         if (mThemeResource == 0) {
159             mThemeResource = R.style.Theme_AppCompat_Light;
160         }
161         initializeTheme();
162 
163         return mTheme;
164     }
165 
166     @Override
getSystemService(String name)167     public Object getSystemService(String name) {
168         if (LAYOUT_INFLATER_SERVICE.equals(name)) {
169             if (mInflater == null) {
170                 mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
171             }
172             return mInflater;
173         }
174         return getBaseContext().getSystemService(name);
175     }
176 
177     /**
178      * Called by {@link #setTheme} and {@link #getTheme} to apply a theme
179      * resource to the current Theme object.  Can override to change the
180      * default (simple) behavior.  This method will not be called in multiple
181      * threads simultaneously.
182      *
183      * @param theme The Theme object being modified.
184      * @param resid The theme style resource being applied to <var>theme</var>.
185      * @param first Set to true if this is the first time a style is being
186      *              applied to <var>theme</var>.
187      */
onApplyThemeResource(Resources.Theme theme, int resid, boolean first)188     protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
189         theme.applyStyle(resid, true);
190     }
191 
initializeTheme()192     private void initializeTheme() {
193         final boolean first = mTheme == null;
194         if (first) {
195             mTheme = getResources().newTheme();
196             Resources.Theme theme = getBaseContext().getTheme();
197             if (theme != null) {
198                 mTheme.setTo(theme);
199             }
200         }
201         onApplyThemeResource(mTheme, mThemeResource, first);
202     }
203 
204     @Override
getAssets()205     public AssetManager getAssets() {
206         // Ensure we're returning assets with the correct configuration.
207         return getResources().getAssets();
208     }
209 
210     /**
211      * @return {@code true} if the specified configuration is {@code null} or is a no-op when
212      *         used as a configuration overlay
213      */
214     @RequiresApi(26)
isEmptyConfiguration(Configuration overrideConfiguration)215     private static boolean isEmptyConfiguration(Configuration overrideConfiguration) {
216         if (overrideConfiguration == null) {
217             return true;
218         }
219 
220         if (sEmptyConfig == null) {
221             Configuration emptyConfig = new Configuration();
222             // Workaround for incorrect default fontScale on earlier SDKs (b/29924927). Note
223             // that Configuration.setToDefaults() is *not* a no-op configuration overlay.
224             emptyConfig.fontScale = 0.0f;
225             sEmptyConfig = emptyConfig;
226         }
227 
228         return overrideConfiguration.equals(sEmptyConfig);
229     }
230 }
231 
232