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