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