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 android.theme.app;
18 
19 import static android.theme.app.TestConfiguration.LAYOUTS;
20 import static android.theme.app.TestConfiguration.THEMES;
21 
22 import android.animation.ValueAnimator;
23 import android.app.Activity;
24 import android.app.KeyguardManager;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.res.Configuration;
28 import android.graphics.Paint;
29 import android.graphics.Typeface;
30 import android.os.Bundle;
31 import android.theme.app.modifiers.AbstractLayoutModifier;
32 import android.util.Log;
33 import android.util.Pair;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.WindowManager.LayoutParams;
37 import android.widget.DatePicker;
38 
39 import java.io.File;
40 import java.util.Arrays;
41 import java.util.List;
42 
43 /**
44  * A activity which display various UI elements with non-modifiable themes.
45  */
46 public class ThemeDeviceActivity extends Activity {
47     public static final String EXTRA_THEME = "theme";
48     public static final String EXTRA_OUTPUT_DIR = "outputDir";
49 
50     private static final String TAG = "ThemeDeviceActivity";
51 
52     /**
53      * Delay that allows the Holo-style CalendarView to settle to its final
54      * position.
55      */
56     private static final long HOLO_CALENDAR_VIEW_ADJUSTMENT_DURATION = 540;
57 
58     private ThemeInfo mTheme;
59     private ReferenceViewGroup mViewGroup;
60     private File mOutputDir;
61     private int mLayoutIndex;
62     private boolean mIsRunning;
63 
64     private Pair<List<Typeface>, List<Typeface>> mOldFontFamilies;
65 
66     @Override
onCreate(Bundle icicle)67     protected void onCreate(Bundle icicle) {
68         super.onCreate(icicle);
69 
70         File fontFile = TypefaceTestUtil.getFirstFont("abc", new Paint());
71         if (!fontFile.getName().startsWith("Roboto")) {
72             Typeface robotoRegular = TypefaceTestUtil.getRobotoTypeface(400, false);
73             Typeface robotoBold = Typeface.create(robotoRegular, 700, false);
74             Typeface robotoItalic = Typeface.create(robotoRegular, 400, true);
75             Typeface robotoBoldItalic = Typeface.create(robotoRegular, 700, true);
76 
77             mOldFontFamilies = Typeface.changeDefaultFontForTest(
78                     Arrays.asList(robotoRegular, robotoBold, robotoItalic, robotoBoldItalic),
79                     Arrays.asList(robotoRegular, Typeface.SERIF, Typeface.MONOSPACE));
80         }
81 
82         final Intent intent = getIntent();
83         final int themeIndex = intent.getIntExtra(EXTRA_THEME, -1);
84         if (themeIndex < 0) {
85             Log.e(TAG, "No theme specified");
86             finish();
87         }
88 
89         final String outputDir = intent.getStringExtra(EXTRA_OUTPUT_DIR);
90         if (outputDir == null) {
91             Log.e(TAG, "No output directory specified");
92             finish();
93         }
94 
95         mOutputDir = new File(outputDir);
96         mTheme = THEMES[themeIndex];
97 
98         // Disable animations.
99         ValueAnimator.setDurationScale(0);
100 
101         // Force text scaling to 1.0 regardless of system default.
102         Configuration config = new Configuration();
103         config.fontScale = 1.0f;
104 
105         Context inflationContext = createConfigurationContext(config);
106         inflationContext.setTheme(mTheme.id);
107 
108         LayoutInflater layoutInflater = LayoutInflater.from(inflationContext);
109         setContentView(layoutInflater.inflate(R.layout.theme_test, null));
110 
111         mViewGroup = findViewById(R.id.reference_view_group);
112 
113         getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
114 
115         // The activity has been created, but we don't want to start image generation until various
116         // asynchronous conditions are satisfied.
117         new ConditionCheck(this, () -> setNextLayout(), message -> finish(message, false))
118                 .addCondition("Device is unlocked",
119                         () -> !getSystemService(KeyguardManager.class).isDeviceLocked())
120                 .addCondition("Window is focused",
121                         () -> hasWindowFocus())
122                 .addCondition("Activity is resumed",
123                         () -> mIsRunning)
124                 .start();
125     }
126 
127     @Override
onResume()128     protected void onResume() {
129         super.onResume();
130 
131         mIsRunning = true;
132     }
133 
134     @Override
onPause()135     protected void onPause() {
136         mIsRunning = false;
137 
138         if (!isFinishing()) {
139             // The activity paused for some reason, likely a system crash
140             // dialog. Finish it so we can move to the next theme.
141             Log.e(TAG, "onPause() called without a call to finish(), check system dialogs");
142         }
143 
144         super.onPause();
145     }
146 
147     @Override
onDestroy()148     protected void onDestroy() {
149         if (mLayoutIndex < LAYOUTS.length) {
150             finish("Only rendered " + mLayoutIndex + "/" + LAYOUTS.length + " layouts", false);
151         }
152 
153         if (mOldFontFamilies != null) {
154             Typeface.changeDefaultFontForTest(mOldFontFamilies.first, mOldFontFamilies.second);
155         }
156 
157         super.onDestroy();
158     }
159 
160     /**
161      * Sets the next layout in the UI.
162      */
setNextLayout()163     private void setNextLayout() {
164         if (mLayoutIndex >= LAYOUTS.length) {
165             finish("Rendered all layouts", true);
166             return;
167         }
168 
169         mViewGroup.removeAllViews();
170 
171         final LayoutInfo layout = LAYOUTS[mLayoutIndex++];
172         final AbstractLayoutModifier modifier = layout.modifier;
173         final String layoutName = String.format("%s_%s", mTheme.name, layout.name);
174         final LayoutInflater layoutInflater = LayoutInflater.from(mViewGroup.getContext());
175         final View view = layoutInflater.inflate(layout.id, mViewGroup, false);
176         if (modifier != null) {
177             modifier.modifyViewBeforeAdd(view);
178         }
179         view.setFocusable(false);
180 
181         mViewGroup.addView(view);
182 
183         if (modifier != null) {
184             modifier.modifyViewAfterAdd(view);
185         }
186 
187         Log.v(TAG, "Rendering layout " + layoutName
188                 + " (" + mLayoutIndex + "/" + LAYOUTS.length + ")");
189 
190         final Runnable generateBitmapRunnable = () ->
191             new BitmapTask(view, layoutName).execute();
192 
193         if (view instanceof DatePicker && mTheme.spec == ThemeInfo.HOLO) {
194             // The Holo-styled DatePicker uses a CalendarView that has a
195             // non-configurable adjustment duration of 540ms.
196             view.postDelayed(generateBitmapRunnable, HOLO_CALENDAR_VIEW_ADJUSTMENT_DURATION);
197         } else {
198             view.post(generateBitmapRunnable);
199         }
200     }
201 
finish(String reason, boolean success)202     private void finish(String reason, boolean success) {
203         final Intent data = new Intent();
204         data.putExtra(GenerateImagesActivity.EXTRA_REASON, reason);
205         setResult(success ? RESULT_OK : RESULT_CANCELED, data);
206 
207         if (!isFinishing()) {
208             finish();
209         }
210     }
211 
212     private class BitmapTask extends GenerateBitmapTask {
213 
BitmapTask(View view, String name)214         public BitmapTask(View view, String name) {
215             super(view, mOutputDir, name);
216         }
217 
218         @Override
onPostExecute(Boolean success)219         protected void onPostExecute(Boolean success) {
220             if (success && mIsRunning) {
221                 setNextLayout();
222             } else {
223                 finish("Failed to render view to bitmap: " + mName + " (activity running? "
224                         + mIsRunning + ")", false);
225             }
226         }
227     }
228 }
229