1 /* 2 * Copyright (C) 2023 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 android.car.view; 18 19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 20 21 import android.annotation.MainThread; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.RequiresPermission; 25 import android.annotation.SystemApi; 26 import android.app.Activity; 27 import android.car.Car; 28 import android.car.app.CarActivityManager; 29 import android.car.builtin.util.Slogf; 30 import android.car.builtin.view.TouchableInsetsProvider; 31 import android.content.Context; 32 import android.graphics.Rect; 33 import android.graphics.Region; 34 import android.os.IBinder; 35 import android.util.AttributeSet; 36 import android.util.Dumpable; 37 import android.util.Log; 38 import android.util.Pair; 39 import android.util.Slog; 40 import android.view.Surface; 41 import android.view.SurfaceControl; 42 import android.view.SurfaceHolder; 43 import android.view.SurfaceView; 44 45 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 46 import com.android.internal.annotations.VisibleForTesting; 47 48 import java.io.PrintWriter; 49 50 /** 51 * {@link SurfaceView} which can render the {@link Surface} of mirrored Task. 52 * 53 * @hide 54 */ 55 @SystemApi 56 @SuppressWarnings("[NotCloseable]") // View object won't be used in try-with-resources statement. 57 public final class MirroredSurfaceView extends SurfaceView { 58 private static final String TAG = MirroredSurfaceView.class.getSimpleName(); 59 private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); 60 61 private final SurfaceControl.Transaction mTransaction; 62 private final TouchableInsetsProvider mTouchableInsetsProvider; 63 64 private SurfaceControl mMirroredSurface; 65 private Rect mSourceBounds; 66 private CarActivityManager mCarAM; 67 MirroredSurfaceView(@onNull Context context)68 public MirroredSurfaceView(@NonNull Context context) { 69 this(context, /* attrs= */ null); 70 } 71 MirroredSurfaceView(@onNull Context context, @Nullable AttributeSet attrs)72 public MirroredSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs) { 73 this(context, attrs, /* defStyle= */ 0); 74 } 75 MirroredSurfaceView(@onNull Context context, @Nullable AttributeSet attrs, int defStyle)76 public MirroredSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs, 77 int defStyle) { 78 this(context, attrs, defStyle, /* defStyleRes= */ 0); 79 } 80 MirroredSurfaceView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)81 public MirroredSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs, 82 int defStyleAttr, int defStyleRes) { 83 this(context, attrs, defStyleAttr, defStyleRes, 84 new SurfaceControl.Transaction(), /* touchableInsetsProvider= */ null); 85 } 86 87 @VisibleForTesting MirroredSurfaceView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes, SurfaceControl.Transaction transaction, TouchableInsetsProvider touchableInsetsProvider)88 MirroredSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs, 89 int defStyleAttr, int defStyleRes, 90 SurfaceControl.Transaction transaction, 91 TouchableInsetsProvider touchableInsetsProvider) { 92 super(context, attrs, defStyleAttr, defStyleRes); 93 mTransaction = transaction; 94 mTouchableInsetsProvider = touchableInsetsProvider != null 95 ? touchableInsetsProvider : new TouchableInsetsProvider(this); 96 97 getHolder().addCallback(mSurfaceCallback); 98 if (context instanceof Activity) { 99 ((Activity) context).addDumpable(mDumper); 100 } 101 102 Car.createCar(/* context= */ context, /* handler= */ null, 103 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 104 (car, ready) -> { 105 if (!ready) { 106 Slog.w(TAG, "CarService looks crashed"); 107 mCarAM = null; 108 return; 109 } 110 mCarAM = car.getCarManager(CarActivityManager.class); 111 }); 112 } 113 114 /** 115 * Attaches the mirrored Surface which is represented by the given token to this View. 116 * <p> 117 * <b>Note:</b> MirroredSurfaceView will hold the Surface unless you call {@link #release()} 118 * explicitly. This is so that the host can keep the Surface when {@link Activity#onStop()} and 119 * {@link Activity#onStart()} are called again. 120 * 121 * @param token A token to access the Task Surface to mirror. 122 * @return true if the operation is successful. 123 */ 124 @RequiresPermission(Car.PERMISSION_ACCESS_MIRRORRED_SURFACE) 125 @MainThread mirrorSurface(@onNull IBinder token)126 public boolean mirrorSurface(@NonNull IBinder token) { 127 if (mCarAM == null) { 128 Slogf.e(TAG, "Failed to mirrorSurface because CarService isn't ready yet"); 129 return false; 130 } 131 if (mMirroredSurface != null) { 132 removeMirroredSurface(); 133 } 134 Pair<SurfaceControl, Rect> mirroredSurfaceInfo = mCarAM.getMirroredSurface(token); 135 if (mirroredSurfaceInfo == null) { 136 Slogf.e(TAG, "Failed to getMirroredSurface: token=%s", token); 137 return false; 138 } 139 mMirroredSurface = mirroredSurfaceInfo.first; 140 mSourceBounds = mirroredSurfaceInfo.second; 141 if (getHolder() == null) { 142 // reparentMirroredSurface() will happen when the SurfaceHolder is created. 143 if (DBG) Slog.d(TAG, "mirrorSurface: Surface is not ready"); 144 return true; 145 } 146 reparentMirroredSurface(); 147 return true; 148 } 149 150 /** 151 * Indicates a region of the view that is not touchable. 152 * 153 * @param obscuredRegion the obscured region of the view. 154 */ 155 @MainThread setObscuredTouchRegion(@ullable Region obscuredRegion)156 public void setObscuredTouchRegion(@Nullable Region obscuredRegion) { 157 mTouchableInsetsProvider.setObscuredTouchRegion(obscuredRegion); 158 } 159 160 /** 161 * Releases {@link MirroredSurfaceView} and associated {@link Surface}. 162 */ 163 @MainThread release()164 public void release() { 165 getHolder().removeCallback(mSurfaceCallback); 166 removeMirroredSurface(); 167 } 168 169 @Override finalize()170 protected void finalize() throws Throwable { 171 if (mMirroredSurface != null) { 172 removeMirroredSurface(); 173 } 174 super.finalize(); 175 } 176 reparentMirroredSurface()177 private void reparentMirroredSurface() { 178 if (DBG) Slog.d(TAG, "reparentMirroredSurface"); 179 calculateScale(); 180 mTransaction.setVisibility(mMirroredSurface, /* visible= */true) 181 .reparent(mMirroredSurface, getSurfaceControl()) 182 .apply(); 183 } 184 removeMirroredSurface()185 private void removeMirroredSurface() { 186 if (mMirroredSurface == null) { 187 Slog.w(TAG, "Skip removeMirroredSurface() on null Surface."); 188 return; 189 } 190 mTransaction.reparent(mMirroredSurface, null).apply(); 191 mMirroredSurface.release(); 192 mMirroredSurface = null; 193 } 194 calculateScale()195 private void calculateScale() { 196 if (mMirroredSurface == null) { 197 Slog.i(TAG, "Skip calculateScale() since MirroredSurface is not attached"); 198 return; 199 } 200 if (getWidth() == 0 || getHeight() == 0) { 201 Slog.i(TAG, "Skip calculateScale() since the View is not inflated."); 202 return; 203 } 204 // scale: > 1.0 Zoom out, < 1.0 Zoom in 205 float horizontalScale = (float) mSourceBounds.width() / getWidth(); 206 float verticalScale = (float) mSourceBounds.height() / getHeight(); 207 float mirroringScale = Math.max(horizontalScale, verticalScale); 208 209 int width = (int) Math.ceil(mSourceBounds.width() / mirroringScale); 210 int height = (int) Math.ceil(mSourceBounds.height() / mirroringScale); 211 Rect destBounds = new Rect(0, 0, width, height); 212 213 if (DBG) Slogf.d(TAG, "calculateScale: scale=%f", mirroringScale); 214 mTransaction.setGeometry(mMirroredSurface, mSourceBounds, destBounds, Surface.ROTATION_0) 215 .apply(); 216 } 217 218 private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { 219 @Override 220 public void surfaceCreated(@NonNull SurfaceHolder holder) { 221 if (mMirroredSurface == null) { 222 // reparentMirroredSurface() will happen when mirrorSurface() is called. 223 if (DBG) { 224 Slog.d(TAG, "surfaceCreated: skip reparenting" 225 + " because the mirrored Surface isn't ready."); 226 } 227 return; 228 } 229 reparentMirroredSurface(); 230 } 231 232 @Override 233 public void surfaceChanged(@NonNull SurfaceHolder holder, int format, 234 int width, int height) { 235 calculateScale(); 236 } 237 238 @Override 239 public void surfaceDestroyed(@NonNull SurfaceHolder holder) { 240 // Don't remove mMirroredSurface autonomously, because it may not get it again 241 // after some timeout. So the host Activity needs to keep it for the next onStart event. 242 } 243 }; 244 245 @Override onAttachedToWindow()246 protected void onAttachedToWindow() { 247 super.onAttachedToWindow(); 248 mTouchableInsetsProvider.addToViewTreeObserver(); 249 } 250 251 @Override onDetachedFromWindow()252 protected void onDetachedFromWindow() { 253 mTouchableInsetsProvider.removeFromViewTreeObserver(); 254 super.onDetachedFromWindow(); 255 } 256 257 private final Dumpable mDumper = new Dumpable() { 258 private static final String INDENTATION = " "; 259 260 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) 261 @NonNull 262 @Override 263 public String getDumpableName() { 264 return TAG; 265 } 266 267 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) 268 @Override 269 public void dump(@NonNull PrintWriter writer, @Nullable String[] args) { 270 writer.println(TAG + ": id=#" + Integer.toHexString(getId())); 271 writer.println(INDENTATION + "mirroredSurface=" + mMirroredSurface); 272 writer.println(INDENTATION + "sourceBound=" + mSourceBounds); 273 writer.println(INDENTATION + "touchableInsetsProvider=" + mTouchableInsetsProvider); 274 } 275 }; 276 } 277