1 /* 2 * Copyright (C) 2012 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 com.example.android.supportv7.media; 18 import com.example.android.supportv7.R; 19 20 import android.content.Context; 21 import android.graphics.Bitmap; 22 import android.graphics.Point; 23 import android.graphics.SurfaceTexture; 24 import android.hardware.display.DisplayManager; 25 import android.os.Build; 26 import android.util.Log; 27 import android.view.Display; 28 import android.util.DisplayMetrics; 29 import android.view.GestureDetector; 30 import android.view.Gravity; 31 import android.view.LayoutInflater; 32 import android.view.MotionEvent; 33 import android.view.ScaleGestureDetector; 34 import android.view.SurfaceHolder; 35 import android.view.SurfaceView; 36 import android.view.TextureView; 37 import android.view.View; 38 import android.view.Surface; 39 import android.view.WindowManager; 40 import android.view.TextureView.SurfaceTextureListener; 41 import android.widget.TextView; 42 43 /** 44 * Manages an overlay display window, used for simulating remote playback. 45 */ 46 public abstract class OverlayDisplayWindow { 47 private static final String TAG = "OverlayDisplayWindow"; 48 private static final boolean DEBUG = false; 49 50 private static final float WINDOW_ALPHA = 0.8f; 51 private static final float INITIAL_SCALE = 0.5f; 52 private static final float MIN_SCALE = 0.3f; 53 private static final float MAX_SCALE = 1.0f; 54 55 protected final Context mContext; 56 protected final String mName; 57 protected final int mWidth; 58 protected final int mHeight; 59 protected final int mGravity; 60 protected OverlayWindowListener mListener; 61 62 protected OverlayDisplayWindow(Context context, String name, 63 int width, int height, int gravity) { 64 mContext = context; 65 mName = name; 66 mWidth = width; 67 mHeight = height; 68 mGravity = gravity; 69 } 70 71 public static OverlayDisplayWindow create(Context context, String name, 72 int width, int height, int gravity) { 73 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 74 return new JellybeanMr1Impl(context, name, width, height, gravity); 75 } else { 76 return new LegacyImpl(context, name, width, height, gravity); 77 } 78 } 79 80 public void setOverlayWindowListener(OverlayWindowListener listener) { 81 mListener = listener; 82 } 83 84 public Context getContext() { 85 return mContext; 86 } 87 88 public abstract void show(); 89 90 public abstract void dismiss(); 91 92 public abstract void updateAspectRatio(int width, int height); 93 94 public abstract Bitmap getSnapshot(); 95 96 // Watches for significant changes in the overlay display window lifecycle. 97 public interface OverlayWindowListener { 98 public void onWindowCreated(Surface surface); 99 public void onWindowCreated(SurfaceHolder surfaceHolder); 100 public void onWindowDestroyed(); 101 } 102 103 /** 104 * Implementation for older versions. 105 */ 106 private static final class LegacyImpl extends OverlayDisplayWindow { 107 private final WindowManager mWindowManager; 108 109 private boolean mWindowVisible; 110 private SurfaceView mSurfaceView; 111 112 public LegacyImpl(Context context, String name, 113 int width, int height, int gravity) { 114 super(context, name, width, height, gravity); 115 116 mWindowManager = (WindowManager)context.getSystemService( 117 Context.WINDOW_SERVICE); 118 } 119 120 @Override 121 public void show() { 122 if (!mWindowVisible) { 123 mSurfaceView = new SurfaceView(mContext); 124 125 Display display = mWindowManager.getDefaultDisplay(); 126 127 WindowManager.LayoutParams params = new WindowManager.LayoutParams( 128 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); 129 params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 130 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS 131 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 132 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 133 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 134 params.alpha = WINDOW_ALPHA; 135 params.gravity = Gravity.LEFT | Gravity.BOTTOM; 136 params.setTitle(mName); 137 138 int width = (int)(display.getWidth() * INITIAL_SCALE); 139 int height = (int)(display.getHeight() * INITIAL_SCALE); 140 if (mWidth > mHeight) { 141 height = mHeight * width / mWidth; 142 } else { 143 width = mWidth * height / mHeight; 144 } 145 params.width = width; 146 params.height = height; 147 148 mWindowManager.addView(mSurfaceView, params); 149 mWindowVisible = true; 150 151 SurfaceHolder holder = mSurfaceView.getHolder(); 152 holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 153 mListener.onWindowCreated(holder); 154 } 155 } 156 157 @Override 158 public void dismiss() { 159 if (mWindowVisible) { 160 mListener.onWindowDestroyed(); 161 162 mWindowManager.removeView(mSurfaceView); 163 mWindowVisible = false; 164 } 165 } 166 167 @Override 168 public void updateAspectRatio(int width, int height) { 169 } 170 171 @Override 172 public Bitmap getSnapshot() { 173 return null; 174 } 175 } 176 177 /** 178 * Implementation for API version 17+. 179 */ 180 private static final class JellybeanMr1Impl extends OverlayDisplayWindow { 181 // When true, disables support for moving and resizing the overlay. 182 // The window is made non-touchable, which makes it possible to 183 // directly interact with the content underneath. 184 private static final boolean DISABLE_MOVE_AND_RESIZE = false; 185 186 private final DisplayManager mDisplayManager; 187 private final WindowManager mWindowManager; 188 189 private final Display mDefaultDisplay; 190 private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics(); 191 192 private View mWindowContent; 193 private WindowManager.LayoutParams mWindowParams; 194 private TextureView mTextureView; 195 private TextView mNameTextView; 196 197 private GestureDetector mGestureDetector; 198 private ScaleGestureDetector mScaleGestureDetector; 199 200 private boolean mWindowVisible; 201 private int mWindowX; 202 private int mWindowY; 203 private float mWindowScale; 204 205 private float mLiveTranslationX; 206 private float mLiveTranslationY; 207 private float mLiveScale = 1.0f; 208 209 public JellybeanMr1Impl(Context context, String name, 210 int width, int height, int gravity) { 211 super(context, name, width, height, gravity); 212 213 mDisplayManager = (DisplayManager)context.getSystemService( 214 Context.DISPLAY_SERVICE); 215 mWindowManager = (WindowManager)context.getSystemService( 216 Context.WINDOW_SERVICE); 217 218 mDefaultDisplay = mWindowManager.getDefaultDisplay(); 219 updateDefaultDisplayInfo(); 220 221 createWindow(); 222 } 223 224 @Override 225 public void show() { 226 if (!mWindowVisible) { 227 mDisplayManager.registerDisplayListener(mDisplayListener, null); 228 if (!updateDefaultDisplayInfo()) { 229 mDisplayManager.unregisterDisplayListener(mDisplayListener); 230 return; 231 } 232 233 clearLiveState(); 234 updateWindowParams(); 235 mWindowManager.addView(mWindowContent, mWindowParams); 236 mWindowVisible = true; 237 } 238 } 239 240 @Override 241 public void dismiss() { 242 if (mWindowVisible) { 243 mDisplayManager.unregisterDisplayListener(mDisplayListener); 244 mWindowManager.removeView(mWindowContent); 245 mWindowVisible = false; 246 } 247 } 248 249 @Override 250 public void updateAspectRatio(int width, int height) { 251 if (mWidth * height < mHeight * width) { 252 mTextureView.getLayoutParams().width = mWidth; 253 mTextureView.getLayoutParams().height = mWidth * height / width; 254 } else { 255 mTextureView.getLayoutParams().width = mHeight * width / height; 256 mTextureView.getLayoutParams().height = mHeight; 257 } 258 relayout(); 259 } 260 261 @Override 262 public Bitmap getSnapshot() { 263 return mTextureView.getBitmap(); 264 } 265 266 private void relayout() { 267 if (mWindowVisible) { 268 updateWindowParams(); 269 mWindowManager.updateViewLayout(mWindowContent, mWindowParams); 270 } 271 } 272 273 private boolean updateDefaultDisplayInfo() { 274 mDefaultDisplay.getMetrics(mDefaultDisplayMetrics); 275 return true; 276 } 277 278 private void createWindow() { 279 LayoutInflater inflater = LayoutInflater.from(mContext); 280 281 mWindowContent = inflater.inflate( 282 R.layout.overlay_display_window, null); 283 mWindowContent.setOnTouchListener(mOnTouchListener); 284 285 mTextureView = (TextureView)mWindowContent.findViewById( 286 R.id.overlay_display_window_texture); 287 mTextureView.setPivotX(0); 288 mTextureView.setPivotY(0); 289 mTextureView.getLayoutParams().width = mWidth; 290 mTextureView.getLayoutParams().height = mHeight; 291 mTextureView.setOpaque(false); 292 mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); 293 294 mNameTextView = (TextView)mWindowContent.findViewById( 295 R.id.overlay_display_window_title); 296 mNameTextView.setText(mName); 297 298 mWindowParams = new WindowManager.LayoutParams( 299 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); 300 mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 301 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS 302 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 303 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 304 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; 305 if (DISABLE_MOVE_AND_RESIZE) { 306 mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 307 } 308 mWindowParams.alpha = WINDOW_ALPHA; 309 mWindowParams.gravity = Gravity.TOP | Gravity.LEFT; 310 mWindowParams.setTitle(mName); 311 312 mGestureDetector = new GestureDetector(mContext, mOnGestureListener); 313 mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener); 314 315 // Set the initial position and scale. 316 // The position and scale will be clamped when the display is first shown. 317 mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ? 318 0 : mDefaultDisplayMetrics.widthPixels; 319 mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ? 320 0 : mDefaultDisplayMetrics.heightPixels; 321 Log.d(TAG, mDefaultDisplayMetrics.toString()); 322 mWindowScale = INITIAL_SCALE; 323 324 // calculate and save initial settings 325 updateWindowParams(); 326 saveWindowParams(); 327 } 328 329 private void updateWindowParams() { 330 float scale = mWindowScale * mLiveScale; 331 scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth); 332 scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight); 333 scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); 334 335 float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f; 336 int width = (int)(mWidth * scale); 337 int height = (int)(mHeight * scale); 338 int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale); 339 int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale); 340 x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width)); 341 y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height)); 342 343 if (DEBUG) { 344 Log.d(TAG, "updateWindowParams: scale=" + scale 345 + ", offsetScale=" + offsetScale 346 + ", x=" + x + ", y=" + y 347 + ", width=" + width + ", height=" + height); 348 } 349 350 mTextureView.setScaleX(scale); 351 mTextureView.setScaleY(scale); 352 353 mTextureView.setTranslationX( 354 (mWidth - mTextureView.getLayoutParams().width) * scale / 2); 355 mTextureView.setTranslationY( 356 (mHeight - mTextureView.getLayoutParams().height) * scale / 2); 357 358 mWindowParams.x = x; 359 mWindowParams.y = y; 360 mWindowParams.width = width; 361 mWindowParams.height = height; 362 } 363 364 private void saveWindowParams() { 365 mWindowX = mWindowParams.x; 366 mWindowY = mWindowParams.y; 367 mWindowScale = mTextureView.getScaleX(); 368 clearLiveState(); 369 } 370 371 private void clearLiveState() { 372 mLiveTranslationX = 0f; 373 mLiveTranslationY = 0f; 374 mLiveScale = 1.0f; 375 } 376 377 private final DisplayManager.DisplayListener mDisplayListener = 378 new DisplayManager.DisplayListener() { 379 @Override 380 public void onDisplayAdded(int displayId) { 381 } 382 383 @Override 384 public void onDisplayChanged(int displayId) { 385 if (displayId == mDefaultDisplay.getDisplayId()) { 386 if (updateDefaultDisplayInfo()) { 387 relayout(); 388 } else { 389 dismiss(); 390 } 391 } 392 } 393 394 @Override 395 public void onDisplayRemoved(int displayId) { 396 if (displayId == mDefaultDisplay.getDisplayId()) { 397 dismiss(); 398 } 399 } 400 }; 401 402 private final SurfaceTextureListener mSurfaceTextureListener = 403 new SurfaceTextureListener() { 404 @Override 405 public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, 406 int width, int height) { 407 if (mListener != null) { 408 mListener.onWindowCreated(new Surface(surfaceTexture)); 409 } 410 } 411 412 @Override 413 public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 414 if (mListener != null) { 415 mListener.onWindowDestroyed(); 416 } 417 return true; 418 } 419 420 @Override 421 public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, 422 int width, int height) { 423 } 424 425 @Override 426 public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 427 } 428 }; 429 430 private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { 431 @Override 432 public boolean onTouch(View view, MotionEvent event) { 433 // Work in screen coordinates. 434 final float oldX = event.getX(); 435 final float oldY = event.getY(); 436 event.setLocation(event.getRawX(), event.getRawY()); 437 438 mGestureDetector.onTouchEvent(event); 439 mScaleGestureDetector.onTouchEvent(event); 440 441 switch (event.getActionMasked()) { 442 case MotionEvent.ACTION_UP: 443 case MotionEvent.ACTION_CANCEL: 444 saveWindowParams(); 445 break; 446 } 447 448 // Revert to window coordinates. 449 event.setLocation(oldX, oldY); 450 return true; 451 } 452 }; 453 454 private final GestureDetector.OnGestureListener mOnGestureListener = 455 new GestureDetector.SimpleOnGestureListener() { 456 @Override 457 public boolean onScroll(MotionEvent e1, MotionEvent e2, 458 float distanceX, float distanceY) { 459 mLiveTranslationX -= distanceX; 460 mLiveTranslationY -= distanceY; 461 relayout(); 462 return true; 463 } 464 }; 465 466 private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = 467 new ScaleGestureDetector.SimpleOnScaleGestureListener() { 468 @Override 469 public boolean onScale(ScaleGestureDetector detector) { 470 mLiveScale *= detector.getScaleFactor(); 471 relayout(); 472 return true; 473 } 474 }; 475 } 476 } 477