blob: f6cd46ac87b533cfbbc8b26a625cad84a71b6697 [file] [log] [blame]
Andreas Huber27366fc2009-11-20 09:32:46 -08001/*
2 * Copyright (C) 2009 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//#define LOG_NDEBUG 0
18#define LOG_TAG "AwesomePlayer"
19#include <utils/Log.h>
20
21#include "include/AwesomePlayer.h"
Andreas Huber1314e732009-12-14 14:18:22 -080022#include "include/SoftwareRenderer.h"
Andreas Huber27366fc2009-11-20 09:32:46 -080023
Andreas Hubera67d5382009-12-10 15:32:12 -080024#include <binder/IPCThreadState.h>
Andreas Huber27366fc2009-11-20 09:32:46 -080025#include <media/stagefright/AudioPlayer.h>
26#include <media/stagefright/DataSource.h>
27#include <media/stagefright/FileSource.h>
28#include <media/stagefright/MediaBuffer.h>
Andreas Huberc79827a2010-01-05 10:54:55 -080029#include <media/stagefright/MediaDefs.h>
Andreas Huber27366fc2009-11-20 09:32:46 -080030#include <media/stagefright/MediaExtractor.h>
31#include <media/stagefright/MediaDebug.h>
32#include <media/stagefright/MediaSource.h>
33#include <media/stagefright/MetaData.h>
34#include <media/stagefright/OMXCodec.h>
Andreas Huberc79827a2010-01-05 10:54:55 -080035
Andreas Huber27366fc2009-11-20 09:32:46 -080036namespace android {
37
38struct AwesomeEvent : public TimedEventQueue::Event {
39 AwesomeEvent(AwesomePlayer *player, int32_t code)
40 : mPlayer(player),
41 mCode(code) {
42 }
43
44protected:
45 virtual ~AwesomeEvent() {}
46
47 virtual void fire(TimedEventQueue *queue, int64_t /* now_us */) {
48 mPlayer->onEvent(mCode);
49 }
50
51private:
52 AwesomePlayer *mPlayer;
53 int32_t mCode;
54
55 AwesomeEvent(const AwesomeEvent &);
56 AwesomeEvent &operator=(const AwesomeEvent &);
57};
58
Andreas Huber1314e732009-12-14 14:18:22 -080059struct AwesomeRemoteRenderer : public AwesomeRenderer {
60 AwesomeRemoteRenderer(const sp<IOMXRenderer> &target)
61 : mTarget(target) {
62 }
63
64 virtual void render(MediaBuffer *buffer) {
65 void *id;
66 if (buffer->meta_data()->findPointer(kKeyBufferID, &id)) {
67 mTarget->render((IOMX::buffer_id)id);
68 }
69 }
70
71private:
72 sp<IOMXRenderer> mTarget;
73
74 AwesomeRemoteRenderer(const AwesomeRemoteRenderer &);
75 AwesomeRemoteRenderer &operator=(const AwesomeRemoteRenderer &);
76};
77
78struct AwesomeLocalRenderer : public AwesomeRenderer {
79 AwesomeLocalRenderer(
80 OMX_COLOR_FORMATTYPE colorFormat,
81 const sp<ISurface> &surface,
82 size_t displayWidth, size_t displayHeight,
83 size_t decodedWidth, size_t decodedHeight)
84 : mTarget(new SoftwareRenderer(
85 colorFormat, surface, displayWidth, displayHeight,
86 decodedWidth, decodedHeight)) {
87 }
88
89 virtual void render(MediaBuffer *buffer) {
90 mTarget->render(
91 (const uint8_t *)buffer->data() + buffer->range_offset(),
92 buffer->range_length(), NULL);
93 }
94
95protected:
96 virtual ~AwesomeLocalRenderer() {
97 delete mTarget;
98 mTarget = NULL;
99 }
100
101private:
102 SoftwareRenderer *mTarget;
103
104 AwesomeLocalRenderer(const AwesomeLocalRenderer &);
105 AwesomeLocalRenderer &operator=(const AwesomeLocalRenderer &);;
106};
107
Andreas Huber27366fc2009-11-20 09:32:46 -0800108AwesomePlayer::AwesomePlayer()
109 : mTimeSource(NULL),
110 mAudioPlayer(NULL),
111 mLastVideoBuffer(NULL),
112 mVideoBuffer(NULL) {
113 CHECK_EQ(mClient.connect(), OK);
114
115 DataSource::RegisterDefaultSniffers();
116
117 mVideoEvent = new AwesomeEvent(this, 0);
118 mVideoEventPending = false;
119 mStreamDoneEvent = new AwesomeEvent(this, 1);
120 mStreamDoneEventPending = false;
121
122 mQueue.start();
123
124 reset();
125}
126
127AwesomePlayer::~AwesomePlayer() {
128 mQueue.stop();
129
130 reset();
131
132 mClient.disconnect();
133}
134
135void AwesomePlayer::cancelPlayerEvents() {
136 mQueue.cancelEvent(mVideoEvent->eventID());
137 mVideoEventPending = false;
138 mQueue.cancelEvent(mStreamDoneEvent->eventID());
139 mStreamDoneEventPending = false;
140}
141
Andreas Hubera3f43842010-01-21 10:28:45 -0800142void AwesomePlayer::setListener(const wp<MediaPlayerBase> &listener) {
Andreas Huber27366fc2009-11-20 09:32:46 -0800143 Mutex::Autolock autoLock(mLock);
144 mListener = listener;
145}
146
147status_t AwesomePlayer::setDataSource(const char *uri) {
148 Mutex::Autolock autoLock(mLock);
149
150 reset_l();
151
152 sp<MediaExtractor> extractor = MediaExtractor::CreateFromURI(uri);
153
154 if (extractor == NULL) {
155 return UNKNOWN_ERROR;
156 }
157
158 return setDataSource_l(extractor);
159}
160
161status_t AwesomePlayer::setDataSource(
162 int fd, int64_t offset, int64_t length) {
163 Mutex::Autolock autoLock(mLock);
164
165 reset_l();
166
167 sp<DataSource> source = new FileSource(fd, offset, length);
168
169 status_t err = source->initCheck();
170
171 if (err != OK) {
172 return err;
173 }
174
175 sp<MediaExtractor> extractor = MediaExtractor::Create(source);
176
177 if (extractor == NULL) {
178 return UNKNOWN_ERROR;
179 }
180
181 return setDataSource_l(extractor);
182}
183
184status_t AwesomePlayer::setDataSource_l(const sp<MediaExtractor> &extractor) {
185 reset_l();
186
187 bool haveAudio = false;
188 bool haveVideo = false;
189 for (size_t i = 0; i < extractor->countTracks(); ++i) {
190 sp<MetaData> meta = extractor->getTrackMetaData(i);
191
192 const char *mime;
193 CHECK(meta->findCString(kKeyMIMEType, &mime));
194
195 if (!haveVideo && !strncasecmp(mime, "video/", 6)) {
196 if (setVideoSource(extractor->getTrack(i)) == OK) {
197 haveVideo = true;
198 }
199 } else if (!haveAudio && !strncasecmp(mime, "audio/", 6)) {
200 if (setAudioSource(extractor->getTrack(i)) == OK) {
201 haveAudio = true;
202 }
203 }
204
205 if (haveAudio && haveVideo) {
206 break;
207 }
208 }
209
210 return !haveAudio && !haveVideo ? UNKNOWN_ERROR : OK;
211}
212
213void AwesomePlayer::reset() {
214 Mutex::Autolock autoLock(mLock);
215 reset_l();
216}
217
218void AwesomePlayer::reset_l() {
219 cancelPlayerEvents();
220
Andreas Huber3522b5a52010-01-22 14:36:53 -0800221 mVideoRenderer.clear();
222
Andreas Huber27366fc2009-11-20 09:32:46 -0800223 if (mLastVideoBuffer) {
224 mLastVideoBuffer->release();
225 mLastVideoBuffer = NULL;
226 }
227
228 if (mVideoBuffer) {
229 mVideoBuffer->release();
230 mVideoBuffer = NULL;
231 }
232
233 if (mVideoSource != NULL) {
234 mVideoSource->stop();
235 mVideoSource.clear();
236 }
237
238 mAudioSource.clear();
239
240 if (mTimeSource != mAudioPlayer) {
241 delete mTimeSource;
242 }
243 mTimeSource = NULL;
244
245 delete mAudioPlayer;
246 mAudioPlayer = NULL;
247
Andreas Huber27366fc2009-11-20 09:32:46 -0800248 mDurationUs = -1;
249 mFlags = 0;
250 mVideoWidth = mVideoHeight = -1;
251 mTimeSourceDeltaUs = 0;
252 mVideoTimeUs = 0;
253
254 mSeeking = false;
255 mSeekTimeUs = 0;
256}
257
258// static
259void AwesomePlayer::AudioNotify(void *_me, int what) {
260 AwesomePlayer *me = (AwesomePlayer *)_me;
261
262 Mutex::Autolock autoLock(me->mLock);
263
264 switch (what) {
265 case AudioPlayer::REACHED_EOS:
266 me->postStreamDoneEvent_l();
267 break;
268
269 case AudioPlayer::SEEK_COMPLETE:
270 {
Andreas Hubera3f43842010-01-21 10:28:45 -0800271 me->notifyListener_l(MEDIA_SEEK_COMPLETE);
Andreas Huber27366fc2009-11-20 09:32:46 -0800272 break;
273 }
274
275 default:
276 CHECK(!"should not be here.");
277 break;
278 }
279}
280
Andreas Hubera3f43842010-01-21 10:28:45 -0800281void AwesomePlayer::notifyListener_l(int msg) {
282 if (mListener != NULL) {
283 sp<MediaPlayerBase> listener = mListener.promote();
284
285 if (listener != NULL) {
286 listener->sendEvent(msg);
287 }
288 }
289}
290
Andreas Huber27366fc2009-11-20 09:32:46 -0800291void AwesomePlayer::onStreamDone() {
292 // Posted whenever any stream finishes playing.
293
294 Mutex::Autolock autoLock(mLock);
295 mStreamDoneEventPending = false;
296
297 if (mFlags & LOOPING) {
298 seekTo_l(0);
299
300 if (mVideoRenderer != NULL) {
301 postVideoEvent_l();
302 }
303 } else {
Andreas Hubera3f43842010-01-21 10:28:45 -0800304 notifyListener_l(MEDIA_PLAYBACK_COMPLETE);
Andreas Huber27366fc2009-11-20 09:32:46 -0800305
306 pause_l();
307 }
308}
309
310status_t AwesomePlayer::play() {
311 Mutex::Autolock autoLock(mLock);
312
313 if (mFlags & PLAYING) {
314 return OK;
315 }
316
317 mFlags |= PLAYING;
318 mFlags |= FIRST_FRAME;
319
Andreas Huberc1d5c922009-12-10 15:49:04 -0800320 bool deferredAudioSeek = false;
321
Andreas Huber27366fc2009-11-20 09:32:46 -0800322 if (mAudioSource != NULL) {
323 if (mAudioPlayer == NULL) {
324 if (mAudioSink != NULL) {
325 mAudioPlayer = new AudioPlayer(mAudioSink);
326
327 mAudioPlayer->setListenerCallback(
328 &AwesomePlayer::AudioNotify, this);
329
330 mAudioPlayer->setSource(mAudioSource);
331 mAudioPlayer->start();
332
333 delete mTimeSource;
334 mTimeSource = mAudioPlayer;
335
Andreas Huberc1d5c922009-12-10 15:49:04 -0800336 deferredAudioSeek = true;
Andreas Huber27366fc2009-11-20 09:32:46 -0800337 }
338 } else {
339 mAudioPlayer->resume();
340 }
341 }
342
343 if (mTimeSource == NULL && mAudioPlayer == NULL) {
344 mTimeSource = new SystemTimeSource;
345 }
346
347 if (mVideoSource != NULL) {
348 if (mVideoRenderer == NULL) {
349 initRenderer_l();
350 }
351
352 if (mVideoRenderer != NULL) {
353 // Kick off video playback
354 postVideoEvent_l();
355 }
356 }
357
Andreas Huberc1d5c922009-12-10 15:49:04 -0800358 if (deferredAudioSeek) {
359 // If there was a seek request while we were paused
360 // and we're just starting up again, honor the request now.
361 seekAudioIfNecessary_l();
362 }
363
Andreas Huber27366fc2009-11-20 09:32:46 -0800364 return OK;
365}
366
367void AwesomePlayer::initRenderer_l() {
368 if (mISurface != NULL) {
369 sp<MetaData> meta = mVideoSource->getFormat();
370
371 int32_t format;
372 const char *component;
373 int32_t decodedWidth, decodedHeight;
374 CHECK(meta->findInt32(kKeyColorFormat, &format));
375 CHECK(meta->findCString(kKeyDecoderComponent, &component));
376 CHECK(meta->findInt32(kKeyWidth, &decodedWidth));
377 CHECK(meta->findInt32(kKeyHeight, &decodedHeight));
378
Andreas Hubera67d5382009-12-10 15:32:12 -0800379 mVideoRenderer.clear();
380
381 // Must ensure that mVideoRenderer's destructor is actually executed
382 // before creating a new one.
383 IPCThreadState::self()->flushCommands();
384
Andreas Huber1314e732009-12-14 14:18:22 -0800385 if (!strncmp("OMX.", component, 4)) {
386 // Our OMX codecs allocate buffers on the media_server side
387 // therefore they require a remote IOMXRenderer that knows how
388 // to display them.
389 mVideoRenderer = new AwesomeRemoteRenderer(
390 mClient.interface()->createRenderer(
391 mISurface, component,
392 (OMX_COLOR_FORMATTYPE)format,
393 decodedWidth, decodedHeight,
394 mVideoWidth, mVideoHeight));
395 } else {
396 // Other decoders are instantiated locally and as a consequence
397 // allocate their buffers in local address space.
398 mVideoRenderer = new AwesomeLocalRenderer(
399 (OMX_COLOR_FORMATTYPE)format,
400 mISurface,
401 mVideoWidth, mVideoHeight,
402 decodedWidth, decodedHeight);
403 }
Andreas Huber27366fc2009-11-20 09:32:46 -0800404 }
405}
406
407status_t AwesomePlayer::pause() {
408 Mutex::Autolock autoLock(mLock);
409 return pause_l();
410}
411
412status_t AwesomePlayer::pause_l() {
413 if (!(mFlags & PLAYING)) {
414 return OK;
415 }
416
417 cancelPlayerEvents();
418
419 if (mAudioPlayer != NULL) {
420 mAudioPlayer->pause();
421 }
422
423 mFlags &= ~PLAYING;
424
425 return OK;
426}
427
428bool AwesomePlayer::isPlaying() const {
429 Mutex::Autolock autoLock(mLock);
430
431 return mFlags & PLAYING;
432}
433
434void AwesomePlayer::setISurface(const sp<ISurface> &isurface) {
435 Mutex::Autolock autoLock(mLock);
436
437 mISurface = isurface;
438}
439
440void AwesomePlayer::setAudioSink(
441 const sp<MediaPlayerBase::AudioSink> &audioSink) {
442 Mutex::Autolock autoLock(mLock);
443
444 mAudioSink = audioSink;
445}
446
447status_t AwesomePlayer::setLooping(bool shouldLoop) {
448 Mutex::Autolock autoLock(mLock);
449
450 mFlags = mFlags & ~LOOPING;
451
452 if (shouldLoop) {
453 mFlags |= LOOPING;
454 }
455
456 return OK;
457}
458
459status_t AwesomePlayer::getDuration(int64_t *durationUs) {
460 Mutex::Autolock autoLock(mLock);
461
462 if (mDurationUs < 0) {
463 return UNKNOWN_ERROR;
464 }
465
466 *durationUs = mDurationUs;
467
468 return OK;
469}
470
471status_t AwesomePlayer::getPosition(int64_t *positionUs) {
472 Mutex::Autolock autoLock(mLock);
473
474 if (mVideoRenderer != NULL) {
475 *positionUs = mVideoTimeUs;
476 } else if (mAudioPlayer != NULL) {
477 *positionUs = mAudioPlayer->getMediaTimeUs();
478 } else {
479 *positionUs = 0;
480 }
481
482 return OK;
483}
484
485status_t AwesomePlayer::seekTo(int64_t timeUs) {
486 Mutex::Autolock autoLock(mLock);
487 return seekTo_l(timeUs);
488}
489
490status_t AwesomePlayer::seekTo_l(int64_t timeUs) {
491 mSeeking = true;
492 mSeekTimeUs = timeUs;
493
494 seekAudioIfNecessary_l();
495
496 return OK;
497}
498
499void AwesomePlayer::seekAudioIfNecessary_l() {
500 if (mSeeking && mVideoRenderer == NULL && mAudioPlayer != NULL) {
501 mAudioPlayer->seekTo(mSeekTimeUs);
502
503 mSeeking = false;
504 }
505}
506
507status_t AwesomePlayer::getVideoDimensions(
508 int32_t *width, int32_t *height) const {
509 Mutex::Autolock autoLock(mLock);
510
511 if (mVideoWidth < 0 || mVideoHeight < 0) {
512 return UNKNOWN_ERROR;
513 }
514
515 *width = mVideoWidth;
516 *height = mVideoHeight;
517
518 return OK;
519}
520
521status_t AwesomePlayer::setAudioSource(const sp<MediaSource> &source) {
522 if (source == NULL) {
523 return UNKNOWN_ERROR;
524 }
525
Andreas Huberc79827a2010-01-05 10:54:55 -0800526 sp<MetaData> meta = source->getFormat();
527
528 const char *mime;
529 CHECK(meta->findCString(kKeyMIMEType, &mime));
530
531 if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_RAW)) {
532 mAudioSource = source;
533 } else {
534 mAudioSource = OMXCodec::Create(
535 mClient.interface(), source->getFormat(),
536 false, // createEncoder
537 source);
538 }
Andreas Huber27366fc2009-11-20 09:32:46 -0800539
540 if (mAudioSource != NULL) {
541 int64_t durationUs;
542 if (source->getFormat()->findInt64(kKeyDuration, &durationUs)) {
543 if (mDurationUs < 0 || durationUs > mDurationUs) {
544 mDurationUs = durationUs;
545 }
546 }
547 }
548
549 return mAudioSource != NULL ? OK : UNKNOWN_ERROR;
550}
551
552status_t AwesomePlayer::setVideoSource(const sp<MediaSource> &source) {
553 if (source == NULL) {
554 return UNKNOWN_ERROR;
555 }
556
557 mVideoSource = OMXCodec::Create(
558 mClient.interface(), source->getFormat(),
559 false, // createEncoder
560 source);
561
562 if (mVideoSource != NULL) {
563 int64_t durationUs;
564 if (source->getFormat()->findInt64(kKeyDuration, &durationUs)) {
565 if (mDurationUs < 0 || durationUs > mDurationUs) {
566 mDurationUs = durationUs;
567 }
568 }
569
570 CHECK(source->getFormat()->findInt32(kKeyWidth, &mVideoWidth));
571 CHECK(source->getFormat()->findInt32(kKeyHeight, &mVideoHeight));
572
573 mVideoSource->start();
574 }
575
576 return mVideoSource != NULL ? OK : UNKNOWN_ERROR;
577}
578
579void AwesomePlayer::onEvent(int32_t code) {
580 if (code == 1) {
581 onStreamDone();
582 return;
583 }
584
585 Mutex::Autolock autoLock(mLock);
586 mVideoEventPending = false;
587
588 if (mSeeking) {
589 if (mLastVideoBuffer) {
590 mLastVideoBuffer->release();
591 mLastVideoBuffer = NULL;
592 }
593
594 if (mVideoBuffer) {
595 mVideoBuffer->release();
596 mVideoBuffer = NULL;
597 }
598 }
599
600 if (!mVideoBuffer) {
601 MediaSource::ReadOptions options;
602 if (mSeeking) {
603 LOGV("seeking to %lld us (%.2f secs)", mSeekTimeUs, mSeekTimeUs / 1E6);
604
605 options.setSeekTo(mSeekTimeUs);
606 }
607 for (;;) {
608 status_t err = mVideoSource->read(&mVideoBuffer, &options);
Andreas Huberb1f5ee42009-12-14 15:34:11 -0800609 options.clearSeekTo();
Andreas Huber27366fc2009-11-20 09:32:46 -0800610
611 if (err != OK) {
612 CHECK_EQ(mVideoBuffer, NULL);
613
614 if (err == INFO_FORMAT_CHANGED) {
615 LOGV("VideoSource signalled format change.");
616
617 initRenderer_l();
618 continue;
619 }
620
621 postStreamDoneEvent_l();
622 return;
623 }
624
Andreas Hubera67d5382009-12-10 15:32:12 -0800625 if (mVideoBuffer->range_length() == 0) {
Andreas Huber6ddcf012009-12-10 15:32:12 -0800626 // Some decoders, notably the PV AVC software decoder
627 // return spurious empty buffers that we just want to ignore.
628
Andreas Hubera67d5382009-12-10 15:32:12 -0800629 mVideoBuffer->release();
630 mVideoBuffer = NULL;
631 continue;
632 }
633
Andreas Huber27366fc2009-11-20 09:32:46 -0800634 break;
635 }
636 }
637
638 int64_t timeUs;
639 CHECK(mVideoBuffer->meta_data()->findInt64(kKeyTime, &timeUs));
640
641 mVideoTimeUs = timeUs;
642
643 if (mSeeking) {
644 if (mAudioPlayer != NULL) {
645 LOGV("seeking audio to %lld us (%.2f secs).", timeUs, timeUs / 1E6);
646
647 mAudioPlayer->seekTo(timeUs);
648 } else {
649 // If we're playing video only, report seek complete now,
650 // otherwise audio player will notify us later.
Andreas Hubera3f43842010-01-21 10:28:45 -0800651 notifyListener_l(MEDIA_SEEK_COMPLETE);
Andreas Huber27366fc2009-11-20 09:32:46 -0800652 }
653
654 mFlags |= FIRST_FRAME;
655 mSeeking = false;
656 }
657
658 if (mFlags & FIRST_FRAME) {
659 mFlags &= ~FIRST_FRAME;
660
661 mTimeSourceDeltaUs = mTimeSource->getRealTimeUs() - timeUs;
662 }
663
664 int64_t realTimeUs, mediaTimeUs;
665 if (mAudioPlayer != NULL
666 && mAudioPlayer->getMediaTimeMapping(&realTimeUs, &mediaTimeUs)) {
667 mTimeSourceDeltaUs = realTimeUs - mediaTimeUs;
668 }
669
670 int64_t nowUs = mTimeSource->getRealTimeUs() - mTimeSourceDeltaUs;
671
672 int64_t latenessUs = nowUs - timeUs;
673
Andreas Huber24b0a952009-11-23 14:02:00 -0800674 if (latenessUs > 40000) {
675 // We're more than 40ms late.
676 LOGI("we're late by %lld us (%.2f secs)", latenessUs, latenessUs / 1E6);
Andreas Huber27366fc2009-11-20 09:32:46 -0800677
678 mVideoBuffer->release();
679 mVideoBuffer = NULL;
680
681 postVideoEvent_l();
682 return;
683 }
684
685 if (latenessUs < -10000) {
686 // We're more than 10ms early.
687
688 postVideoEvent_l(10000);
689 return;
690 }
691
Andreas Huber1314e732009-12-14 14:18:22 -0800692 mVideoRenderer->render(mVideoBuffer);
Andreas Huber27366fc2009-11-20 09:32:46 -0800693
694 if (mLastVideoBuffer) {
695 mLastVideoBuffer->release();
696 mLastVideoBuffer = NULL;
697 }
698 mLastVideoBuffer = mVideoBuffer;
699 mVideoBuffer = NULL;
700
701 postVideoEvent_l();
702}
703
704void AwesomePlayer::postVideoEvent_l(int64_t delayUs) {
705 if (mVideoEventPending) {
706 return;
707 }
708
709 mVideoEventPending = true;
710 mQueue.postEventWithDelay(mVideoEvent, delayUs < 0 ? 10000 : delayUs);
711}
712
713void AwesomePlayer::postStreamDoneEvent_l() {
714 if (mStreamDoneEventPending) {
715 return;
716 }
717 mStreamDoneEventPending = true;
718 mQueue.postEvent(mStreamDoneEvent);
719}
720
721} // namespace android
722