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