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; 130 if (Build.VERSION.SDK_INT >= 26) { 131 // TYPE_SYSTEM_ALERT is deprecated in android O. 132 params = new WindowManager.LayoutParams( 133 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); 134 } else { 135 params = new WindowManager.LayoutParams( 136 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); 137 } 138 params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 139 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS 140 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 141 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 142 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 143 params.alpha = WINDOW_ALPHA; 144 params.gravity = Gravity.LEFT | Gravity.BOTTOM; 145 params.setTitle(mName); 146 147 int width = (int)(display.getWidth() * INITIAL_SCALE); 148 int height = (int)(display.getHeight() * INITIAL_SCALE); 149 if (mWidth > mHeight) { 150 height = mHeight * width / mWidth; 151 } else { 152 width = mWidth * height / mHeight; 153 } 154 params.width = width; 155 params.height = height; 156 157 mWindowManager.addView(mSurfaceView, params); 158 mWindowVisible = true; 159 160 SurfaceHolder holder = mSurfaceView.getHolder(); 161 holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 162 mListener.onWindowCreated(holder); 163 } 164 } 165 166 @Override 167 public void dismiss() { 168 if (mWindowVisible) { 169 mListener.onWindowDestroyed(); 170 171 mWindowManager.removeView(mSurfaceView); 172 mWindowVisible = false; 173 } 174 } 175 176 @Override 177 public void updateAspectRatio(int width, int height) { 178 } 179 180 @Override 181 public Bitmap getSnapshot() { 182 return null; 183 } 184 } 185 186 /** 187 * Implementation for API version 17+. 188 */ 189 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 190 private static final class JellybeanMr1Impl extends OverlayDisplayWindow { 191 // When true, disables support for moving and resizing the overlay. 192 // The window is made non-touchable, which makes it possible to 193 // directly interact with the content underneath. 194 private static final boolean DISABLE_MOVE_AND_RESIZE = false; 195 196 private final DisplayManager mDisplayManager; 197 private final WindowManager mWindowManager; 198 199 private final Display mDefaultDisplay; 200 private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics(); 201 202 private View mWindowContent; 203 private WindowManager.LayoutParams mWindowParams; 204 private TextureView mTextureView; 205 private TextView mNameTextView; 206 207 private GestureDetector mGestureDetector; 208 private ScaleGestureDetector mScaleGestureDetector; 209 210 private boolean mWindowVisible; 211 private int mWindowX; 212 private int mWindowY; 213 private float mWindowScale; 214 215 private float mLiveTranslationX; 216 private float mLiveTranslationY; 217 private float mLiveScale = 1.0f; 218 219 public JellybeanMr1Impl(Context context, String name, 220 int width, int height, int gravity) { 221 super(context, name, width, height, gravity); 222 223 mDisplayManager = (DisplayManager)context.getSystemService( 224 Context.DISPLAY_SERVICE); 225 mWindowManager = (WindowManager)context.getSystemService( 226 Context.WINDOW_SERVICE); 227 228 mDefaultDisplay = mWindowManager.getDefaultDisplay(); 229 updateDefaultDisplayInfo(); 230 231 createWindow(); 232 } 233 234 @Override 235 public void show() { 236 if (!mWindowVisible) { 237 mDisplayManager.registerDisplayListener(mDisplayListener, null); 238 if (!updateDefaultDisplayInfo()) { 239 mDisplayManager.unregisterDisplayListener(mDisplayListener); 240 return; 241 } 242 243 clearLiveState(); 244 updateWindowParams(); 245 mWindowManager.addView(mWindowContent, mWindowParams); 246 mWindowVisible = true; 247 } 248 } 249 250 @Override 251 public void dismiss() { 252 if (mWindowVisible) { 253 mDisplayManager.unregisterDisplayListener(mDisplayListener); 254 mWindowManager.removeView(mWindowContent); 255 mWindowVisible = false; 256 } 257 } 258 259 @Override 260 public void updateAspectRatio(int width, int height) { 261 if (mWidth * height < mHeight * width) { 262 mTextureView.getLayoutParams().width = mWidth; 263 mTextureView.getLayoutParams().height = mWidth * height / width; 264 } else { 265 mTextureView.getLayoutParams().width = mHeight * width / height; 266 mTextureView.getLayoutParams().height = mHeight; 267 } 268 relayout(); 269 } 270 271 @Override 272 public Bitmap getSnapshot() { 273 return mTextureView.getBitmap(); 274 } 275 276 private void relayout() { 277 if (mWindowVisible) { 278 updateWindowParams(); 279 mWindowManager.updateViewLayout(mWindowContent, mWindowParams); 280 } 281 } 282 283 private boolean updateDefaultDisplayInfo() { 284 mDefaultDisplay.getMetrics(mDefaultDisplayMetrics); 285 return true; 286 } 287 288 private void createWindow() { 289 LayoutInflater inflater = LayoutInflater.from(mContext); 290 291 mWindowContent = inflater.inflate( 292 R.layout.overlay_display_window, null); 293 mWindowContent.setOnTouchListener(mOnTouchListener); 294 295 mTextureView = (TextureView)mWindowContent.findViewById( 296 R.id.overlay_display_window_texture); 297 mTextureView.setPivotX(0); 298 mTextureView.setPivotY(0); 299 mTextureView.getLayoutParams().width = mWidth; 300 mTextureView.getLayoutParams().height = mHeight; 301 mTextureView.setOpaque(false); 302 mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); 303 304 mNameTextView = (TextView)mWindowContent.findViewById( 305 R.id.overlay_display_window_title); 306 mNameTextView.setText(mName); 307 308 if (Build.VERSION.SDK_INT >= 26) { 309 // TYPE_SYSTEM_ALERT is deprecated in android O. 310 mWindowParams = new WindowManager.LayoutParams( 311 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); 312 } else { 313 mWindowParams = new WindowManager.LayoutParams( 314 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); 315 } 316 mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 317 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS 318 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 319 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 320 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; 321 if (DISABLE_MOVE_AND_RESIZE) { 322 mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 323 } 324 mWindowParams.alpha = WINDOW_ALPHA; 325 mWindowParams.gravity = Gravity.TOP | Gravity.LEFT; 326 mWindowParams.setTitle(mName); 327 328 mGestureDetector = new GestureDetector(mContext, mOnGestureListener); 329 mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener); 330 331 // Set the initial position and scale. 332 // The position and scale will be clamped when the display is first shown. 333 mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ? 334 0 : mDefaultDisplayMetrics.widthPixels; 335 mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ? 336 0 : mDefaultDisplayMetrics.heightPixels; 337 Log.d(TAG, mDefaultDisplayMetrics.toString()); 338 mWindowScale = INITIAL_SCALE; 339 340 // calculate and save initial settings 341 updateWindowParams(); 342 saveWindowParams(); 343 } 344 345 private void updateWindowParams() { 346 float scale = mWindowScale * mLiveScale; 347 scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth); 348 scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight); 349 scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); 350 351 float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f; 352 int width = (int)(mWidth * scale); 353 int height = (int)(mHeight * scale); 354 int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale); 355 int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale); 356 x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width)); 357 y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height)); 358 359 if (DEBUG) { 360 Log.d(TAG, "updateWindowParams: scale=" + scale 361 + ", offsetScale=" + offsetScale 362 + ", x=" + x + ", y=" + y 363 + ", width=" + width + ", height=" + height); 364 } 365 366 mTextureView.setScaleX(scale); 367 mTextureView.setScaleY(scale); 368 369 mTextureView.setTranslationX( 370 (mWidth - mTextureView.getLayoutParams().width) * scale / 2); 371 mTextureView.setTranslationY( 372 (mHeight - mTextureView.getLayoutParams().height) * scale / 2); 373 374 mWindowParams.x = x; 375 mWindowParams.y = y; 376 mWindowParams.width = width; 377 mWindowParams.height = height; 378 } 379 380 private void saveWindowParams() { 381 mWindowX = mWindowParams.x; 382 mWindowY = mWindowParams.y; 383 mWindowScale = mTextureView.getScaleX(); 384 clearLiveState(); 385 } 386 387 private void clearLiveState() { 388 mLiveTranslationX = 0f; 389 mLiveTranslationY = 0f; 390 mLiveScale = 1.0f; 391 } 392 393 private final DisplayManager.DisplayListener mDisplayListener = 394 new DisplayManager.DisplayListener() { 395 @Override 396 public void onDisplayAdded(int displayId) { 397 } 398 399 @Override 400 public void onDisplayChanged(int displayId) { 401 if (displayId == mDefaultDisplay.getDisplayId()) { 402 if (updateDefaultDisplayInfo()) { 403 relayout(); 404 } else { 405 dismiss(); 406 } 407 } 408 } 409 410 @Override 411 public void onDisplayRemoved(int displayId) { 412 if (displayId == mDefaultDisplay.getDisplayId()) { 413 dismiss(); 414 } 415 } 416 }; 417 418 private final SurfaceTextureListener mSurfaceTextureListener = 419 new SurfaceTextureListener() { 420 @Override 421 public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, 422 int width, int height) { 423 if (mListener != null) { 424 mListener.onWindowCreated(new Surface(surfaceTexture)); 425 } 426 } 427 428 @Override 429 public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 430 if (mListener != null) { 431 mListener.onWindowDestroyed(); 432 } 433 return true; 434 } 435 436 @Override 437 public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, 438 int width, int height) { 439 } 440 441 @Override 442 public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 443 } 444 }; 445 446 private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { 447 @Override 448 public boolean onTouch(View view, MotionEvent event) { 449 // Work in screen coordinates. 450 final float oldX = event.getX(); 451 final float oldY = event.getY(); 452 event.setLocation(event.getRawX(), event.getRawY()); 453 454 mGestureDetector.onTouchEvent(event); 455 mScaleGestureDetector.onTouchEvent(event); 456 457 switch (event.getActionMasked()) { 458 case MotionEvent.ACTION_UP: 459 case MotionEvent.ACTION_CANCEL: 460 saveWindowParams(); 461 break; 462 } 463 464 // Revert to window coordinates. 465 event.setLocation(oldX, oldY); 466 return true; 467 } 468 }; 469 470 private final GestureDetector.OnGestureListener mOnGestureListener = 471 new GestureDetector.SimpleOnGestureListener() { 472 @Override 473 public boolean onScroll(MotionEvent e1, MotionEvent e2, 474 float distanceX, float distanceY) { 475 mLiveTranslationX -= distanceX; 476 mLiveTranslationY -= distanceY; 477 relayout(); 478 return true; 479 } 480 }; 481 482 private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = 483 new ScaleGestureDetector.SimpleOnScaleGestureListener() { 484 @Override 485 public boolean onScale(ScaleGestureDetector detector) { 486 mLiveScale *= detector.getScaleFactor(); 487 relayout(); 488 return true; 489 } 490 }; 491 } 492} 493