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