1 /*
2 * Copyright 2019 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 #include <cutils/compiler.h>
18 #include <gui/BufferQueue.h>
19 #include <surfacetexture/ImageConsumer.h>
20 #include <surfacetexture/SurfaceTexture.h>
21 #include <surfacetexture/surface_texture_platform.h>
22 #include <math/mat4.h>
23 #include <system/window.h>
24 #include <utils/Trace.h>
25
26 #include <com_android_graphics_libgui_flags.h>
27
28 namespace android {
29
30 // Macros for including the SurfaceTexture name in log messages
31 #define SFT_LOGV(x, ...) ALOGV("[%s] " x, mName.c_str(), ##__VA_ARGS__)
32 #define SFT_LOGD(x, ...) ALOGD("[%s] " x, mName.c_str(), ##__VA_ARGS__)
33 #define SFT_LOGW(x, ...) ALOGW("[%s] " x, mName.c_str(), ##__VA_ARGS__)
34 #define SFT_LOGE(x, ...) ALOGE("[%s] " x, mName.c_str(), ##__VA_ARGS__)
35
36 static const mat4 mtxIdentity;
37
SurfaceTexture(const sp<IGraphicBufferConsumer> & bq,uint32_t tex,uint32_t texTarget,bool useFenceSync,bool isControlledByApp)38 SurfaceTexture::SurfaceTexture(const sp<IGraphicBufferConsumer>& bq, uint32_t tex,
39 uint32_t texTarget, bool useFenceSync, bool isControlledByApp)
40 : ConsumerBase(bq, isControlledByApp),
41 mCurrentCrop(Rect::EMPTY_RECT),
42 mCurrentTransform(0),
43 mCurrentScalingMode(NATIVE_WINDOW_SCALING_MODE_FREEZE),
44 mCurrentFence(Fence::NO_FENCE),
45 mCurrentTimestamp(0),
46 mCurrentDataSpace(HAL_DATASPACE_UNKNOWN),
47 mCurrentFrameNumber(0),
48 mDefaultWidth(1),
49 mDefaultHeight(1),
50 mFilteringEnabled(true),
51 mTexName(tex),
52 mUseFenceSync(useFenceSync),
53 mTexTarget(texTarget),
54 mCurrentTexture(BufferQueue::INVALID_BUFFER_SLOT),
55 mOpMode(OpMode::attachedToGL) {
56 SFT_LOGV("SurfaceTexture");
57
58 memcpy(mCurrentTransformMatrix, mtxIdentity.asArray(), sizeof(mCurrentTransformMatrix));
59
60 mConsumer->setConsumerUsageBits(DEFAULT_USAGE_FLAGS);
61 }
62
SurfaceTexture(const sp<IGraphicBufferConsumer> & bq,uint32_t texTarget,bool useFenceSync,bool isControlledByApp)63 SurfaceTexture::SurfaceTexture(const sp<IGraphicBufferConsumer>& bq, uint32_t texTarget,
64 bool useFenceSync, bool isControlledByApp)
65 : ConsumerBase(bq, isControlledByApp),
66 mCurrentCrop(Rect::EMPTY_RECT),
67 mCurrentTransform(0),
68 mCurrentScalingMode(NATIVE_WINDOW_SCALING_MODE_FREEZE),
69 mCurrentFence(Fence::NO_FENCE),
70 mCurrentTimestamp(0),
71 mCurrentDataSpace(HAL_DATASPACE_UNKNOWN),
72 mCurrentFrameNumber(0),
73 mDefaultWidth(1),
74 mDefaultHeight(1),
75 mFilteringEnabled(true),
76 mTexName(0),
77 mUseFenceSync(useFenceSync),
78 mTexTarget(texTarget),
79 mCurrentTexture(BufferQueue::INVALID_BUFFER_SLOT),
80 mOpMode(OpMode::detached) {
81 SFT_LOGV("SurfaceTexture");
82
83 memcpy(mCurrentTransformMatrix, mtxIdentity.asArray(), sizeof(mCurrentTransformMatrix));
84
85 mConsumer->setConsumerUsageBits(DEFAULT_USAGE_FLAGS);
86 }
87
setDefaultBufferSize(uint32_t w,uint32_t h)88 status_t SurfaceTexture::setDefaultBufferSize(uint32_t w, uint32_t h) {
89 Mutex::Autolock lock(mMutex);
90 if (mAbandoned) {
91 SFT_LOGE("setDefaultBufferSize: SurfaceTexture is abandoned!");
92 return NO_INIT;
93 }
94 mDefaultWidth = w;
95 mDefaultHeight = h;
96 return mConsumer->setDefaultBufferSize(w, h);
97 }
98
updateTexImage()99 status_t SurfaceTexture::updateTexImage() {
100 ATRACE_CALL();
101 SFT_LOGV("updateTexImage");
102 Mutex::Autolock lock(mMutex);
103
104 if (mAbandoned) {
105 SFT_LOGE("updateTexImage: SurfaceTexture is abandoned!");
106 return NO_INIT;
107 }
108
109 return mEGLConsumer.updateTexImage(*this);
110 }
111
releaseTexImage()112 status_t SurfaceTexture::releaseTexImage() {
113 // releaseTexImage can be invoked even when not attached to a GL context.
114 ATRACE_CALL();
115 SFT_LOGV("releaseTexImage");
116 Mutex::Autolock lock(mMutex);
117
118 if (mAbandoned) {
119 SFT_LOGE("releaseTexImage: SurfaceTexture is abandoned!");
120 return NO_INIT;
121 }
122
123 return mEGLConsumer.releaseTexImage(*this);
124 }
125
acquireBufferLocked(BufferItem * item,nsecs_t presentWhen,uint64_t maxFrameNumber)126 status_t SurfaceTexture::acquireBufferLocked(BufferItem* item, nsecs_t presentWhen,
127 uint64_t maxFrameNumber) {
128 status_t err = ConsumerBase::acquireBufferLocked(item, presentWhen, maxFrameNumber);
129 if (err != NO_ERROR) {
130 return err;
131 }
132
133 switch (mOpMode) {
134 case OpMode::attachedToConsumer:
135 break;
136 case OpMode::attachedToGL:
137 mEGLConsumer.onAcquireBufferLocked(item, *this);
138 break;
139 case OpMode::detached:
140 break;
141 }
142
143 return NO_ERROR;
144 }
145
releaseBufferLocked(int buf,sp<GraphicBuffer> graphicBuffer,EGLDisplay display,EGLSyncKHR eglFence)146 status_t SurfaceTexture::releaseBufferLocked(int buf, sp<GraphicBuffer> graphicBuffer,
147 EGLDisplay display, EGLSyncKHR eglFence) {
148 // release the buffer if it hasn't already been discarded by the
149 // BufferQueue. This can happen, for example, when the producer of this
150 // buffer has reallocated the original buffer slot after this buffer
151 // was acquired.
152 status_t err = ConsumerBase::releaseBufferLocked(buf, graphicBuffer, display, eglFence);
153 // We could be releasing an EGL/Vulkan buffer, even if not currently
154 // attached to a GL context.
155 mImageConsumer.onReleaseBufferLocked(buf);
156 mEGLConsumer.onReleaseBufferLocked(buf);
157 return err;
158 }
159
detachFromContext()160 status_t SurfaceTexture::detachFromContext() {
161 ATRACE_CALL();
162 SFT_LOGV("detachFromContext");
163 Mutex::Autolock lock(mMutex);
164
165 if (mAbandoned) {
166 SFT_LOGE("detachFromContext: abandoned SurfaceTexture");
167 return NO_INIT;
168 }
169
170 if (mOpMode != OpMode::attachedToGL) {
171 SFT_LOGE("detachFromContext: SurfaceTexture is not attached to a GL context");
172 return INVALID_OPERATION;
173 }
174
175 status_t err = mEGLConsumer.detachFromContext(*this);
176 if (err == OK) {
177 mOpMode = OpMode::detached;
178 }
179
180 return err;
181 }
182
attachToContext(uint32_t tex)183 status_t SurfaceTexture::attachToContext(uint32_t tex) {
184 ATRACE_CALL();
185 SFT_LOGV("attachToContext");
186 Mutex::Autolock lock(mMutex);
187
188 if (mAbandoned) {
189 SFT_LOGE("attachToContext: abandoned SurfaceTexture");
190 return NO_INIT;
191 }
192
193 if (mOpMode != OpMode::detached) {
194 SFT_LOGE("attachToContext: SurfaceTexture is already attached to a "
195 "context");
196 return INVALID_OPERATION;
197 }
198
199 return mEGLConsumer.attachToContext(tex, *this);
200 }
201
takeConsumerOwnership()202 void SurfaceTexture::takeConsumerOwnership() {
203 ATRACE_CALL();
204 Mutex::Autolock _l(mMutex);
205 if (mAbandoned) {
206 SFT_LOGE("attachToView: abandoned SurfaceTexture");
207 return;
208 }
209 if (mOpMode == OpMode::detached) {
210 mOpMode = OpMode::attachedToConsumer;
211
212 if (mCurrentTexture != BufferQueue::INVALID_BUFFER_SLOT) {
213 // release possible EGLConsumer texture cache
214 mEGLConsumer.onFreeBufferLocked(mCurrentTexture);
215 mEGLConsumer.onAbandonLocked();
216 }
217 } else {
218 SFT_LOGE("attachToView: already attached");
219 }
220 }
221
releaseConsumerOwnership()222 void SurfaceTexture::releaseConsumerOwnership() {
223 ATRACE_CALL();
224 Mutex::Autolock _l(mMutex);
225
226 if (mAbandoned) {
227 SFT_LOGE("detachFromView: abandoned SurfaceTexture");
228 return;
229 }
230
231 if (mOpMode == OpMode::attachedToConsumer) {
232 mOpMode = OpMode::detached;
233 } else {
234 SFT_LOGE("detachFromView: not attached to View");
235 }
236 }
237
getCurrentTextureTarget() const238 uint32_t SurfaceTexture::getCurrentTextureTarget() const {
239 return mTexTarget;
240 }
241
getTransformMatrix(float mtx[16])242 void SurfaceTexture::getTransformMatrix(float mtx[16]) {
243 Mutex::Autolock lock(mMutex);
244 memcpy(mtx, mCurrentTransformMatrix, sizeof(mCurrentTransformMatrix));
245 }
246
setFilteringEnabled(bool enabled)247 void SurfaceTexture::setFilteringEnabled(bool enabled) {
248 Mutex::Autolock lock(mMutex);
249 if (mAbandoned) {
250 SFT_LOGE("setFilteringEnabled: SurfaceTexture is abandoned!");
251 return;
252 }
253 bool needsRecompute = mFilteringEnabled != enabled;
254 mFilteringEnabled = enabled;
255
256 if (needsRecompute && mCurrentTexture == BufferQueue::INVALID_BUFFER_SLOT) {
257 SFT_LOGD("setFilteringEnabled called with no current item");
258 }
259
260 if (needsRecompute && mCurrentTexture != BufferQueue::INVALID_BUFFER_SLOT) {
261 computeCurrentTransformMatrixLocked();
262 }
263 }
264
computeCurrentTransformMatrixLocked()265 void SurfaceTexture::computeCurrentTransformMatrixLocked() {
266 SFT_LOGV("computeCurrentTransformMatrixLocked");
267 sp<GraphicBuffer> buf = (mCurrentTexture == BufferQueue::INVALID_BUFFER_SLOT)
268 ? nullptr
269 : mSlots[mCurrentTexture].mGraphicBuffer;
270 if (buf == nullptr) {
271 SFT_LOGD("computeCurrentTransformMatrixLocked: no current item");
272 }
273 computeTransformMatrix(mCurrentTransformMatrix, buf, mCurrentCrop, mCurrentTransform,
274 mFilteringEnabled);
275 }
276
computeTransformMatrix(float outTransform[16],const sp<GraphicBuffer> & buf,const Rect & cropRect,uint32_t transform,bool filtering)277 void SurfaceTexture::computeTransformMatrix(float outTransform[16], const sp<GraphicBuffer>& buf,
278 const Rect& cropRect, uint32_t transform,
279 bool filtering) {
280 // Transform matrices
281 static const mat4 mtxFlipH(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1);
282 static const mat4 mtxFlipV(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1);
283 static const mat4 mtxRot90(0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1);
284
285 mat4 xform;
286 if (transform & NATIVE_WINDOW_TRANSFORM_FLIP_H) {
287 xform *= mtxFlipH;
288 }
289 if (transform & NATIVE_WINDOW_TRANSFORM_FLIP_V) {
290 xform *= mtxFlipV;
291 }
292 if (transform & NATIVE_WINDOW_TRANSFORM_ROT_90) {
293 xform *= mtxRot90;
294 }
295
296 if (!cropRect.isEmpty() && buf.get()) {
297 float tx = 0.0f, ty = 0.0f, sx = 1.0f, sy = 1.0f;
298 float bufferWidth = buf->getWidth();
299 float bufferHeight = buf->getHeight();
300 float shrinkAmount = 0.0f;
301 if (filtering) {
302 // In order to prevent bilinear sampling beyond the edge of the
303 // crop rectangle we may need to shrink it by 2 texels in each
304 // dimension. Normally this would just need to take 1/2 a texel
305 // off each end, but because the chroma channels of YUV420 images
306 // are subsampled we may need to shrink the crop region by a whole
307 // texel on each side.
308 switch (buf->getPixelFormat()) {
309 case PIXEL_FORMAT_RGBA_8888:
310 case PIXEL_FORMAT_RGBX_8888:
311 case PIXEL_FORMAT_RGBA_FP16:
312 case PIXEL_FORMAT_RGBA_1010102:
313 case PIXEL_FORMAT_RGB_888:
314 case PIXEL_FORMAT_RGB_565:
315 case PIXEL_FORMAT_BGRA_8888:
316 // We know there's no subsampling of any channels, so we
317 // only need to shrink by a half a pixel.
318 shrinkAmount = 0.5;
319 break;
320
321 default:
322 // If we don't recognize the format, we must assume the
323 // worst case (that we care about), which is YUV420.
324 shrinkAmount = 1.0;
325 break;
326 }
327 }
328
329 // Only shrink the dimensions that are not the size of the buffer.
330 if (cropRect.width() < bufferWidth) {
331 tx = (float(cropRect.left) + shrinkAmount) / bufferWidth;
332 sx = (float(cropRect.width()) - (2.0f * shrinkAmount)) / bufferWidth;
333 }
334 if (cropRect.height() < bufferHeight) {
335 ty = (float(bufferHeight - cropRect.bottom) + shrinkAmount) / bufferHeight;
336 sy = (float(cropRect.height()) - (2.0f * shrinkAmount)) / bufferHeight;
337 }
338
339 mat4 crop(sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1);
340 xform = crop * xform;
341 }
342
343 // SurfaceFlinger expects the top of its window textures to be at a Y
344 // coordinate of 0, so SurfaceTexture must behave the same way. We don't
345 // want to expose this to applications, however, so we must add an
346 // additional vertical flip to the transform after all the other transforms.
347 xform = mtxFlipV * xform;
348
349 memcpy(outTransform, xform.asArray(), sizeof(xform));
350 }
351
scaleDownCrop(const Rect & crop,uint32_t bufferWidth,uint32_t bufferHeight)352 Rect SurfaceTexture::scaleDownCrop(const Rect& crop, uint32_t bufferWidth, uint32_t bufferHeight) {
353 Rect outCrop = crop;
354
355 uint32_t newWidth = static_cast<uint32_t>(crop.width());
356 uint32_t newHeight = static_cast<uint32_t>(crop.height());
357
358 if (newWidth * bufferHeight > newHeight * bufferWidth) {
359 newWidth = newHeight * bufferWidth / bufferHeight;
360 ALOGV("too wide: newWidth = %d", newWidth);
361 } else if (newWidth * bufferHeight < newHeight * bufferWidth) {
362 newHeight = newWidth * bufferHeight / bufferWidth;
363 ALOGV("too tall: newHeight = %d", newHeight);
364 }
365
366 uint32_t currentWidth = static_cast<uint32_t>(crop.width());
367 uint32_t currentHeight = static_cast<uint32_t>(crop.height());
368
369 // The crop is too wide
370 if (newWidth < currentWidth) {
371 uint32_t dw = currentWidth - newWidth;
372 auto halfdw = dw / 2;
373 outCrop.left += halfdw;
374 // Not halfdw because it would subtract 1 too few when dw is odd
375 outCrop.right -= (dw - halfdw);
376 // The crop is too tall
377 } else if (newHeight < currentHeight) {
378 uint32_t dh = currentHeight - newHeight;
379 auto halfdh = dh / 2;
380 outCrop.top += halfdh;
381 // Not halfdh because it would subtract 1 too few when dh is odd
382 outCrop.bottom -= (dh - halfdh);
383 }
384
385 ALOGV("getCurrentCrop final crop [%d,%d,%d,%d]", outCrop.left, outCrop.top, outCrop.right,
386 outCrop.bottom);
387
388 return outCrop;
389 }
390
getTimestamp()391 nsecs_t SurfaceTexture::getTimestamp() {
392 SFT_LOGV("getTimestamp");
393 Mutex::Autolock lock(mMutex);
394 return mCurrentTimestamp;
395 }
396
getCurrentDataSpace()397 android_dataspace SurfaceTexture::getCurrentDataSpace() {
398 SFT_LOGV("getCurrentDataSpace");
399 Mutex::Autolock lock(mMutex);
400 return mCurrentDataSpace;
401 }
402
getFrameNumber()403 uint64_t SurfaceTexture::getFrameNumber() {
404 SFT_LOGV("getFrameNumber");
405 Mutex::Autolock lock(mMutex);
406 return mCurrentFrameNumber;
407 }
408
getCurrentCrop() const409 Rect SurfaceTexture::getCurrentCrop() const {
410 Mutex::Autolock lock(mMutex);
411 return (mCurrentScalingMode == NATIVE_WINDOW_SCALING_MODE_SCALE_CROP)
412 ? scaleDownCrop(mCurrentCrop, mDefaultWidth, mDefaultHeight)
413 : mCurrentCrop;
414 }
415
getCurrentTransform() const416 uint32_t SurfaceTexture::getCurrentTransform() const {
417 Mutex::Autolock lock(mMutex);
418 return mCurrentTransform;
419 }
420
getCurrentScalingMode() const421 uint32_t SurfaceTexture::getCurrentScalingMode() const {
422 Mutex::Autolock lock(mMutex);
423 return mCurrentScalingMode;
424 }
425
getCurrentFence() const426 sp<Fence> SurfaceTexture::getCurrentFence() const {
427 Mutex::Autolock lock(mMutex);
428 return mCurrentFence;
429 }
430
getCurrentFenceTime() const431 std::shared_ptr<FenceTime> SurfaceTexture::getCurrentFenceTime() const {
432 Mutex::Autolock lock(mMutex);
433 return mCurrentFenceTime;
434 }
435
freeBufferLocked(int slotIndex)436 void SurfaceTexture::freeBufferLocked(int slotIndex) {
437 SFT_LOGV("freeBufferLocked: slotIndex=%d", slotIndex);
438 if (slotIndex == mCurrentTexture) {
439 mCurrentTexture = BufferQueue::INVALID_BUFFER_SLOT;
440 }
441 // The slotIndex buffer could have EGL cache, but there is no way to tell
442 // for sure. Buffers can be freed after SurfaceTexture has detached from GL
443 // context or View.
444 mEGLConsumer.onFreeBufferLocked(slotIndex);
445 ConsumerBase::freeBufferLocked(slotIndex);
446 }
447
abandonLocked()448 void SurfaceTexture::abandonLocked() {
449 SFT_LOGV("abandonLocked");
450 mEGLConsumer.onAbandonLocked();
451 ConsumerBase::abandonLocked();
452 }
453
setConsumerUsageBits(uint64_t usage)454 status_t SurfaceTexture::setConsumerUsageBits(uint64_t usage) {
455 return ConsumerBase::setConsumerUsageBits(usage | DEFAULT_USAGE_FLAGS);
456 }
457
dumpLocked(String8 & result,const char * prefix) const458 void SurfaceTexture::dumpLocked(String8& result, const char* prefix) const {
459 result.appendFormat("%smTexName=%d mCurrentTexture=%d\n"
460 "%smCurrentCrop=[%d,%d,%d,%d] mCurrentTransform=%#x\n",
461 prefix, mTexName, mCurrentTexture, prefix, mCurrentCrop.left,
462 mCurrentCrop.top, mCurrentCrop.right, mCurrentCrop.bottom,
463 mCurrentTransform);
464
465 ConsumerBase::dumpLocked(result, prefix);
466 }
467
dequeueBuffer(int * outSlotid,android_dataspace * outDataspace,HdrMetadata * outHdrMetadata,float * outTransformMatrix,uint32_t * outTransform,bool * outQueueEmpty,SurfaceTexture_createReleaseFence createFence,SurfaceTexture_fenceWait fenceWait,void * fencePassThroughHandle,ARect * currentCrop)468 sp<GraphicBuffer> SurfaceTexture::dequeueBuffer(int* outSlotid, android_dataspace* outDataspace,
469 HdrMetadata* outHdrMetadata,
470 float* outTransformMatrix, uint32_t* outTransform,
471 bool* outQueueEmpty,
472 SurfaceTexture_createReleaseFence createFence,
473 SurfaceTexture_fenceWait fenceWait,
474 void* fencePassThroughHandle, ARect* currentCrop) {
475 Mutex::Autolock _l(mMutex);
476 sp<GraphicBuffer> buffer;
477
478 if (mAbandoned) {
479 SFT_LOGE("dequeueImage: SurfaceTexture is abandoned!");
480 return buffer;
481 }
482
483 if (mOpMode != OpMode::attachedToConsumer) {
484 SFT_LOGE("dequeueImage: SurfaceTexture is not attached to a View");
485 return buffer;
486 }
487
488 buffer = mImageConsumer.dequeueBuffer(outSlotid, outDataspace, outHdrMetadata, outQueueEmpty,
489 *this, createFence, fenceWait, fencePassThroughHandle);
490 memcpy(outTransformMatrix, mCurrentTransformMatrix, sizeof(mCurrentTransformMatrix));
491 *outTransform = mCurrentTransform;
492 *currentCrop = mCurrentCrop;
493 return buffer;
494 }
495
setSurfaceTextureListener(const sp<android::SurfaceTexture::SurfaceTextureListener> & listener)496 void SurfaceTexture::setSurfaceTextureListener(
497 const sp<android::SurfaceTexture::SurfaceTextureListener>& listener) {
498 SFT_LOGV("setSurfaceTextureListener");
499
500 Mutex::Autolock _l(mMutex);
501 mSurfaceTextureListener = listener;
502 if (mSurfaceTextureListener != nullptr) {
503 mFrameAvailableListenerProxy =
504 sp<FrameAvailableListenerProxy>::make(mSurfaceTextureListener);
505 setFrameAvailableListener(mFrameAvailableListenerProxy);
506 } else {
507 mFrameAvailableListenerProxy.clear();
508 }
509 }
510
onFrameAvailable(const BufferItem & item)511 void SurfaceTexture::FrameAvailableListenerProxy::onFrameAvailable(const BufferItem& item) {
512 const auto listener = mSurfaceTextureListener.promote();
513 if (listener) {
514 listener->onFrameAvailable(item);
515 }
516 }
517
518 #if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(BQ_SETFRAMERATE)
onSetFrameRate(float frameRate,int8_t compatibility,int8_t changeFrameRateStrategy)519 void SurfaceTexture::onSetFrameRate(float frameRate, int8_t compatibility,
520 int8_t changeFrameRateStrategy) {
521 SFT_LOGV("onSetFrameRate: %.2f", frameRate);
522
523 auto listener = [&] {
524 Mutex::Autolock _l(mMutex);
525 return mSurfaceTextureListener;
526 }();
527
528 if (listener) {
529 listener->onSetFrameRate(frameRate, compatibility, changeFrameRateStrategy);
530 }
531 }
532 #endif
533
534 } // namespace android
535