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