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