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