1 // Copyright (c) 2014 Intel Corporation. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.xwalk.core.internal;
7 import java.lang.Runnable;
8 import java.util.ArrayList;
10 import android.app.Activity;
11 import android.app.Dialog;
12 import android.content.BroadcastReceiver;
13 import android.content.Context;
14 import android.content.DialogInterface;
15 import android.content.Intent;
16 import android.content.IntentFilter;
17 import android.content.res.Configuration;
18 import android.graphics.Bitmap;
19 import android.graphics.BitmapFactory;
20 import android.graphics.drawable.BitmapDrawable;
21 import android.graphics.drawable.Drawable;
22 import android.graphics.Point;
23 import android.graphics.Rect;
24 import android.graphics.Shader.TileMode;
25 import android.hardware.SensorManager;
26 import android.os.Bundle;
27 import android.util.DisplayMetrics;
28 import android.util.Log;
29 import android.util.TypedValue;
30 import android.view.Display;
31 import android.view.Gravity;
32 import android.view.KeyEvent;
33 import android.view.OrientationEventListener;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.Window;
37 import android.view.WindowManager;
38 import android.widget.FrameLayout;
39 import android.widget.ImageView;
40 import android.widget.ImageView.ScaleType;
41 import android.widget.LinearLayout;
42 import android.widget.RelativeLayout;
44 import org.chromium.content.browser.ContentViewRenderView.FirstRenderedFrameListener;
47 * Provisionally set it as public due to the use of launch screen extension.
50 public class XWalkLaunchScreenManager
51 implements FirstRenderedFrameListener, DialogInterface.OnShowListener,
52 DialogInterface.OnDismissListener, PageLoadListener {
53 // This string will be initialized before extension initialized,
54 // and used by LaunchScreenExtension.
55 private static String mIntentFilterStr;
57 private final static String BORDER_MODE_REPEAT = "repeat";
58 private final static String BORDER_MODE_STRETCH = "stretch";
59 private final static String BORDER_MODE_ROUND = "round";
61 private XWalkViewInternal mXWalkView;
62 private Activity mActivity;
63 private Context mLibContext;
64 private Dialog mLaunchScreenDialog;
65 private boolean mPageLoadFinished;
66 private ReadyWhenType mReadyWhen;
67 private boolean mFirstFrameReceived;
68 private BroadcastReceiver mLaunchScreenReadyWhenReceiver;
69 private boolean mCustomHideLaunchScreen;
70 private int mCurrentOrientation;
71 private OrientationEventListener mOrientationListener;
73 private enum ReadyWhenType {
80 private enum BorderModeType {
87 public XWalkLaunchScreenManager(Context context, XWalkViewInternal xwView) {
89 mLibContext = context;
90 mActivity = mXWalkView.getActivity();
91 mIntentFilterStr = mActivity.getPackageName() + ".hideLaunchScreen";
94 public void displayLaunchScreen(String readyWhen, final String imageBorderList) {
95 if (mXWalkView == null) return;
96 setReadyWhen(readyWhen);
98 Runnable runnable = new Runnable() {
100 int bgResId = mActivity.getResources().getIdentifier(
101 "launchscreen_bg", "drawable", mActivity.getPackageName());
102 if (bgResId == 0) return;
103 Drawable bgDrawable = mActivity.getResources().getDrawable(bgResId);
104 if (bgDrawable == null) return;
106 mLaunchScreenDialog = new Dialog(mLibContext,
107 android.R.style.Theme_Holo_Light_NoActionBar);
108 if ((mActivity.getWindow().getAttributes().flags &
109 WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0) {
110 mLaunchScreenDialog.getWindow().setFlags(
111 WindowManager.LayoutParams.FLAG_FULLSCREEN,
112 WindowManager.LayoutParams.FLAG_FULLSCREEN);
114 mLaunchScreenDialog.setOnKeyListener(new Dialog.OnKeyListener() {
116 public boolean onKey(DialogInterface arg0, int keyCode,
118 if (keyCode == KeyEvent.KEYCODE_BACK) {
119 performHideLaunchScreen();
120 mActivity.onBackPressed();
125 mLaunchScreenDialog.setOnShowListener(XWalkLaunchScreenManager.this);
126 mLaunchScreenDialog.setOnDismissListener(XWalkLaunchScreenManager.this);
128 mLaunchScreenDialog.getWindow().setBackgroundDrawable(bgDrawable);
129 // Set foreground image
130 RelativeLayout root = getLaunchScreenLayout(imageBorderList);
131 // The root layout can be null when there is no 'image' provided in the manifest.
132 // We can just display the background instead of no launch screen dialog displayed.
133 if (root != null) mLaunchScreenDialog.setContentView(root);
134 mLaunchScreenDialog.show();
136 // Change the layout depends on the orientation change.
137 mOrientationListener = new OrientationEventListener(mActivity,
138 SensorManager.SENSOR_DELAY_NORMAL) {
139 public void onOrientationChanged(int ori) {
140 if (mLaunchScreenDialog == null || !mLaunchScreenDialog.isShowing()) {
143 int orientation = getScreenOrientation();
144 if (orientation != mCurrentOrientation) {
145 RelativeLayout root = getLaunchScreenLayout(imageBorderList);
146 if (root == null) return;
147 mLaunchScreenDialog.setContentView(root);
151 mOrientationListener.enable();
152 if (mReadyWhen == ReadyWhenType.CUSTOM) registerBroadcastReceiver();
155 mActivity.runOnUiThread(runnable);
159 public void onFirstFrameReceived() {
160 mFirstFrameReceived = true;
161 hideLaunchScreenWhenReady();
165 public void onShow(DialogInterface dialog) {
166 mActivity.getWindow().setBackgroundDrawable(null);
167 if (mFirstFrameReceived) hideLaunchScreenWhenReady();
171 public void onDismiss(DialogInterface dialog) {
172 mOrientationListener.disable();
173 mOrientationListener = null;
177 public void onPageFinished(String url) {
178 mPageLoadFinished = true;
179 hideLaunchScreenWhenReady();
182 public static String getHideLaunchScreenFilterStr() {
183 return mIntentFilterStr;
186 public int getScreenOrientation() {
187 // getResources().getConfiguration().orientation returns wrong value in some devices.
188 // Below is another way to calculate screen orientation.
189 Display display = mActivity.getWindowManager().getDefaultDisplay();
190 Point size = new Point();
191 display.getSize(size);
193 if (size.x < size.y) {
194 orientation = Configuration.ORIENTATION_PORTRAIT;
196 orientation = Configuration.ORIENTATION_LANDSCAPE;
201 private RelativeLayout getLaunchScreenLayout(String imageBorderList) {
202 // Parse the borders depends on orientation.
203 // imageBorderList format:"[default];[landscape];[portrait]"
204 String[] borders = imageBorderList.split(";");
205 // When there is no borders defined, display with no borders.
206 if (borders.length < 1) return parseImageBorder("");
207 int orientation = getScreenOrientation();
208 mCurrentOrientation = orientation;
209 if (borders.length >= 2 && orientation == Configuration.ORIENTATION_LANDSCAPE) {
210 if (borders[1].equals("empty")) {
211 // Has launch_screen.landscape configured, but no image_border set.
212 // Display the iamge with no borders.
213 return parseImageBorder("");
214 } else if (borders[1].isEmpty()) {
215 // No launch_screen.landscape configured.
216 // Use launch_screen.default.
217 return parseImageBorder(borders[0]);
219 return parseImageBorder(borders[1]);
221 } else if (borders.length == 3 && orientation == Configuration.ORIENTATION_PORTRAIT) {
222 if (borders[2].equals("empty")) {
223 // Has launch_screen.portrait configured, but no image_border set.
224 // Display the iamge with no borders.
225 return parseImageBorder("");
226 } else if (borders[2].isEmpty()) {
227 // No launch_screen.portrait configured.
228 // Use launch_screen.default.
229 return parseImageBorder(borders[0]);
231 return parseImageBorder(borders[2]);
235 return parseImageBorder(borders[0]);
238 private int getSuitableSize(int maxSize, int divider) {
239 int finalSize = divider;
240 float minMod = divider;
241 for (; divider > 1; divider--) {
242 int mod = maxSize % divider;
243 // Found the suitable size.
248 // Record the best suitable one.
249 // If there is no mod==0 found, return the divider which min(mod).
259 * Get each section from 9-piece format image depends on the spec defined
260 * @param img The foreground image.
261 * @param x The position where the section start.
262 * @param y The position where the section start.
263 * @param width The width of the section.
264 * @param height The height of the section.
265 * @param mode The border type for this section.
266 * @param maxWidth When mode == ROUND, this will be used.
267 * @param maxHeight When mode == ROUND, this will be used.
268 * @return The ImageView for this section.
270 private ImageView getSubImageView(Bitmap img, int x, int y, int width, int height,
271 BorderModeType mode, int maxWidth, int maxHeight) {
272 if (img == null) return null;
274 if (width <= 0 || height <= 0) return null;
276 // Check whether the section is inside the foreground image.
277 Rect imgRect = new Rect(0, 0, img.getWidth(), img.getHeight());
278 Rect subRect = new Rect(x, y, x + width, y + height);
279 if (!imgRect.contains(subRect)) return null;
281 Bitmap subImage = Bitmap.createBitmap(img, x, y, width, height);
282 ImageView subImageView = new ImageView(mActivity);
283 BitmapDrawable drawable;
284 if (mode == BorderModeType.ROUND) {
285 int originW = subImage.getWidth();
286 int originH = subImage.getHeight();
289 // Scale down the sub image to let the last image not cropped when it's repeated.
290 if (maxWidth > 0) newW = getSuitableSize(maxWidth, originW);
291 if (maxHeight > 0) newH = getSuitableSize(maxHeight, originH);
292 // recreate the new scaled bitmap.
293 Bitmap resizedBitmap = Bitmap.createScaledBitmap(subImage, newW, newH, true);
294 // Treat as repeat mode.
295 subImage = resizedBitmap;
296 mode = BorderModeType.REPEAT;
298 if (mode == BorderModeType.REPEAT) {
299 drawable = new BitmapDrawable(mActivity.getResources(), subImage);
300 drawable.setTileModeXY(TileMode.REPEAT, TileMode.REPEAT);
301 subImageView.setImageDrawable(drawable);
302 subImageView.setScaleType(ScaleType.FIT_XY);
303 } else if (mode == BorderModeType.STRETCH) {
304 subImageView.setImageBitmap(subImage);
305 subImageView.setScaleType(ScaleType.FIT_XY);
307 subImageView.setImageBitmap(subImage);
313 private int getStatusBarHeight() {
314 int resourceId = mActivity.getResources().getIdentifier(
315 "status_bar_height", "dimen", "android");
316 if (resourceId > 0) {
317 return mActivity.getResources().getDimensionPixelSize(resourceId);
319 // If not found, return default one.
323 private RelativeLayout parseImageBorder(String imageBorder) {
327 int bottomBorder = 0;
328 BorderModeType horizontalMode = BorderModeType.STRETCH;
329 BorderModeType verticalMode = BorderModeType.STRETCH;
331 if (imageBorder.equals("empty")) imageBorder = "";
333 // Parse the value of image_border.
334 String[] items = imageBorder.split(" ");
335 ArrayList<String> borders = new ArrayList<String>();
336 ArrayList<BorderModeType> modes = new ArrayList<BorderModeType>();
337 for (int i = 0; i < items.length; i++) {
338 String item = items[i];
339 if (item.endsWith("px")) {
340 borders.add(item.replaceAll("px", ""));
341 } else if (item.equals(BORDER_MODE_REPEAT)) {
342 modes.add(BorderModeType.REPEAT);
343 } else if (item.equals(BORDER_MODE_STRETCH)) {
344 modes.add(BorderModeType.STRETCH);
345 } else if (item.equals(BORDER_MODE_ROUND)) {
346 modes.add(BorderModeType.ROUND);
349 // Parse borders as defined by the spec.
351 if (borders.size() == 1) {
352 topBorder = rightBorder = leftBorder = bottomBorder =
353 Integer.valueOf(borders.get(0));
354 } else if (borders.size() == 2) {
355 topBorder = bottomBorder = Integer.valueOf(borders.get(0));
356 rightBorder = leftBorder = Integer.valueOf(borders.get(1));
357 } else if (borders.size() == 3) {
358 rightBorder = leftBorder = Integer.valueOf(borders.get(1));
359 topBorder = Integer.valueOf(borders.get(0));
360 bottomBorder = Integer.valueOf(borders.get(2));
361 } else if (borders.size() == 4) {
362 topBorder = Integer.valueOf(borders.get(0));
363 rightBorder = Integer.valueOf(borders.get(1));
364 leftBorder = Integer.valueOf(borders.get(2));
365 bottomBorder = Integer.valueOf(borders.get(3));
367 } catch (NumberFormatException e) {
368 topBorder = rightBorder = leftBorder = bottomBorder = 0;
371 // The border values are dpi from manifest.json, need to translate to px.
372 DisplayMetrics matrix = mActivity.getResources().getDisplayMetrics();
373 topBorder = (int)TypedValue.applyDimension(
374 TypedValue.COMPLEX_UNIT_DIP, topBorder, matrix);
375 rightBorder = (int)TypedValue.applyDimension(
376 TypedValue.COMPLEX_UNIT_DIP, rightBorder, matrix);
377 leftBorder = (int)TypedValue.applyDimension(
378 TypedValue.COMPLEX_UNIT_DIP, leftBorder, matrix);
379 bottomBorder = (int)TypedValue.applyDimension(
380 TypedValue.COMPLEX_UNIT_DIP, bottomBorder, matrix);
382 // Parse border mode as spec defined.
383 if (modes.size() == 1) {
384 horizontalMode = verticalMode = modes.get(0);
385 } else if (modes.size() == 2) {
386 horizontalMode = modes.get(0);
387 verticalMode = modes.get(1);
390 // Get foreground image
391 int imgResId = mActivity.getResources().getIdentifier(
392 "launchscreen_img", "drawable", mActivity.getPackageName());
393 if (imgResId == 0) return null;
394 Bitmap img = BitmapFactory.decodeResource(mActivity.getResources(), imgResId);
395 if (img == null) return null;
397 RelativeLayout root = new RelativeLayout(mActivity);
398 root.setLayoutParams(new RelativeLayout.LayoutParams(
399 ViewGroup.LayoutParams.MATCH_PARENT,
400 ViewGroup.LayoutParams.MATCH_PARENT));
401 RelativeLayout.LayoutParams params;
402 ImageView subImageView;
404 // If no border specified, display the foreground image centered horizontally and vertically.
405 if (borders.size() == 0) {
406 subImageView = new ImageView(mActivity);
407 subImageView.setImageBitmap(img);
408 params = new RelativeLayout.LayoutParams(
409 RelativeLayout.LayoutParams.WRAP_CONTENT,
410 RelativeLayout.LayoutParams.WRAP_CONTENT);
411 params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
412 root.addView(subImageView, params);
416 // Create the 9-piece layout as spec defined.
418 // Get Screen width and height.
419 Display display = mActivity.getWindowManager().getDefaultDisplay();
420 Point size = new Point();
421 display.getSize(size);
423 // For non fullscreen, the height should substract status bar height
424 if ((mActivity.getWindow().getAttributes().flags &
425 WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0) {
426 size.y -= getStatusBarHeight();
429 // Image section-1 top left
430 subImageView = getSubImageView(img, 0, 0, leftBorder, topBorder, BorderModeType.NONE, 0, 0);
431 if (subImageView != null) {
432 params = new RelativeLayout.LayoutParams(
433 RelativeLayout.LayoutParams.WRAP_CONTENT,
434 RelativeLayout.LayoutParams.WRAP_CONTENT);
435 params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
436 params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
437 root.addView(subImageView, params);
440 // Image section-2 top
441 subImageView = getSubImageView(img, leftBorder, 0, img.getWidth() - leftBorder
442 - rightBorder, topBorder, horizontalMode, size.x - leftBorder - rightBorder, 0);
443 if (subImageView != null) {
444 params = new RelativeLayout.LayoutParams(
445 RelativeLayout.LayoutParams.MATCH_PARENT,
446 RelativeLayout.LayoutParams.WRAP_CONTENT);
447 params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
448 params.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
449 params.leftMargin = leftBorder;
450 params.rightMargin = rightBorder;
451 root.addView(subImageView, params);
454 // Image section-3 top right
455 subImageView = getSubImageView(img, img.getWidth() - rightBorder, 0,
456 rightBorder, topBorder, BorderModeType.NONE, 0, 0);
457 if (subImageView != null) {
458 params = new RelativeLayout.LayoutParams(
459 RelativeLayout.LayoutParams.WRAP_CONTENT,
460 RelativeLayout.LayoutParams.WRAP_CONTENT);
461 params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
462 params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
463 root.addView(subImageView, params);
466 // Image section-4 left
467 subImageView = getSubImageView(img, 0, topBorder, leftBorder, img.getHeight()
468 - topBorder - bottomBorder, verticalMode, 0, size.y - topBorder - bottomBorder);
469 if (subImageView != null) {
470 params = new RelativeLayout.LayoutParams(
471 RelativeLayout.LayoutParams.WRAP_CONTENT,
472 RelativeLayout.LayoutParams.MATCH_PARENT);
473 params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
474 params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
475 params.topMargin = topBorder;
476 params.bottomMargin = bottomBorder;
477 root.addView(subImageView, params);
480 // Image section-5 middle
481 subImageView = getSubImageView(img, leftBorder, topBorder, img.getWidth() - leftBorder - rightBorder,
482 img.getHeight() - topBorder - bottomBorder, BorderModeType.NONE, 0, 0);
483 if (subImageView != null) {
484 subImageView.setScaleType(ScaleType.FIT_XY);
485 params = new RelativeLayout.LayoutParams(
486 RelativeLayout.LayoutParams.MATCH_PARENT,
487 RelativeLayout.LayoutParams.MATCH_PARENT);
488 params.leftMargin = leftBorder;
489 params.topMargin = topBorder;
490 params.rightMargin = rightBorder;
491 params.bottomMargin = bottomBorder;
492 root.addView(subImageView, params);
495 // Image section-6 right
496 subImageView = getSubImageView(img, img.getWidth() - rightBorder, topBorder, rightBorder,
497 img.getHeight() - topBorder - bottomBorder, verticalMode, 0,
498 size.y - topBorder - bottomBorder);
499 if (subImageView != null) {
500 params = new RelativeLayout.LayoutParams(
501 RelativeLayout.LayoutParams.WRAP_CONTENT,
502 RelativeLayout.LayoutParams.MATCH_PARENT);
503 params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
504 params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
505 params.topMargin = topBorder;
506 params.bottomMargin = bottomBorder;
507 root.addView(subImageView, params);
510 // Image section-7 bottom left
511 subImageView = getSubImageView(img, 0, img.getHeight() - bottomBorder,
512 leftBorder, bottomBorder, BorderModeType.NONE, 0, 0);
513 if (subImageView != null) {
514 params = new RelativeLayout.LayoutParams(
515 RelativeLayout.LayoutParams.WRAP_CONTENT,
516 RelativeLayout.LayoutParams.WRAP_CONTENT);
517 params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
518 params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
519 root.addView(subImageView, params);
522 // Image section-8 bottom
523 subImageView = getSubImageView(img, leftBorder, img.getHeight() - bottomBorder,
524 img.getWidth() - leftBorder - rightBorder, bottomBorder, horizontalMode,
525 size.x - leftBorder - rightBorder, 0);
526 if (subImageView != null) {
527 params = new RelativeLayout.LayoutParams(
528 RelativeLayout.LayoutParams.MATCH_PARENT,
529 RelativeLayout.LayoutParams.WRAP_CONTENT);
530 params.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
531 params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
532 params.leftMargin = leftBorder;
533 params.rightMargin = rightBorder;
534 root.addView(subImageView, params);
537 // Image section-9 bottom right
538 subImageView = getSubImageView(img, img.getWidth() - rightBorder,
539 img.getHeight() - bottomBorder, rightBorder, bottomBorder,
540 BorderModeType.NONE, 0, 0);
541 if (subImageView != null) {
542 params = new RelativeLayout.LayoutParams(
543 RelativeLayout.LayoutParams.WRAP_CONTENT,
544 RelativeLayout.LayoutParams.WRAP_CONTENT);
545 params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
546 params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
547 root.addView(subImageView, params);
552 private void registerBroadcastReceiver() {
553 IntentFilter intentFilter = new IntentFilter(mIntentFilterStr);
554 mLaunchScreenReadyWhenReceiver = new BroadcastReceiver() {
556 public void onReceive(Context context, Intent intent) {
557 mCustomHideLaunchScreen = true;
558 hideLaunchScreenWhenReady();
561 mActivity.registerReceiver(mLaunchScreenReadyWhenReceiver, intentFilter);
564 private void hideLaunchScreenWhenReady() {
565 if (mLaunchScreenDialog == null || !mFirstFrameReceived) return;
566 if (mReadyWhen == ReadyWhenType.FIRST_PAINT) {
567 performHideLaunchScreen();
568 } else if (mReadyWhen == ReadyWhenType.USER_INTERACTIVE) {
569 // TODO: Need to listen js DOMContentLoaded event,
570 // will be implemented in the next step.
571 performHideLaunchScreen();
572 } else if (mReadyWhen == ReadyWhenType.COMPLETE) {
573 if (mPageLoadFinished) performHideLaunchScreen();
574 } else if (mReadyWhen == ReadyWhenType.CUSTOM) {
575 if (mCustomHideLaunchScreen) performHideLaunchScreen();
579 private void performHideLaunchScreen() {
580 mLaunchScreenDialog.dismiss();
581 mLaunchScreenDialog = null;
582 if (mReadyWhen == ReadyWhenType.CUSTOM) {
583 mActivity.unregisterReceiver(mLaunchScreenReadyWhenReceiver);
587 private void setReadyWhen(String readyWhen) {
588 if (readyWhen.equals("first-paint")) {
589 mReadyWhen = ReadyWhenType.FIRST_PAINT;
590 } else if (readyWhen.equals("user-interactive")) {
591 mReadyWhen = ReadyWhenType.USER_INTERACTIVE;
592 } else if (readyWhen.equals("complete")) {
593 mReadyWhen = ReadyWhenType.COMPLETE;
594 } else if (readyWhen.equals("custom")) {
595 mReadyWhen = ReadyWhenType.CUSTOM;
597 mReadyWhen = ReadyWhenType.FIRST_PAINT;