diff options
230 files changed, 14598 insertions, 4443 deletions
@@ -136,6 +136,7 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/appwidget/IAppWidgetService.aidl \ core/java/com/android/internal/appwidget/IAppWidgetHost.aidl \ core/java/com/android/internal/backup/IBackupTransport.aidl \ + core/java/com/android/internal/os/IDropBoxService.aidl \ core/java/com/android/internal/os/IResultReceiver.aidl \ core/java/com/android/internal/view/IInputContext.aidl \ core/java/com/android/internal/view/IInputContextCallback.aidl \ @@ -217,6 +218,7 @@ aidl_files := \ frameworks/base/core/java/android/appwidget/AppWidgetProviderInfo.aidl \ frameworks/base/core/java/android/net/Uri.aidl \ frameworks/base/core/java/android/os/Bundle.aidl \ + frameworks/base/core/java/android/os/DropBox.aidl \ frameworks/base/core/java/android/os/ParcelFileDescriptor.aidl \ frameworks/base/core/java/android/os/ParcelUuid.aidl \ frameworks/base/core/java/android/view/KeyEvent.aidl \ diff --git a/api/current.xml b/api/current.xml index 32d0f1e..7c1497f 100644 --- a/api/current.xml +++ b/api/current.xml @@ -1967,6 +1967,17 @@ visibility="public" > </field> +<field name="autoStart" + type="int" + transient="false" + volatile="false" + value="16843446" + static="true" + final="true" + deprecated="not deprecated" + visibility="public" +> +</field> <field name="autoText" type="int" transient="false" @@ -119287,6 +119298,23 @@ <parameter name="origId" type="long"> </parameter> </method> +<method name="cancelThumbnailRequest" + return="void" + abstract="false" + native="false" + synchronized="false" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="cr" type="android.content.ContentResolver"> +</parameter> +<parameter name="origId" type="long"> +</parameter> +<parameter name="groupId" type="long"> +</parameter> +</method> <method name="getContentUri" return="android.net.Uri" abstract="false" @@ -119319,6 +119347,27 @@ <parameter name="options" type="android.graphics.BitmapFactory.Options"> </parameter> </method> +<method name="getThumbnail" + return="android.graphics.Bitmap" + abstract="false" + native="false" + synchronized="false" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="cr" type="android.content.ContentResolver"> +</parameter> +<parameter name="origId" type="long"> +</parameter> +<parameter name="groupId" type="long"> +</parameter> +<parameter name="kind" type="int"> +</parameter> +<parameter name="options" type="android.graphics.BitmapFactory.Options"> +</parameter> +</method> <method name="query" return="android.database.Cursor" abstract="false" @@ -119744,6 +119793,23 @@ <parameter name="origId" type="long"> </parameter> </method> +<method name="cancelThumbnailRequest" + return="void" + abstract="false" + native="false" + synchronized="false" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="cr" type="android.content.ContentResolver"> +</parameter> +<parameter name="origId" type="long"> +</parameter> +<parameter name="groupId" type="long"> +</parameter> +</method> <method name="getContentUri" return="android.net.Uri" abstract="false" @@ -119776,6 +119842,27 @@ <parameter name="options" type="android.graphics.BitmapFactory.Options"> </parameter> </method> +<method name="getThumbnail" + return="android.graphics.Bitmap" + abstract="false" + native="false" + synchronized="false" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="cr" type="android.content.ContentResolver"> +</parameter> +<parameter name="origId" type="long"> +</parameter> +<parameter name="groupId" type="long"> +</parameter> +<parameter name="kind" type="int"> +</parameter> +<parameter name="options" type="android.graphics.BitmapFactory.Options"> +</parameter> +</method> <field name="DATA" type="java.lang.String" transient="false" @@ -132567,6 +132654,673 @@ </parameter> </method> </class> +<class name="MockCursor" + extends="java.lang.Object" + abstract="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<implements name="android.database.Cursor"> +</implements> +<constructor name="MockCursor" + type="android.test.mock.MockCursor" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</constructor> +<method name="abortUpdates" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="close" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="commitUpdates" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="commitUpdates" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="values" type="java.util.Map<? extends java.lang.Long, ? extends java.util.Map<java.lang.String, java.lang.Object>>"> +</parameter> +</method> +<method name="copyStringToBuffer" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="buffer" type="android.database.CharArrayBuffer"> +</parameter> +</method> +<method name="deactivate" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="deleteRow" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getBlob" + return="byte[]" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getColumnCount" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getColumnIndex" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnName" type="java.lang.String"> +</parameter> +</method> +<method name="getColumnIndexOrThrow" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnName" type="java.lang.String"> +</parameter> +</method> +<method name="getColumnName" + return="java.lang.String" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getColumnNames" + return="java.lang.String[]" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getCount" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getDouble" + return="double" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getExtras" + return="android.os.Bundle" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getFloat" + return="float" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getInt" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getLong" + return="long" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getPosition" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getShort" + return="short" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getString" + return="java.lang.String" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="getWantsAllOnMoveCalls" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="hasUpdates" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isAfterLast" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isBeforeFirst" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isClosed" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isFirst" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isLast" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isNull" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +<method name="move" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="offset" type="int"> +</parameter> +</method> +<method name="moveToFirst" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="moveToLast" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="moveToNext" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="moveToPosition" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="position" type="int"> +</parameter> +</method> +<method name="moveToPrevious" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="registerContentObserver" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="observer" type="android.database.ContentObserver"> +</parameter> +</method> +<method name="registerDataSetObserver" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="observer" type="android.database.DataSetObserver"> +</parameter> +</method> +<method name="requery" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="respond" + return="android.os.Bundle" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="extras" type="android.os.Bundle"> +</parameter> +</method> +<method name="setNotificationUri" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="cr" type="android.content.ContentResolver"> +</parameter> +<parameter name="uri" type="android.net.Uri"> +</parameter> +</method> +<method name="supportsUpdates" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="unregisterContentObserver" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="observer" type="android.database.ContentObserver"> +</parameter> +</method> +<method name="unregisterDataSetObserver" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="observer" type="android.database.DataSetObserver"> +</parameter> +</method> +<method name="updateBlob" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="value" type="byte[]"> +</parameter> +</method> +<method name="updateDouble" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="value" type="double"> +</parameter> +</method> +<method name="updateFloat" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="value" type="float"> +</parameter> +</method> +<method name="updateInt" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="value" type="int"> +</parameter> +</method> +<method name="updateLong" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="value" type="long"> +</parameter> +</method> +<method name="updateShort" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="value" type="short"> +</parameter> +</method> +<method name="updateString" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +<parameter name="value" type="java.lang.String"> +</parameter> +</method> +<method name="updateToNull" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="columnIndex" type="int"> +</parameter> +</method> +</class> <class name="MockDialogInterface" extends="java.lang.Object" abstract="false" @@ -144503,6 +145257,29 @@ </parameter> </method> </interface> +<interface name="LeadingMarginSpan.LeadingMarginSpan2" + abstract="true" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +> +<implements name="android.text.style.LeadingMarginSpan"> +</implements> +<implements name="android.text.style.WrapTogetherSpan"> +</implements> +<method name="getLeadingMarginLineCount" + return="int" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +</interface> <class name="LeadingMarginSpan.Standard" extends="java.lang.Object" abstract="false" @@ -189594,6 +190371,21 @@ <parameter name="parcel" type="android.os.Parcel"> </parameter> </constructor> +<method name="addView" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="viewId" type="int"> +</parameter> +<parameter name="nestedView" type="android.widget.RemoteViews"> +</parameter> +</method> <method name="apply" return="android.view.View" abstract="false" @@ -189670,6 +190462,19 @@ <parameter name="v" type="android.view.View"> </parameter> </method> +<method name="removeAllViews" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="viewId" type="int"> +</parameter> +</method> <method name="setBitmap" return="void" abstract="false" @@ -196255,6 +197060,17 @@ <parameter name="attrs" type="android.util.AttributeSet"> </parameter> </constructor> +<method name="isAutoStart" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> <method name="isFlipping" return="boolean" abstract="false" @@ -196266,6 +197082,19 @@ visibility="public" > </method> +<method name="setAutoStart" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="autoStart" type="boolean"> +</parameter> +</method> <method name="setFlipInterval" return="void" abstract="false" diff --git a/cmds/stagefright/SineSource.cpp b/cmds/stagefright/SineSource.cpp index e5a6ccb..e272a65 100644 --- a/cmds/stagefright/SineSource.cpp +++ b/cmds/stagefright/SineSource.cpp @@ -86,8 +86,8 @@ status_t SineSource::read( x += k; } - buffer->meta_data()->setInt32(kKeyTimeUnits, mPhase); - buffer->meta_data()->setInt32(kKeyTimeScale, mSampleRate); + buffer->meta_data()->setInt64( + kKeyTime, ((int64_t)mPhase * 1000000) / mSampleRate); mPhase += numFramesPerBuffer; diff --git a/cmds/stagefright/record.cpp b/cmds/stagefright/record.cpp index 323d448..0025c5e 100644 --- a/cmds/stagefright/record.cpp +++ b/cmds/stagefright/record.cpp @@ -23,7 +23,7 @@ #include <media/stagefright/MediaDebug.h> #include <media/stagefright/MediaDefs.h> #include <media/stagefright/MetaData.h> -#include <media/stagefright/MPEG4Extractor.h> +#include <media/stagefright/MediaExtractor.h> #include <media/stagefright/MPEG4Writer.h> #include <media/stagefright/MmapSource.h> #include <media/stagefright/OMXClient.h> @@ -32,8 +32,10 @@ using namespace android; -#if 0 +#if 1 class DummySource : public MediaSource { + static const int32_t kFramerate = 24; // fps + public: DummySource(int width, int height) : mWidth(width), @@ -52,6 +54,7 @@ public: } virtual status_t start(MetaData *params) { + mNumFramesOutput = 0; return OK; } @@ -61,6 +64,12 @@ public: virtual status_t read( MediaBuffer **buffer, const MediaSource::ReadOptions *options) { + if (mNumFramesOutput == kFramerate * 10) { + // Stop returning data after 10 secs. + return ERROR_END_OF_STREAM; + } + + // printf("DummySource::read\n"); status_t err = mGroup.acquire_buffer(buffer); if (err != OK) { return err; @@ -69,7 +78,13 @@ public: char x = (char)((double)rand() / RAND_MAX * 255); memset((*buffer)->data(), x, mSize); (*buffer)->set_range(0, mSize); + (*buffer)->meta_data()->clear(); + (*buffer)->meta_data()->setInt64( + kKeyTime, (mNumFramesOutput * 1000000) / kFramerate); + ++mNumFramesOutput; + // printf("DummySource::read - returning buffer\n"); + // LOGI("DummySource::read - returning buffer"); return OK; } @@ -80,6 +95,7 @@ private: MediaBufferGroup mGroup; int mWidth, mHeight; size_t mSize; + int64_t mNumFramesOutput;; DummySource(const DummySource &); DummySource &operator=(const DummySource &); @@ -88,8 +104,8 @@ private: sp<MediaSource> createSource(const char *filename) { sp<MediaSource> source; - sp<MPEG4Extractor> extractor = - new MPEG4Extractor(new MmapSource(filename)); + sp<MediaExtractor> extractor = + MediaExtractor::Create(new MmapSource(filename)); size_t num_tracks = extractor->countTracks(); @@ -144,8 +160,8 @@ int main(int argc, char **argv) { success = success && meta->findInt32(kKeyHeight, &height); CHECK(success); #else - int width = 320; - int height = 240; + int width = 800; + int height = 480; sp<MediaSource> decoder = new DummySource(width, height); #endif @@ -159,19 +175,26 @@ int main(int argc, char **argv) { OMXCodec::Create( client.interface(), enc_meta, true /* createEncoder */, decoder); -#if 0 +#if 1 sp<MPEG4Writer> writer = new MPEG4Writer("/sdcard/output.mp4"); - writer->addSource(enc_meta, encoder); + writer->addSource(encoder); writer->start(); - sleep(20); - printf("stopping now.\n"); + while (!writer->reachedEOS()) { + usleep(100000); + } writer->stop(); #else encoder->start(); MediaBuffer *buffer; while (encoder->read(&buffer) == OK) { - printf("got an output frame of size %d\n", buffer->range_length()); + int32_t isSync; + if (!buffer->meta_data()->findInt32(kKeyIsSyncFrame, &isSync)) { + isSync = false; + } + + printf("got an output frame of size %d%s\n", buffer->range_length(), + isSync ? " (SYNC)" : ""); buffer->release(); buffer = NULL; diff --git a/cmds/stagefright/stagefright.cpp b/cmds/stagefright/stagefright.cpp index 4ffc8e4..d26e558 100644 --- a/cmds/stagefright/stagefright.cpp +++ b/cmds/stagefright/stagefright.cpp @@ -52,21 +52,26 @@ static int64_t getNowUs() { static void playSource(OMXClient *client, const sp<MediaSource> &source) { sp<MetaData> meta = source->getFormat(); - int32_t durationUnits; - int32_t timeScale; - CHECK(meta->findInt32(kKeyDuration, &durationUnits)); - CHECK(meta->findInt32(kKeyTimeScale, &timeScale)); + int64_t durationUs; + CHECK(meta->findInt64(kKeyDuration, &durationUs)); - int64_t durationUs = ((int64_t)durationUnits * 1000000) / timeScale; + const char *mime; + CHECK(meta->findCString(kKeyMIMEType, &mime)); - sp<OMXCodec> decoder = OMXCodec::Create( + sp<MediaSource> rawSource; + if (!strcasecmp(MEDIA_MIMETYPE_AUDIO_RAW, mime)) { + rawSource = source; + } else { + rawSource = OMXCodec::Create( client->interface(), meta, false /* createEncoder */, source); - if (decoder == NULL) { - return; + if (rawSource == NULL) { + fprintf(stderr, "Failed to instantiate decoder for '%s'.\n", mime); + return; + } } - decoder->start(); + rawSource->start(); if (gReproduceBug >= 3 && gReproduceBug <= 5) { status_t err; @@ -74,24 +79,28 @@ static void playSource(OMXClient *client, const sp<MediaSource> &source) { MediaSource::ReadOptions options; int64_t seekTimeUs = -1; for (;;) { - err = decoder->read(&buffer, &options); + err = rawSource->read(&buffer, &options); options.clearSeekTo(); bool shouldSeek = false; - if (err != OK) { + if (err == INFO_FORMAT_CHANGED) { + CHECK_EQ(buffer, NULL); + + printf("format changed.\n"); + continue; + } else if (err != OK) { printf("reached EOF.\n"); shouldSeek = true; } else { - int32_t timestampUnits; - CHECK(buffer->meta_data()->findInt32(kKeyTimeUnits, ×tampUnits)); - - int64_t timestampUs = ((int64_t)timestampUnits * 1000000) / timeScale; + int64_t timestampUs; + CHECK(buffer->meta_data()->findInt64(kKeyTime, ×tampUs)); bool failed = false; if (seekTimeUs >= 0) { int64_t diff = timestampUs - seekTimeUs; + if (diff < 0) { diff = -diff; } @@ -134,7 +143,7 @@ static void playSource(OMXClient *client, const sp<MediaSource> &source) { } } - decoder->stop(); + rawSource->stop(); return; } @@ -151,12 +160,17 @@ static void playSource(OMXClient *client, const sp<MediaSource> &source) { MediaBuffer *buffer; for (;;) { - status_t err = decoder->read(&buffer, &options); + status_t err = rawSource->read(&buffer, &options); options.clearSeekTo(); if (err != OK) { CHECK_EQ(buffer, NULL); + if (err == INFO_FORMAT_CHANGED) { + printf("format changed.\n"); + continue; + } + break; } @@ -188,7 +202,7 @@ static void playSource(OMXClient *client, const sp<MediaSource> &source) { options.setSeekTo(0); } - decoder->stop(); + rawSource->stop(); printf("\n"); int64_t delay = getNowUs() - startTime; diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index 4f59c4e..2d36241 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -16,20 +16,27 @@ package android.accounts; +import android.Manifest; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.RegisteredServicesCache; -import android.content.pm.PackageInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.RegisteredServicesCacheListener; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -37,17 +44,13 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; -import android.os.Binder; import android.os.SystemProperties; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import android.app.PendingIntent; -import android.app.NotificationManager; -import android.app.Notification; -import android.Manifest; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -58,8 +61,9 @@ import java.util.HashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import com.android.internal.telephony.TelephonyIntents; import com.android.internal.R; +import com.android.internal.telephony.ITelephony; +import com.android.internal.telephony.TelephonyIntents; /** * A system service that provides account, password, and authtoken management for all @@ -90,11 +94,8 @@ public class AccountManagerService // Messages that can be sent on mHandler private static final int MESSAGE_TIMED_OUT = 3; - private static final int MESSAGE_CONNECTED = 7; - private static final int MESSAGE_DISCONNECTED = 8; private final AccountAuthenticatorCache mAuthenticatorCache; - private final AuthenticatorBindHelper mBindHelper; private final DatabaseHelper mOpenHelper; private final SimWatcher mSimWatcher; @@ -221,8 +222,6 @@ public class AccountManagerService mAuthenticatorCache = new AccountAuthenticatorCache(mContext); mAuthenticatorCache.setListener(this); - mBindHelper = new AuthenticatorBindHelper(mContext, mAuthenticatorCache, mMessageHandler, - MESSAGE_CONNECTED, MESSAGE_DISCONNECTED); mSimWatcher = new SimWatcher(mContext); sThis.set(this); @@ -1081,7 +1080,7 @@ public class AccountManagerService } private abstract class Session extends IAccountAuthenticatorResponse.Stub - implements AuthenticatorBindHelper.Callback, IBinder.DeathRecipient { + implements IBinder.DeathRecipient, ServiceConnection { IAccountManagerResponse mResponse; final String mAccountType; final boolean mExpectActivityLaunch; @@ -1163,7 +1162,7 @@ public class AccountManagerService if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "initiating bind to authenticator type " + mAccountType); } - if (!mBindHelper.bind(mAccountType, this)) { + if (!bindToAuthenticator(mAccountType)) { Log.d(TAG, "bind attempt failed for " + toDebugString()); onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure"); } @@ -1172,7 +1171,7 @@ public class AccountManagerService private void unbind() { if (mAuthenticator != null) { mAuthenticator = null; - mBindHelper.unbind(this); + mContext.unbindService(this); } } @@ -1185,7 +1184,7 @@ public class AccountManagerService mMessageHandler.removeMessages(MESSAGE_TIMED_OUT, this); } - public void onConnected(IBinder service) { + public void onServiceConnected(ComponentName name, IBinder service) { mAuthenticator = IAccountAuthenticator.Stub.asInterface(service); try { run(); @@ -1195,9 +1194,7 @@ public class AccountManagerService } } - public abstract void run() throws RemoteException; - - public void onDisconnected() { + public void onServiceDisconnected(ComponentName name) { mAuthenticator = null; IAccountManagerResponse response = getResponseAndClose(); if (response != null) { @@ -1206,6 +1203,8 @@ public class AccountManagerService } } + public abstract void run() throws RemoteException; + public void onTimedOut() { IAccountManagerResponse response = getResponseAndClose(); if (response != null) { @@ -1275,6 +1274,39 @@ public class AccountManagerService } } } + + /** + * find the component name for the authenticator and initiate a bind + * if no authenticator or the bind fails then return false, otherwise return true + */ + private boolean bindToAuthenticator(String authenticatorType) { + AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo = + mAuthenticatorCache.getServiceInfo( + AuthenticatorDescription.newKey(authenticatorType)); + if (authenticatorInfo == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there is no authenticator for " + authenticatorType + + ", bailing out"); + } + return false; + } + + Intent intent = new Intent(); + intent.setAction(AccountManager.ACTION_AUTHENTICATOR_INTENT); + intent.setComponent(authenticatorInfo.componentName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName); + } + if (!mContext.bindService(intent, this, Context.BIND_AUTO_CREATE)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed"); + } + return false; + } + + + return true; + } } private class MessageHandler extends Handler { @@ -1283,9 +1315,6 @@ public class AccountManagerService } public void handleMessage(Message msg) { - if (mBindHelper.handleMessage(msg)) { - return; - } switch (msg.what) { case MESSAGE_TIMED_OUT: Session session = (Session)msg.obj; @@ -1425,16 +1454,58 @@ public class AccountManagerService */ @Override public void onReceive(Context context, Intent intent) { - // Check IMSI on every update; nothing happens if the IMSI is missing or unchanged. - String imsi = ((TelephonyManager) context.getSystemService( - Context.TELEPHONY_SERVICE)).getSubscriberId(); + // Check IMSI on every update; nothing happens if the IMSI + // is missing or unchanged. + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager == null) { + Log.w(TAG, "failed to get TelephonyManager"); + return; + } + String imsi = telephonyManager.getSubscriberId(); + + // If the subscriber ID is an empty string, don't do anything. if (TextUtils.isEmpty(imsi)) return; + // If the current IMSI matches what's stored, don't do anything. String storedImsi = getMetaValue("imsi"); - if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "current IMSI=" + imsi + "; stored IMSI=" + storedImsi); } + if (imsi.equals(storedImsi)) return; + + // If a CDMA phone is unprovisioned, getSubscriberId() + // will return a different value, but we *don't* erase the + // passwords. We only erase them if it has a different + // subscriber ID once it's provisioned. + if (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { + IBinder service = ServiceManager.checkService(Context.TELEPHONY_SERVICE); + if (service == null) { + Log.w(TAG, "call to checkService(TELEPHONY_SERVICE) failed"); + return; + } + ITelephony telephony = ITelephony.Stub.asInterface(service); + if (telephony == null) { + Log.w(TAG, "failed to get ITelephony interface"); + return; + } + boolean needsProvisioning; + try { + needsProvisioning = telephony.getCdmaNeedsProvisioning(); + } catch (RemoteException e) { + Log.w(TAG, "exception while checking provisioning", e); + // default to NOT wiping out the passwords + needsProvisioning = true; + } + if (needsProvisioning) { + // if the phone needs re-provisioning, don't do anything. + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "current IMSI=" + imsi + " (needs provisioning); stored IMSI=" + + storedImsi); + } + return; + } + } if (!imsi.equals(storedImsi) && !TextUtils.isEmpty(storedImsi)) { Log.w(TAG, "wiping all passwords and authtokens because IMSI changed (" @@ -1578,6 +1649,7 @@ public class AccountManagerService } private boolean permissionIsGranted(Account account, String authTokenType, int callerUid) { + final boolean inSystemImage = inSystemImage(callerUid); final boolean fromAuthenticator = account != null && hasAuthenticatorUid(account.type, callerUid); final boolean hasExplicitGrants = account != null @@ -1588,7 +1660,7 @@ public class AccountManagerService + ": is authenticator? " + fromAuthenticator + ", has explicit permission? " + hasExplicitGrants); } - return fromAuthenticator || hasExplicitGrants || inSystemImage(callerUid); + return fromAuthenticator || hasExplicitGrants || inSystemImage; } private boolean hasAuthenticatorUid(String accountType, int callingUid) { diff --git a/core/java/android/accounts/AuthenticatorBindHelper.java b/core/java/android/accounts/AuthenticatorBindHelper.java deleted file mode 100644 index 2ca1f0e..0000000 --- a/core/java/android/accounts/AuthenticatorBindHelper.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Map; - -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; - -/** - * A helper object that simplifies binding to Account Authenticators. It uses the - * {@link AccountAuthenticatorCache} to find the component name of the authenticators, - * allowing the user to bind by account name. It also allows multiple, simultaneous binds - * to the same authenticator, with each bind call guaranteed to return either - * {@link Callback#onConnected} or {@link Callback#onDisconnected} if the bind() call - * itself succeeds, even if the authenticator is already bound internally. - * @hide - */ -public class AuthenticatorBindHelper { - private static final String TAG = "Accounts"; - private final Handler mHandler; - private final Context mContext; - private final int mMessageWhatConnected; - private final int mMessageWhatDisconnected; - private final Map<String, MyServiceConnection> mServiceConnections = Maps.newHashMap(); - private final Map<String, ArrayList<Callback>> mServiceUsers = Maps.newHashMap(); - private final AccountAuthenticatorCache mAuthenticatorCache; - - public AuthenticatorBindHelper(Context context, - AccountAuthenticatorCache authenticatorCache, Handler handler, - int messageWhatConnected, int messageWhatDisconnected) { - mContext = context; - mHandler = handler; - mAuthenticatorCache = authenticatorCache; - mMessageWhatConnected = messageWhatConnected; - mMessageWhatDisconnected = messageWhatDisconnected; - } - - public interface Callback { - void onConnected(IBinder service); - void onDisconnected(); - } - - public boolean bind(String authenticatorType, Callback callback) { - // if the authenticator is connecting or connected then return true - synchronized (mServiceConnections) { - if (mServiceConnections.containsKey(authenticatorType)) { - MyServiceConnection connection = mServiceConnections.get(authenticatorType); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "service connection already exists for " + authenticatorType); - } - mServiceUsers.get(authenticatorType).add(callback); - if (connection.mService != null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service is connected, scheduling a connected message for " - + authenticatorType); - } - connection.scheduleCallbackConnectedMessage(callback); - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service is *not* connected, waiting for for " - + authenticatorType); - } - } - return true; - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "there is no service connection for " + authenticatorType); - } - - // otherwise find the component name for the authenticator and initiate a bind - // if no authenticator or the bind fails then return false, otherwise return true - AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo = - mAuthenticatorCache.getServiceInfo( - AuthenticatorDescription.newKey(authenticatorType)); - if (authenticatorInfo == null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "there is no authenticator for " + authenticatorType - + ", bailing out"); - } - return false; - } - - MyServiceConnection connection = new MyServiceConnection(authenticatorType); - - Intent intent = new Intent(); - intent.setAction("android.accounts.AccountAuthenticator"); - intent.setComponent(authenticatorInfo.componentName); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName); - } - if (!mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed"); - } - return false; - } - - mServiceConnections.put(authenticatorType, connection); - mServiceUsers.put(authenticatorType, Lists.newArrayList(callback)); - return true; - } - } - - public void unbind(Callback callbackToUnbind) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "unbinding callback " + callbackToUnbind); - } - synchronized (mServiceConnections) { - for (Map.Entry<String, ArrayList<Callback>> entry : mServiceUsers.entrySet()) { - final String authenticatorType = entry.getKey(); - final ArrayList<Callback> serviceUsers = entry.getValue(); - for (Callback callback : serviceUsers) { - if (callback == callbackToUnbind) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "found callback in service" + authenticatorType); - } - serviceUsers.remove(callbackToUnbind); - if (serviceUsers.isEmpty()) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "there are no more callbacks for service " - + authenticatorType + ", unbinding service"); - } - unbindFromServiceLocked(authenticatorType); - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "leaving service " + authenticatorType - + " around since there are still callbacks using it"); - } - } - return; - } - } - } - Log.e(TAG, "did not find callback " + callbackToUnbind + " in any of the services"); - } - } - - /** - * You must synchronized on mServiceConnections before calling this - */ - private void unbindFromServiceLocked(String authenticatorType) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "unbindService from " + authenticatorType); - } - mContext.unbindService(mServiceConnections.get(authenticatorType)); - mServiceUsers.remove(authenticatorType); - mServiceConnections.remove(authenticatorType); - } - - private class ConnectedMessagePayload { - public final IBinder mService; - public final Callback mCallback; - public ConnectedMessagePayload(IBinder service, Callback callback) { - mService = service; - mCallback = callback; - } - } - - private class MyServiceConnection implements ServiceConnection { - private final String mAuthenticatorType; - private IBinder mService = null; - - public MyServiceConnection(String authenticatorType) { - mAuthenticatorType = authenticatorType; - } - - public void onServiceConnected(ComponentName name, IBinder service) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "onServiceConnected for account type " + mAuthenticatorType); - } - // post a message for each service user to tell them that the service is connected - synchronized (mServiceConnections) { - mService = service; - for (Callback callback : mServiceUsers.get(mAuthenticatorType)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service became connected, scheduling a connected " - + "message for " + mAuthenticatorType); - } - scheduleCallbackConnectedMessage(callback); - } - } - } - - private void scheduleCallbackConnectedMessage(Callback callback) { - final ConnectedMessagePayload payload = - new ConnectedMessagePayload(mService, callback); - mHandler.obtainMessage(mMessageWhatConnected, payload).sendToTarget(); - } - - public void onServiceDisconnected(ComponentName name) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "onServiceDisconnected for account type " + mAuthenticatorType); - } - // post a message for each service user to tell them that the service is disconnected, - // and unbind from the service. - synchronized (mServiceConnections) { - final ArrayList<Callback> callbackList = mServiceUsers.get(mAuthenticatorType); - if (callbackList != null) { - for (Callback callback : callbackList) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service became disconnected, scheduling a " - + "disconnected message for " - + mAuthenticatorType); - } - mHandler.obtainMessage(mMessageWhatDisconnected, callback).sendToTarget(); - } - unbindFromServiceLocked(mAuthenticatorType); - } - } - } - } - - boolean handleMessage(Message message) { - if (message.what == mMessageWhatConnected) { - ConnectedMessagePayload payload = (ConnectedMessagePayload)message.obj; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "notifying callback " + payload.mCallback + " that it is connected"); - } - payload.mCallback.onConnected(payload.mService); - return true; - } else if (message.what == mMessageWhatDisconnected) { - Callback callback = (Callback)message.obj; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "notifying callback " + callback + " that it is disconnected"); - } - callback.onDisconnected(); - return true; - } else { - return false; - } - } -} diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java index e3ed2e9..f4b7258 100644 --- a/core/java/android/accounts/GrantCredentialsPermissionActivity.java +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -18,14 +18,16 @@ package android.accounts; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; -import android.widget.ArrayAdapter; -import android.widget.ListView; +import android.widget.LinearLayout; +import android.widget.ImageView; import android.view.View; import android.view.LayoutInflater; -import android.view.ViewGroup; +import android.view.Window; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.text.TextUtils; +import android.graphics.drawable.Drawable; import com.android.internal.R; /** @@ -43,62 +45,68 @@ public class GrantCredentialsPermissionActivity extends Activity implements View private String mAuthTokenType; private int mUid; private Bundle mResultBundle = null; + protected LayoutInflater mInflater; protected void onCreate(Bundle savedInstanceState) { + requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); - getWindow().setContentView(R.layout.grant_credentials_permission); - mAccount = getIntent().getExtras().getParcelable(EXTRAS_ACCOUNT); - mAuthTokenType = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_TYPE); - mUid = getIntent().getExtras().getInt(EXTRAS_REQUESTING_UID); - final String accountTypeLabel = - getIntent().getExtras().getString(EXTRAS_ACCOUNT_TYPE_LABEL); - final String[] packages = getIntent().getExtras().getStringArray(EXTRAS_PACKAGES); + setContentView(R.layout.grant_credentials_permission); - findViewById(R.id.allow).setOnClickListener(this); - findViewById(R.id.deny).setOnClickListener(this); + mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - TextView messageView = (TextView) getWindow().findViewById(R.id.message); - String authTokenLabel = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_LABEL); - if (authTokenLabel.length() == 0) { - CharSequence grantCredentialsPermissionFormat = getResources().getText( - R.string.grant_credentials_permission_message_desc); - messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), - mAccount.name, accountTypeLabel)); - } else { - CharSequence grantCredentialsPermissionFormat = getResources().getText( - R.string.grant_credentials_permission_message_with_authtokenlabel_desc); - messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), - authTokenLabel, mAccount.name, accountTypeLabel)); - } + final Bundle extras = getIntent().getExtras(); + mAccount = extras.getParcelable(EXTRAS_ACCOUNT); + mAuthTokenType = extras.getString(EXTRAS_AUTH_TOKEN_TYPE); + mUid = extras.getInt(EXTRAS_REQUESTING_UID); + final String accountTypeLabel = extras.getString(EXTRAS_ACCOUNT_TYPE_LABEL); + final String[] packages = extras.getStringArray(EXTRAS_PACKAGES); + final String authTokenLabel = extras.getString(EXTRAS_AUTH_TOKEN_LABEL); + + findViewById(R.id.allow_button).setOnClickListener(this); + findViewById(R.id.deny_button).setOnClickListener(this); + + LinearLayout packagesListView = (LinearLayout) findViewById(R.id.packages_list); - String[] packageLabels = new String[packages.length]; final PackageManager pm = getPackageManager(); - for (int i = 0; i < packages.length; i++) { + for (String pkg : packages) { + String packageLabel; try { - packageLabels[i] = - pm.getApplicationLabel(pm.getApplicationInfo(packages[i], 0)).toString(); + packageLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString(); } catch (PackageManager.NameNotFoundException e) { - packageLabels[i] = packages[i]; + packageLabel = pkg; } + packagesListView.addView(newPackageView(packageLabel)); + } + + ((TextView) findViewById(R.id.account_name)).setText(mAccount.name); + ((TextView) findViewById(R.id.account_type)).setText(accountTypeLabel); + TextView authTokenTypeView = (TextView) findViewById(R.id.authtoken_type); + if (TextUtils.isEmpty(authTokenLabel)) { + authTokenTypeView.setVisibility(View.GONE); + } else { + authTokenTypeView.setText(authTokenLabel); } - ((ListView) findViewById(R.id.packages_list)).setAdapter( - new PackagesArrayAdapter(this, packageLabels)); + } + + private View newPackageView(String packageLabel) { + View view = mInflater.inflate(R.layout.permissions_package_list_item, null); + ((TextView) view.findViewById(R.id.package_label)).setText(packageLabel); + return view; } public void onClick(View v) { + final AccountManagerService accountManagerService = AccountManagerService.getSingleton(); switch (v.getId()) { - case R.id.allow: - AccountManagerService.getSingleton().grantAppPermission(mAccount, mAuthTokenType, - mUid); + case R.id.allow_button: + accountManagerService.grantAppPermission(mAccount, mAuthTokenType, mUid); Intent result = new Intent(); result.putExtra("retry", true); setResult(RESULT_OK, result); setAccountAuthenticatorResult(result.getExtras()); break; - case R.id.deny: - AccountManagerService.getSingleton().revokeAppPermission(mAccount, mAuthTokenType, - mUid); + case R.id.deny_button: + accountManagerService.revokeAppPermission(mAccount, mAuthTokenType, mUid); setResult(RESULT_CANCELED); break; } @@ -110,63 +118,20 @@ public class GrantCredentialsPermissionActivity extends Activity implements View } /** - * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + * Sends the result or a {@link AccountManager#ERROR_CODE_CANCELED} error if a + * result isn't present. */ public void finish() { Intent intent = getIntent(); - AccountAuthenticatorResponse accountAuthenticatorResponse = - intent.getParcelableExtra(EXTRAS_RESPONSE); - if (accountAuthenticatorResponse != null) { + AccountAuthenticatorResponse response = intent.getParcelableExtra(EXTRAS_RESPONSE); + if (response != null) { // send the result bundle back if set, otherwise send an error. if (mResultBundle != null) { - accountAuthenticatorResponse.onResult(mResultBundle); + response.onResult(mResultBundle); } else { - accountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); + response.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); } } super.finish(); } - - private static class PackagesArrayAdapter extends ArrayAdapter<String> { - protected LayoutInflater mInflater; - private static final int mResource = R.layout.simple_list_item_1; - - public PackagesArrayAdapter(Context context, String[] items) { - super(context, mResource, items); - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - static class ViewHolder { - TextView label; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // A ViewHolder keeps references to children views to avoid unneccessary calls - // to findViewById() on each row. - ViewHolder holder; - - // When convertView is not null, we can reuse it directly, there is no need - // to reinflate it. We only inflate a new View when the convertView supplied - // by ListView is null. - if (convertView == null) { - convertView = mInflater.inflate(mResource, null); - - // Creates a ViewHolder and store references to the two children views - // we want to bind data to. - holder = new ViewHolder(); - holder.label = (TextView) convertView.findViewById(R.id.text1); - - convertView.setTag(holder); - } else { - // Get the ViewHolder back to get fast access to the TextView - // and the ImageView. - holder = (ViewHolder) convertView.getTag(); - } - - holder.label.setText(getItem(position)); - - return convertView; - } - } } diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index f48f150..305ee6a 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -70,6 +70,7 @@ import android.net.wifi.IWifiManager; import android.net.wifi.WifiManager; import android.os.Binder; import android.os.Bundle; +import android.os.DropBox; import android.os.FileUtils; import android.os.Handler; import android.os.IBinder; @@ -93,6 +94,8 @@ import android.view.inputmethod.InputMethodManager; import android.accounts.AccountManager; import android.accounts.IAccountManager; +import com.android.internal.os.IDropBoxService; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -182,6 +185,7 @@ class ApplicationContext extends Context { private ClipboardManager mClipboardManager = null; private boolean mRestricted; private AccountManager mAccountManager; // protected by mSync + private DropBox mDropBox = null; private final Object mSync = new Object(); @@ -896,6 +900,8 @@ class ApplicationContext extends Context { return getClipboardManager(); } else if (WALLPAPER_SERVICE.equals(name)) { return getWallpaperManager(); + } else if (DROPBOX_SERVICE.equals(name)) { + return getDropBox(); } return null; @@ -1045,7 +1051,7 @@ class ApplicationContext extends Context { } return mVibrator; } - + private AudioManager getAudioManager() { if (mAudioManager == null) { @@ -1054,6 +1060,17 @@ class ApplicationContext extends Context { return mAudioManager; } + private DropBox getDropBox() { + synchronized (mSync) { + if (mDropBox == null) { + IBinder b = ServiceManager.getService(DROPBOX_SERVICE); + IDropBoxService service = IDropBoxService.Stub.asInterface(b); + mDropBox = new DropBox(service); + } + } + return mDropBox; + } + @Override public int checkPermission(String permission, int pid, int uid) { if (permission == null) { diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 4684f45..45c33ef 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -46,7 +46,7 @@ import java.util.UUID; */ public final class BluetoothAdapter { private static final String TAG = "BluetoothAdapter"; - private static final boolean DBG = false; + private static final boolean DBG = true; //STOPSHIP: Remove excess logging /** * Sentinel error value for this class. Guaranteed to not equal any other diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java index eba8715..9866862 100644 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -747,4 +747,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro public void writeSyncDataBytes(Account account, byte[] data) { mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); } + + protected ContentProvider getSyncStateProvider() { + return mSyncState.asContentProvider(); + } } diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java index fb6091a..0db6155 100644 --- a/core/java/android/content/AbstractThreadedSyncAdapter.java +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.os.Process; import android.os.NetStat; import android.os.IBinder; +import android.os.RemoteException; import android.util.EventLog; import java.util.concurrent.atomic.AtomicInteger; @@ -117,6 +118,12 @@ public abstract class AbstractThreadedSyncAdapter { } } } + + public void initialize(Account account, String authority) throws RemoteException { + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); + startSync(null, authority, account, extras); + } } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 8f1c671..b4ab408 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1309,7 +1309,7 @@ public abstract class Context { * @see #getSystemService */ public static final String APPWIDGET_SERVICE = "appwidget"; - + /** * Use with {@link #getSystemService} to retrieve an * {@blink android.backup.IBackupManager IBackupManager} for communicating @@ -1319,7 +1319,16 @@ public abstract class Context { * @see #getSystemService */ public static final String BACKUP_SERVICE = "backup"; - + + /** + * Use with {@link #getSystemService} to retrieve a + * {@blink android.os.DropBox DropBox} instance for recording + * diagnostic logs. + * @hide + * @see #getSystemService + */ + public static final String DROPBOX_SERVICE = "dropbox"; + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. diff --git a/core/java/android/content/ISyncAdapter.aidl b/core/java/android/content/ISyncAdapter.aidl index 4660527..dd9d14e 100644 --- a/core/java/android/content/ISyncAdapter.aidl +++ b/core/java/android/content/ISyncAdapter.aidl @@ -44,4 +44,12 @@ oneway interface ISyncAdapter { * @param syncContext the ISyncContext that was passed to {@link #startSync} */ void cancelSync(ISyncContext syncContext); + + /** + * Initialize the SyncAdapter for this account and authority. + * + * @param account the account that should be synced + * @param authority the authority that should be synced + */ + void initialize(in Account account, String authority); } diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java index 88dc332..af1634e 100644 --- a/core/java/android/content/SyncAdapter.java +++ b/core/java/android/content/SyncAdapter.java @@ -38,6 +38,12 @@ public abstract class SyncAdapter { public void cancelSync(ISyncContext syncContext) throws RemoteException { SyncAdapter.this.cancelSync(); } + + public void initialize(Account account, String authority) throws RemoteException { + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); + startSync(null, authority, account, extras); + } } Transport mTransport = new Transport(); diff --git a/core/java/android/content/SyncContext.java b/core/java/android/content/SyncContext.java index 587586d..cc914c0 100644 --- a/core/java/android/content/SyncContext.java +++ b/core/java/android/content/SyncContext.java @@ -56,7 +56,9 @@ public class SyncContext { if (now < mLastHeartbeatSendTime + HEARTBEAT_SEND_INTERVAL_IN_MS) return; try { mLastHeartbeatSendTime = now; - mSyncContext.sendHeartbeat(); + if (mSyncContext != null) { + mSyncContext.sendHeartbeat(); + } } catch (RemoteException e) { // this should never happen } @@ -64,13 +66,15 @@ public class SyncContext { public void onFinished(SyncResult result) { try { - mSyncContext.onFinished(result); + if (mSyncContext != null) { + mSyncContext.onFinished(result); + } } catch (RemoteException e) { // this should never happen } } public IBinder getSyncContextBinder() { - return mSyncContext.asBinder(); + return (mSyncContext == null) ? null : mSyncContext.asBinder(); } } diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index ba18615..1580c66 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -544,6 +544,46 @@ class SyncManager implements OnAccountsUpdateListener { return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; } + private void initializeSyncAdapter(Account account, String authority) { + SyncAdapterType syncAdapterType = SyncAdapterType.newKey(authority, account.type); + RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo = + mSyncAdapters.getServiceInfo(syncAdapterType); + if (syncAdapterInfo == null) { + Log.w(TAG, "can't find a sync adapter for " + syncAdapterType); + return; + } + + Intent intent = new Intent(); + intent.setAction("android.content.SyncAdapter"); + intent.setComponent(syncAdapterInfo.componentName); + mContext.bindService(intent, new InitializerServiceConnection(account, authority), + Context.BIND_AUTO_CREATE); + } + + private class InitializerServiceConnection implements ServiceConnection { + private final Account mAccount; + private final String mAuthority; + + public InitializerServiceConnection(Account account, String authority) { + mAccount = account; + mAuthority = authority; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + try { + ISyncAdapter.Stub.asInterface(service).initialize(mAccount, mAuthority); + } catch (RemoteException e) { + // doesn't matter, we will retry again later + } finally { + mContext.unbindService(this); + } + } + + public void onServiceDisconnected(ComponentName name) { + mContext.unbindService(this); + } + } + /** * Returns whether or not sync is enabled. Sync can be enabled by * setting the system property "ro.config.sync" to the value "yes". @@ -686,36 +726,34 @@ class SyncManager implements OnAccountsUpdateListener { continue; } - // make this an initialization sync if the isSyncable state is unknown - Bundle extrasCopy = extras; - long delayCopy = delay; + // initialize the SyncAdapter if the isSyncable state is unknown if (isSyncable < 0) { - extrasCopy = new Bundle(extras); - extrasCopy.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); - delayCopy = -1; // expedite this - } else { - final boolean syncAutomatically = masterSyncAutomatically - && mSyncStorageEngine.getSyncAutomatically(account, authority); - boolean syncAllowed = - manualSync || (backgroundDataUsageAllowed && syncAutomatically); - if (!syncAllowed) { - if (isLoggable) { - Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority - + " is not allowed, dropping request"); - } - continue; + initializeSyncAdapter(account, authority); + continue; + } + + final boolean syncAutomatically = masterSyncAutomatically + && mSyncStorageEngine.getSyncAutomatically(account, authority); + boolean syncAllowed = + manualSync || (backgroundDataUsageAllowed && syncAutomatically); + if (!syncAllowed) { + if (isLoggable) { + Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority + + " is not allowed, dropping request"); } + continue; } + if (isLoggable) { Log.v(TAG, "scheduleSync:" - + " delay " + delayCopy + + " delay " + delay + ", source " + source + ", account " + account + ", authority " + authority - + ", extras " + extrasCopy); + + ", extras " + extras); } scheduleSyncOperation( - new SyncOperation(account, source, authority, extrasCopy, delayCopy)); + new SyncOperation(account, source, authority, extras, delay)); } } } diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java index 2d39e39..b8e17da 100644 --- a/core/java/android/net/http/Connection.java +++ b/core/java/android/net/http/Connection.java @@ -94,7 +94,6 @@ abstract class Connection { */ private static final String HTTP_CONNECTION = "http.connection"; - RequestQueue.ConnectionManager mConnectionManager; RequestFeeder mRequestFeeder; /** @@ -104,11 +103,9 @@ abstract class Connection { private byte[] mBuf; protected Connection(Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, RequestFeeder requestFeeder) { mContext = context; mHost = host; - mConnectionManager = connectionManager; mRequestFeeder = requestFeeder; mCanPersist = false; @@ -124,18 +121,15 @@ abstract class Connection { * necessary */ static Connection getConnection( - Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, + Context context, HttpHost host, HttpHost proxy, RequestFeeder requestFeeder) { if (host.getSchemeName().equals("http")) { - return new HttpConnection(context, host, connectionManager, - requestFeeder); + return new HttpConnection(context, host, requestFeeder); } // Otherwise, default to https - return new HttpsConnection(context, host, connectionManager, - requestFeeder); + return new HttpsConnection(context, host, proxy, requestFeeder); } /** @@ -338,7 +332,7 @@ abstract class Connection { mRequestFeeder.requeueRequest(tReq); empty = false; } - if (empty) empty = mRequestFeeder.haveRequest(mHost); + if (empty) empty = !mRequestFeeder.haveRequest(mHost); } return empty; } diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java index 0b30e58..32191d2 100644 --- a/core/java/android/net/http/ConnectionThread.java +++ b/core/java/android/net/http/ConnectionThread.java @@ -108,24 +108,11 @@ class ConnectionThread extends Thread { if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " + request.mHost + " " + request ); - HttpHost proxy = mConnectionManager.getProxyHost(); - - HttpHost host; - if (false) { - // Allow https proxy - host = proxy == null ? request.mHost : proxy; - } else { - // Disallow https proxy -- tmob proxy server - // serves a request loop for https reqs - host = (proxy == null || - request.mHost.getSchemeName().equals("https")) ? - request.mHost : proxy; - } - mConnection = mConnectionManager.getConnection(mContext, host); + mConnection = mConnectionManager.getConnection(mContext, + request.mHost); mConnection.processRequests(request); if (mConnection.getCanPersist()) { - if (!mConnectionManager.recycleConnection(host, - mConnection)) { + if (!mConnectionManager.recycleConnection(mConnection)) { mConnection.closeConnection(); } } else { diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java index 8b12d0b..6df86bf 100644 --- a/core/java/android/net/http/HttpConnection.java +++ b/core/java/android/net/http/HttpConnection.java @@ -35,9 +35,8 @@ import org.apache.http.params.HttpConnectionParams; class HttpConnection extends Connection { HttpConnection(Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, RequestFeeder requestFeeder) { - super(context, host, connectionManager, requestFeeder); + super(context, host, requestFeeder); } /** diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java index 8a69d0d..f735f3d 100644 --- a/core/java/android/net/http/HttpsConnection.java +++ b/core/java/android/net/http/HttpsConnection.java @@ -131,13 +131,16 @@ public class HttpsConnection extends Connection { */ private boolean mAborted = false; + // Used when connecting through a proxy. + private HttpHost mProxyHost; + /** * Contructor for a https connection. */ - HttpsConnection(Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, + HttpsConnection(Context context, HttpHost host, HttpHost proxy, RequestFeeder requestFeeder) { - super(context, host, connectionManager, requestFeeder); + super(context, host, requestFeeder); + mProxyHost = proxy; } /** @@ -159,8 +162,7 @@ public class HttpsConnection extends Connection { AndroidHttpClientConnection openConnection(Request req) throws IOException { SSLSocket sslSock = null; - HttpHost proxyHost = mConnectionManager.getProxyHost(); - if (proxyHost != null) { + if (mProxyHost != null) { // If we have a proxy set, we first send a CONNECT request // to the proxy; if the proxy returns 200 OK, we negotiate // a secure connection to the target server via the proxy. @@ -172,7 +174,7 @@ public class HttpsConnection extends Connection { Socket proxySock = null; try { proxySock = new Socket - (proxyHost.getHostName(), proxyHost.getPort()); + (mProxyHost.getHostName(), mProxyHost.getPort()); proxySock.setSoTimeout(60 * 1000); diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java index 190ae7a..77cd544 100644 --- a/core/java/android/net/http/RequestHandle.java +++ b/core/java/android/net/http/RequestHandle.java @@ -42,15 +42,13 @@ public class RequestHandle { private WebAddress mUri; private String mMethod; private Map<String, String> mHeaders; - private RequestQueue mRequestQueue; - private Request mRequest; - private InputStream mBodyProvider; private int mBodyLength; - private int mRedirectCount = 0; + // Used only with synchronous requests. + private Connection mConnection; private final static String AUTHORIZATION_HEADER = "Authorization"; private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; @@ -81,6 +79,19 @@ public class RequestHandle { } /** + * Creates a new request session with a given Connection. This connection + * is used during a synchronous load to handle this request. + */ + public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, + String method, Map<String, String> headers, + InputStream bodyProvider, int bodyLength, Request request, + Connection conn) { + this(requestQueue, url, uri, method, headers, bodyProvider, bodyLength, + request); + mConnection = conn; + } + + /** * Cancels this request */ public void cancel() { @@ -262,6 +273,12 @@ public class RequestHandle { mRequest.waitUntilComplete(); } + public void processRequest() { + if (mConnection != null) { + mConnection.processRequests(mRequest); + } + } + /** * @return Digest-scheme authentication response. */ diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java index 875caa0..84b6487 100644 --- a/core/java/android/net/http/RequestQueue.java +++ b/core/java/android/net/http/RequestQueue.java @@ -171,16 +171,17 @@ public class RequestQueue implements RequestFeeder { } public Connection getConnection(Context context, HttpHost host) { + host = RequestQueue.this.determineHost(host); Connection con = mIdleCache.getConnection(host); if (con == null) { mTotalConnection++; - con = Connection.getConnection( - mContext, host, this, RequestQueue.this); + con = Connection.getConnection(mContext, host, mProxyHost, + RequestQueue.this); } return con; } - public boolean recycleConnection(HttpHost host, Connection connection) { - return mIdleCache.cacheConnection(host, connection); + public boolean recycleConnection(Connection connection) { + return mIdleCache.cacheConnection(connection.getHost(), connection); } } @@ -342,6 +343,66 @@ public class RequestQueue implements RequestFeeder { req); } + private static class SyncFeeder implements RequestFeeder { + // This is used in the case where the request fails and needs to be + // requeued into the RequestFeeder. + private Request mRequest; + SyncFeeder() { + } + public Request getRequest() { + Request r = mRequest; + mRequest = null; + return r; + } + public Request getRequest(HttpHost host) { + return getRequest(); + } + public boolean haveRequest(HttpHost host) { + return mRequest != null; + } + public void requeueRequest(Request r) { + mRequest = r; + } + } + + public RequestHandle queueSynchronousRequest(String url, WebAddress uri, + String method, Map<String, String> headers, + EventHandler eventHandler, InputStream bodyProvider, + int bodyLength) { + if (HttpLog.LOGV) { + HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri); + } + + HttpHost host = new HttpHost(uri.mHost, uri.mPort, uri.mScheme); + + Request req = new Request(method, host, mProxyHost, uri.mPath, + bodyProvider, bodyLength, eventHandler, headers); + + // Open a new connection that uses our special RequestFeeder + // implementation. + host = determineHost(host); + Connection conn = Connection.getConnection(mContext, host, mProxyHost, + new SyncFeeder()); + + // TODO: I would like to process the request here but LoadListener + // needs a RequestHandle to process some messages. + return new RequestHandle(this, url, uri, method, headers, bodyProvider, + bodyLength, req, conn); + + } + + // Chooses between the proxy and the request's host. + private HttpHost determineHost(HttpHost host) { + // There used to be a comment in ConnectionThread about t-mob's proxy + // being really bad about https. But, HttpsConnection actually looks + // for a proxy and connects through it anyway. I think that this check + // is still valid because if a site is https, we will use + // HttpsConnection rather than HttpConnection if the proxy address is + // not secure. + return (mProxyHost == null || "https".equals(host.getSchemeName())) + ? host : mProxyHost; + } + /** * @return true iff there are any non-active requests pending */ @@ -478,6 +539,6 @@ public class RequestQueue implements RequestFeeder { interface ConnectionManager { HttpHost getProxyHost(); Connection getConnection(Context context, HttpHost host); - boolean recycleConnection(HttpHost host, Connection connection); + boolean recycleConnection(Connection connection); } } diff --git a/core/java/android/os/DropBox.aidl b/core/java/android/os/DropBox.aidl new file mode 100644 index 0000000..77abd22 --- /dev/null +++ b/core/java/android/os/DropBox.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +parcelable DropBox.Entry; diff --git a/core/java/android/os/DropBox.java b/core/java/android/os/DropBox.java new file mode 100644 index 0000000..0551dc1 --- /dev/null +++ b/core/java/android/os/DropBox.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.util.Log; + +import com.android.internal.os.IDropBoxService; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +/** + * Enqueues chunks of data (from various sources -- application crashes, kernel + * log records, etc.). The queue is size bounded and will drop old data if the + * enqueued data exceeds the maximum size. You can think of this as a + * persistent, system-wide, blob-oriented "logcat". + * + * <p>You can obtain an instance of this class by calling + * {@link android.content.Context#getSystemService} + * with {@link android.content.Context#DROPBOX_SERVICE}. + * + * <p>DropBox entries are not sent anywhere directly, but other system services + * and debugging tools may scan and upload entries for processing. + * + * {@pending} + */ +public class DropBox { + private static final String TAG = "DropBox"; + private final IDropBoxService mService; + + /** Flag value: Entry's content was deleted to save space. */ + public static final int IS_EMPTY = 1; + + /** Flag value: Content is human-readable UTF-8 text (can be combined with IS_GZIPPED). */ + public static final int IS_TEXT = 2; + + /** Flag value: Content can be decompressed with {@link GZIPOutputStream}. */ + public static final int IS_GZIPPED = 4; + + /** + * A single entry retrieved from the drop box. + * This may include a reference to a stream, so you must call + * {@link #close()} when you are done using it. + */ + public static class Entry implements Parcelable { + private final String mTag; + private final long mTimeMillis; + + private final byte[] mData; + private final ParcelFileDescriptor mFileDescriptor; + private final int mFlags; + + /** Create a new empty Entry with no contents. */ + public Entry(String tag, long millis) { + this(tag, millis, (Object) null, IS_EMPTY); + } + + /** Create a new Entry with plain text contents. */ + public Entry(String tag, long millis, String text) { + this(tag, millis, (Object) text.getBytes(), IS_TEXT); + } + + /** + * Create a new Entry with byte array contents. + * The data array must not be modified after creating this entry. + */ + public Entry(String tag, long millis, byte[] data, int flags) { + this(tag, millis, (Object) data, flags); + } + + /** + * Create a new Entry with streaming data contents. + * Takes ownership of the ParcelFileDescriptor. + */ + public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) { + this(tag, millis, (Object) data, flags); + } + + /** + * Create a new Entry with the contents read from a file. + * The file will be read when the entry's contents are requested. + */ + public Entry(String tag, long millis, File data, int flags) throws IOException { + this(tag, millis, (Object) ParcelFileDescriptor.open( + data, ParcelFileDescriptor.MODE_READ_ONLY), flags); + } + + /** Internal constructor for CREATOR.createFromParcel(). */ + private Entry(String tag, long millis, Object value, int flags) { + if (tag == null) throw new NullPointerException(); + if (((flags & IS_EMPTY) != 0) != (value == null)) throw new IllegalArgumentException(); + + mTag = tag; + mTimeMillis = millis; + mFlags = flags; + + if (value == null) { + mData = null; + mFileDescriptor = null; + } else if (value instanceof byte[]) { + mData = (byte[]) value; + mFileDescriptor = null; + } else if (value instanceof ParcelFileDescriptor) { + mData = null; + mFileDescriptor = (ParcelFileDescriptor) value; + } else { + throw new IllegalArgumentException(); + } + } + + /** Close the input stream associated with this entry. */ + public void close() { + try { if (mFileDescriptor != null) mFileDescriptor.close(); } catch (IOException e) { } + } + + /** @return the tag originally attached to the entry. */ + public String getTag() { return mTag; } + + /** @return time when the entry was originally created. */ + public long getTimeMillis() { return mTimeMillis; } + + /** @return flags describing the content returned by @{link #getInputStream()}. */ + public int getFlags() { return mFlags & ~IS_GZIPPED; } // getInputStream() decompresses. + + /** + * @param maxBytes of string to return (will truncate at this length). + * @return the uncompressed text contents of the entry, null if the entry is not text. + */ + public String getText(int maxBytes) { + if ((mFlags & IS_TEXT) == 0) return null; + if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length)); + + InputStream is = null; + try { + is = getInputStream(); + byte[] buf = new byte[maxBytes]; + return new String(buf, 0, Math.max(0, is.read(buf))); + } catch (IOException e) { + return null; + } finally { + try { if (is != null) is.close(); } catch (IOException e) {} + } + } + + /** @return the uncompressed contents of the entry, or null if the contents were lost */ + public InputStream getInputStream() throws IOException { + InputStream is; + if (mData != null) { + is = new ByteArrayInputStream(mData); + } else if (mFileDescriptor != null) { + is = new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor); + } else { + return null; + } + return (mFlags & IS_GZIPPED) != 0 ? new GZIPInputStream(is) : is; + } + + public static final Parcelable.Creator<Entry> CREATOR = new Parcelable.Creator() { + public Entry[] newArray(int size) { return new Entry[size]; } + public Entry createFromParcel(Parcel in) { + return new Entry( + in.readString(), in.readLong(), in.readValue(null), in.readInt()); + } + }; + + public int describeContents() { + return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(mTag); + out.writeLong(mTimeMillis); + if (mFileDescriptor != null) { + out.writeValue(mFileDescriptor); + } else { + out.writeValue(mData); + } + out.writeInt(mFlags); + } + } + + /** {@hide} */ + public DropBox(IDropBoxService service) { mService = service; } + + /** + * Create a dummy instance for testing. All methods will fail unless + * overridden with an appropriate mock implementation. To obtain a + * functional instance, use {@link android.content.Context#getSystemService}. + */ + protected DropBox() { mService = null; } + + /** + * Stores human-readable text. The data may be discarded eventually (or even + * immediately) if space is limited, or ignored entirely if the tag has been + * blocked (see {@link #isTagEnabled}). + * + * @param tag describing the type of entry being stored + * @param data value to store + */ + public void addText(String tag, String data) { + try { mService.add(new Entry(tag, 0, data)); } catch (RemoteException e) {} + } + + /** + * Stores binary data, which may be ignored or discarded as with {@link #addText}. + * + * @param tag describing the type of entry being stored + * @param data value to store + * @param flags describing the data + */ + public void addData(String tag, byte[] data, int flags) { + if (data == null) throw new NullPointerException(); + try { mService.add(new Entry(tag, 0, data, flags)); } catch (RemoteException e) {} + } + + /** + * Stores data read from a file descriptor. The data may be ignored or + * discarded as with {@link #addText}. You must close your + * ParcelFileDescriptor object after calling this method! + * + * @param tag describing the type of entry being stored + * @param fd file descriptor to read from + * @param flags describing the data + */ + public void addFile(String tag, ParcelFileDescriptor fd, int flags) { + if (fd == null) throw new NullPointerException(); + try { mService.add(new Entry(tag, 0, fd, flags)); } catch (RemoteException e) {} + } + + /** + * Checks any blacklists (set in system settings) to see whether a certain + * tag is allowed. Entries with disabled tags will be dropped immediately, + * so you can save the work of actually constructing and sending the data. + * + * @param tag that would be used in {@link #addText} or {@link #addFile} + * @return whether events with that tag would be accepted + */ + public boolean isTagEnabled(String tag) { + try { return mService.isTagEnabled(tag); } catch (RemoteException e) { return false; } + } + + /** + * Gets the next entry from the drop box *after* the specified time. + * Requires android.permission.READ_LOGS. You must always call + * {@link Entry#close()} on the return value! + * + * @param tag of entry to look for, null for all tags + * @param msec time of the last entry seen + * @return the next entry, or null if there are no more entries + */ + public Entry getNextEntry(String tag, long msec) { + try { return mService.getNextEntry(tag, msec); } catch (RemoteException e) { return null; } + } + + // TODO: It may be useful to have some sort of notification mechanism + // when data is added to the dropbox, for demand-driven readers -- + // for now readers need to poll the dropbox to find new data. +} diff --git a/core/java/android/pim/vcard/Constants.java b/core/java/android/pim/vcard/Constants.java index ca41ce5..1e2ccdf 100644 --- a/core/java/android/pim/vcard/Constants.java +++ b/core/java/android/pim/vcard/Constants.java @@ -16,16 +16,47 @@ package android.pim.vcard; /** - * Constants used in both composer and parser. + * Constants used in both exporter and importer code. */ /* package */ class Constants { - public static final String ATTR_TYPE = "TYPE"; - public static final String VERSION_V21 = "2.1"; public static final String VERSION_V30 = "3.0"; + + // The property names valid both in vCard 2.1 and 3.0. + public static final String PROPERTY_BEGIN = "BEGIN"; + public static final String PROPERTY_VERSION = "VERSION"; + public static final String PROPERTY_N = "N"; + public static final String PROPERTY_FN = "FN"; + public static final String PROPERTY_ADR = "ADR"; + public static final String PROPERTY_EMAIL = "EMAIL"; + public static final String PROPERTY_NOTE = "NOTE"; + public static final String PROPERTY_ORG = "ORG"; + public static final String PROPERTY_SOUND = "SOUND"; // Not fully supported. + public static final String PROPERTY_TEL = "TEL"; + public static final String PROPERTY_TITLE = "TITLE"; + public static final String PROPERTY_ROLE = "ROLE"; + public static final String PROPERTY_PHOTO = "PHOTO"; + public static final String PROPERTY_LOGO = "LOGO"; + public static final String PROPERTY_URL = "URL"; + public static final String PROPERTY_BDAY = "BDAY"; // Birthday + public static final String PROPERTY_END = "END"; + + // Valid property names not supported (not appropriately handled) by our vCard importer now. + public static final String PROPERTY_REV = "REV"; + public static final String PROPERTY_AGENT = "AGENT"; + + // Available in vCard 3.0. Shoud not use when composing vCard 2.1 file. + public static final String PROPERTY_NAME = "NAME"; + public static final String PROPERTY_NICKNAME = "NICKNAME"; + public static final String PROPERTY_SORT_STRING = "SORT-STRING"; - // Properties both the current (as of 2009-08-17) ContactsStruct and de-fact vCard extensions + // De-fact property values expressing phonetic names. + public static final String PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; + public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; + public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; + + // Properties both ContactsStruct in Eclair and de-fact vCard extensions // shown in http://en.wikipedia.org/wiki/VCard support are defined here. public static final String PROPERTY_X_AIM = "X-AIM"; public static final String PROPERTY_X_MSN = "X-MSN"; @@ -34,12 +65,24 @@ package android.pim.vcard; public static final String PROPERTY_X_JABBER = "X-JABBER"; public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK"; public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME"; + // Properties only ContactsStruct has. We alse use this. + public static final String PROPERTY_X_QQ = "X-QQ"; + public static final String PROPERTY_X_NETMEETING = "X-NETMEETING"; + // Phone number for Skype, available as usual phone. public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; - // Some device emits this "X-" attribute, which is specifically invalid but should be - // always properly accepted, and emitted in some special case (for that device/application). - public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; - + + // Property for Android-specific fields. + public static final String PROPERTY_X_ANDROID_CUSTOM = "X-ANDROID-CUSTOM"; + + // Properties for DoCoMo vCard. + public static final String PROPERTY_X_CLASS = "X-CLASS"; + public static final String PROPERTY_X_REDUCTION = "X-REDUCTION"; + public static final String PROPERTY_X_NO = "X-NO"; + public static final String PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; + + public static final String PARAM_TYPE = "TYPE"; + // How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0 // // e.g. @@ -52,42 +95,69 @@ package android.pim.vcard; // // So we are currently not sure which type is the best; probably we will have to change which // type should be emitted depending on the device. - public static final String ATTR_TYPE_HOME = "HOME"; - public static final String ATTR_TYPE_WORK = "WORK"; - public static final String ATTR_TYPE_FAX = "FAX"; - public static final String ATTR_TYPE_CELL = "CELL"; - public static final String ATTR_TYPE_VOICE = "VOICE"; - public static final String ATTR_TYPE_INTERNET = "INTERNET"; + public static final String PARAM_TYPE_HOME = "HOME"; + public static final String PARAM_TYPE_WORK = "WORK"; + public static final String PARAM_TYPE_FAX = "FAX"; + public static final String PARAM_TYPE_CELL = "CELL"; + public static final String PARAM_TYPE_VOICE = "VOICE"; + public static final String PARAM_TYPE_INTERNET = "INTERNET"; - public static final String ATTR_TYPE_PREF = "PREF"; + // Abbreviation of "prefered" according to vCard 2.1 specification. + // We interpret this value as "primary" property during import/export. + // + // Note: Both vCard specs does not mention anything about the requirement for this parameter, + // but there may be some vCard importer which will get confused with more than + // one "PREF"s in one property name, while Android accepts them. + public static final String PARAM_TYPE_PREF = "PREF"; - // Phone types valid in vCard and known to ContactsContract, but not so common. - public static final String ATTR_TYPE_CAR = "CAR"; - public static final String ATTR_TYPE_ISDN = "ISDN"; - public static final String ATTR_TYPE_PAGER = "PAGER"; + // Phone type parameters valid in vCard and known to ContactsContract, but not so common. + public static final String PARAM_TYPE_CAR = "CAR"; + public static final String PARAM_TYPE_ISDN = "ISDN"; + public static final String PARAM_TYPE_PAGER = "PAGER"; + public static final String PARAM_TYPE_TLX = "TLX"; // Telex // Phone types existing in vCard 2.1 but not known to ContactsContract. // TODO: should make parser make these TYPE_CUSTOM. - public static final String ATTR_TYPE_MODEM = "MODEM"; - public static final String ATTR_TYPE_MSG = "MSG"; - public static final String ATTR_TYPE_BBS = "BBS"; - public static final String ATTR_TYPE_VIDEO = "VIDEO"; - - // Phone types existing in the current Contacts structure but not valid in vCard (at least 2.1) - // These types are encoded to "X-" attributes when composing vCard for now. - // Parser passes these even if "X-" is added to the attribute. - public static final String ATTR_TYPE_PHONE_EXTRA_OTHER = "OTHER"; - public static final String ATTR_TYPE_PHONE_EXTRA_CALLBACK = "CALLBACK"; - // TODO: may be "TYPE=COMPANY,PREF", not "COMPANY-MAIN". - public static final String ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN = "COMPANY-MAIN"; - public static final String ATTR_TYPE_PHONE_EXTRA_RADIO = "RADIO"; - public static final String ATTR_TYPE_PHONE_EXTRA_TELEX = "TELEX"; - public static final String ATTR_TYPE_PHONE_EXTRA_TTY_TDD = "TTY-TDD"; - public static final String ATTR_TYPE_PHONE_EXTRA_ASSISTANT = "ASSISTANT"; - - // DoCoMo specific attribute. Used with "SOUND" property, which is alternate of SORT-STRING in + public static final String PARAM_TYPE_MODEM = "MODEM"; + public static final String PARAM_TYPE_MSG = "MSG"; + public static final String PARAM_TYPE_BBS = "BBS"; + public static final String PARAM_TYPE_VIDEO = "VIDEO"; + + // TYPE parameters for Phones, which are not formally valid in vCard (at least 2.1). + // These types are basically encoded to "X-" parameters when composing vCard. + // Parser passes these when "X-" is added to the parameter or not. + public static final String PARAM_PHONE_EXTRA_TYPE_CALLBACK = "CALLBACK"; + public static final String PARAM_PHONE_EXTRA_TYPE_RADIO = "RADIO"; + public static final String PARAM_PHONE_EXTRA_TYPE_TTY_TDD = "TTY-TDD"; + public static final String PARAM_PHONE_EXTRA_TYPE_ASSISTANT = "ASSISTANT"; + // vCard composer translates this type to "WORK" + "PREF". Just for parsing. + public static final String PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN"; + // vCard composer translates this type to "VOICE" Just for parsing. + public static final String PARAM_PHONE_EXTRA_TYPE_OTHER = "OTHER"; + + // TYPE parameters for postal addresses. + public static final String PARAM_ADR_TYPE_PARCEL = "PARCEL"; + public static final String PARAM_ADR_TYPE_DOM = "DOM"; + public static final String PARAM_ADR_TYPE_INTL = "INTL"; + + // TYPE parameters not officially valid but used in some vCard exporter. + // Do not use in composer side. + public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY"; + + // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of SORT-STRING in // vCard 3.0. - public static final String ATTR_TYPE_X_IRMC_N = "X-IRMC-N"; + public static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; + + public interface ImportOnly { + public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; + // Some device emits this "X-" parameter for expressing Google Talk, + // which is specifically invalid but should be always properly accepted, and emitted + // in some special case (for that device/application). + public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; + } + + // TODO: Should be in ContactsContract? + /* package */ static final int MAX_DATA_COLUMN = 15; private Constants() { } diff --git a/core/java/android/pim/vcard/ContactStruct.java b/core/java/android/pim/vcard/ContactStruct.java index 36e5e23..f01659e8 100644 --- a/core/java/android/pim/vcard/ContactStruct.java +++ b/core/java/android/pim/vcard/ContactStruct.java @@ -46,7 +46,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -55,11 +54,11 @@ import java.util.Map; */ public class ContactStruct { private static final String LOG_TAG = "vcard.ContactStruct"; - + // Key: the name shown in VCard. e.g. "X-AIM", "X-ICQ" // Value: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol} private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); - + static { sImMap.put(Constants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); sImMap.put(Constants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); @@ -68,12 +67,10 @@ public class ContactStruct { sImMap.put(Constants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); sImMap.put(Constants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); - sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK); + sImMap.put(Constants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, + Im.PROTOCOL_GOOGLE_TALK); } - /** - * @hide only for testing - */ static public class PhoneData { public final int type; public final String data; @@ -90,7 +87,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof PhoneData) { + if (!(obj instanceof PhoneData)) { return false; } PhoneData phoneData = (PhoneData)obj; @@ -125,7 +122,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof EmailData) { + if (!(obj instanceof EmailData)) { return false; } EmailData emailData = (EmailData)obj; @@ -202,7 +199,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof PostalData) { + if (!(obj instanceof PostalData)) { return false; } PostalData postalData = (PostalData)obj; @@ -251,87 +248,117 @@ public class ContactStruct { } } - /** - * @hide only for testing. - */ static public class OrganizationData { public final int type; - public final String companyName; - // can be changed in some VCard format. - public String positionName; + // non-final is Intended: we may change the values since this info is separated into + // two parts in vCard: "ORG" + "TITLE". + public String companyName; + public String departmentName; + public String titleName; // isPrimary is changable only when there's no appropriate one existing in // the original VCard. public boolean isPrimary; - public OrganizationData(int type, String companyName, String positionName, + public OrganizationData(int type, + String companyName, + String departmentName, + String titleName, boolean isPrimary) { this.type = type; this.companyName = companyName; - this.positionName = positionName; + this.departmentName = departmentName; + this.titleName = titleName; this.isPrimary = isPrimary; } @Override public boolean equals(Object obj) { - if (obj instanceof OrganizationData) { + if (!(obj instanceof OrganizationData)) { return false; } OrganizationData organization = (OrganizationData)obj; - return (type == organization.type && companyName.equals(organization.companyName) && - positionName.equals(organization.positionName) && + return (type == organization.type && + TextUtils.equals(companyName, organization.companyName) && + TextUtils.equals(departmentName, organization.departmentName) && + TextUtils.equals(titleName, organization.titleName) && isPrimary == organization.isPrimary); } - + @Override public String toString() { - return String.format("type: %d, company: %s, position: %s, isPrimary: %s", - type, companyName, positionName, isPrimary); + return String.format( + "type: %d, company: %s, department: %s, title: %s, isPrimary: %s", + type, companyName, departmentName, titleName, isPrimary); } } static public class ImData { + public final int protocol; + public final String customProtocol; public final int type; public final String data; - public final String label; public final boolean isPrimary; - // TODO: ContactsConstant#PROTOCOL, ContactsConstant#CUSTOM_PROTOCOL should be used? - public ImData(int type, String data, String label, boolean isPrimary) { + public ImData(int protocol, String customProtocol, int type, + String data, boolean isPrimary) { + this.protocol = protocol; + this.customProtocol = customProtocol; this.type = type; this.data = data; - this.label = label; this.isPrimary = isPrimary; } @Override public boolean equals(Object obj) { - if (obj instanceof ImData) { + if (!(obj instanceof ImData)) { return false; } ImData imData = (ImData)obj; - return (type == imData.type && data.equals(imData.data) && - label.equals(imData.label) && isPrimary == imData.isPrimary); + return (type == imData.type && protocol == imData.protocol + && (customProtocol != null ? customProtocol.equals(imData.customProtocol) : + (imData.customProtocol == null)) + && (data != null ? data.equals(imData.data) : (imData.data == null)) + && isPrimary == imData.isPrimary); } @Override public String toString() { - return String.format("type: %d, data: %s, label: %s, isPrimary: %s", - type, data, label, isPrimary); + return String.format( + "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", + type, protocol, customProtocol, data, isPrimary); } } - /** - * @hide only for testing. - */ static public class PhotoData { public static final String FORMAT_FLASH = "SWF"; public final int type; public final String formatName; // used when type is not defined in ContactsContract. public final byte[] photoBytes; + public final boolean isPrimary; - public PhotoData(int type, String formatName, byte[] photoBytes) { + public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) { this.type = type; this.formatName = formatName; this.photoBytes = photoBytes; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PhotoData)) { + return false; + } + PhotoData photoData = (PhotoData)obj; + return (type == photoData.type && + (formatName == null ? (photoData.formatName == null) : + formatName.equals(photoData.formatName)) && + (Arrays.equals(photoBytes, photoData.photoBytes)) && + (isPrimary == photoData.isPrimary)); + } + + @Override + public String toString() { + return String.format("type: %d, format: %s: size: %d, isPrimary: %s", + type, formatName, photoBytes.length, isPrimary); } } @@ -342,10 +369,6 @@ public class ContactStruct { private List<String> mPropertyValueList = new ArrayList<String>(); private byte[] mPropertyBytes; - public Property() { - clear(); - } - public void setPropertyName(final String propertyName) { mPropertyName = propertyName; } @@ -385,6 +408,7 @@ public class ContactStruct { mPropertyName = null; mParameterMap.clear(); mPropertyValueList.clear(); + mPropertyBytes = null; } } @@ -417,21 +441,13 @@ public class ContactStruct { private List<ImData> mImList; private List<PhotoData> mPhotoList; private List<String> mWebsiteList; - + private List<List<String>> mAndroidCustomPropertyList; + private final int mVCardType; private final Account mAccount; - // Each Column of four properties has ISPRIMARY field - // (See android.provider.Contacts) - // If false even after the parsing loop, we choose the first entry as a "primary" - // entry. - private boolean mPrefIsSet_Address; - private boolean mPrefIsSet_Phone; - private boolean mPrefIsSet_Email; - private boolean mPrefIsSet_Organization; - public ContactStruct() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC); + this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); } public ContactStruct(int vcardType) { @@ -444,186 +460,6 @@ public class ContactStruct { } /** - * @hide only for testing. - */ - public ContactStruct(String givenName, - String familyName, - String middleName, - String prefix, - String suffix, - String phoneticGivenName, - String pheneticFamilyName, - String phoneticMiddleName, - List<byte[]> photoBytesList, - List<String> notes, - List<PhoneData> phoneList, - List<EmailData> emailList, - List<PostalData> postalList, - List<OrganizationData> organizationList, - List<PhotoData> photoList, - List<String> websiteList) { - this(VCardConfig.VCARD_TYPE_DEFAULT); - mGivenName = givenName; - mFamilyName = familyName; - mPrefix = prefix; - mSuffix = suffix; - mPhoneticGivenName = givenName; - mPhoneticFamilyName = familyName; - mPhoneticMiddleName = middleName; - mEmailList = emailList; - mPostalList = postalList; - mOrganizationList = organizationList; - mPhotoList = photoList; - mWebsiteList = websiteList; - } - - // All getter methods should be used carefully, since they may change - // in the future as of 2009-09-24, on which I cannot be sure this structure - // is completely consolidated. - // When we are sure we will no longer change them, we'll be happy to - // make it complete public (withouth @hide tag) - // - // Also note that these getter methods should be used only after - // all properties being pushed into this object. If not, incorrect - // value will "be stored in the local cache and" be returned to you. - - /** - * @hide - */ - public String getFamilyName() { - return mFamilyName; - } - - /** - * @hide - */ - public String getGivenName() { - return mGivenName; - } - - /** - * @hide - */ - public String getMiddleName() { - return mMiddleName; - } - - /** - * @hide - */ - public String getPrefix() { - return mPrefix; - } - - /** - * @hide - */ - public String getSuffix() { - return mSuffix; - } - - /** - * @hide - */ - public String getFullName() { - return mFullName; - } - - /** - * @hide - */ - public String getPhoneticFamilyName() { - return mPhoneticFamilyName; - } - - /** - * @hide - */ - public String getPhoneticGivenName() { - return mPhoneticGivenName; - } - - /** - * @hide - */ - public String getPhoneticMiddleName() { - return mPhoneticMiddleName; - } - - /** - * @hide - */ - public String getPhoneticFullName() { - return mPhoneticFullName; - } - - /** - * @hide - */ - public final List<String> getNickNameList() { - return mNickNameList; - } - - /** - * @hide - */ - public String getDisplayName() { - if (mDisplayName == null) { - constructDisplayName(); - } - return mDisplayName; - } - - /** - * @hide - */ - public String getBirthday() { - return mBirthday; - } - - /** - * @hide - */ - public final List<PhotoData> getPhotoList() { - return mPhotoList; - } - - /** - * @hide - */ - public final List<String> getNotes() { - return mNoteList; - } - - /** - * @hide - */ - public final List<PhoneData> getPhoneList() { - return mPhoneList; - } - - /** - * @hide - */ - public final List<EmailData> getEmailList() { - return mEmailList; - } - - /** - * @hide - */ - public final List<PostalData> getPostalList() { - return mPostalList; - } - - /** - * @hide - */ - public final List<OrganizationData> getOrganizationList() { - return mOrganizationList; - } - - /** * Add a phone info to phoneList. * @param data phone number * @param type type col of content://contacts/phones @@ -635,18 +471,24 @@ public class ContactStruct { } StringBuilder builder = new StringBuilder(); String trimed = data.trim(); - int length = trimed.length(); - for (int i = 0; i < length; i++) { - char ch = trimed.charAt(i); - if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { - builder.append(ch); + final String formattedNumber; + if (type == Phone.TYPE_PAGER) { + formattedNumber = trimed; + } else { + final int length = trimed.length(); + for (int i = 0; i < length; i++) { + char ch = trimed.charAt(i); + if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { + builder.append(ch); + } } - } - - PhoneData phoneData = new PhoneData(type, - PhoneNumberUtils.formatNumber(builder.toString()), - label, isPrimary); + // Use NANP in default when there's no information about locale. + final int formattingType = (VCardConfig.isJapaneseDevice(mVCardType) ? + PhoneNumberUtils.FORMAT_JAPAN : PhoneNumberUtils.FORMAT_NANP); + formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType); + } + PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary); mPhoneList.add(phoneData); } @@ -666,24 +508,122 @@ public class ContactStruct { private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){ if (mPostalList == null) { - mPostalList = new ArrayList<PostalData>(); + mPostalList = new ArrayList<PostalData>(0); } mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); } - private void addOrganization(int type, final String companyName, - final String positionName, boolean isPrimary) { + /** + * Should be called via {@link #handleOrgValue(int, List, boolean)} or + * {@link #handleTitleValue(String)}. + */ + private void addNewOrganization(int type, final String companyName, + final String departmentName, + final String titleName, boolean isPrimary) { if (mOrganizationList == null) { mOrganizationList = new ArrayList<OrganizationData>(); } - mOrganizationList.add(new OrganizationData(type, companyName, positionName, isPrimary)); + mOrganizationList.add(new OrganizationData(type, companyName, + departmentName, titleName, isPrimary)); } + + private static final List<String> sEmptyList = new ArrayList<String>(0); - private void addIm(int type, String data, String label, boolean isPrimary) { + /** + * Set "ORG" related values to the appropriate data. If there's more than one + * OrganizationData objects, this input data are attached to the last one which does not + * have valid values (not including empty but only null). If there's no + * OrganizationData object, a new OrganizationData is created, whose title is set to null. + */ + private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) { + if (orgList == null) { + orgList = sEmptyList; + } + final String companyName; + final String departmentName; + final int size = orgList.size(); + switch (size) { + case 0: { + companyName = ""; + departmentName = null; + break; + } + case 1: { + companyName = orgList.get(0); + departmentName = null; + break; + } + default: { // More than 1. + companyName = orgList.get(0); + // We're not sure which is the correct string for department. + // In order to keep all the data, concatinate the rest of elements. + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < size; i++) { + if (i > 1) { + builder.append(' '); + } + builder.append(orgList.get(i)); + } + departmentName = builder.toString(); + } + } + if (mOrganizationList == null) { + // Create new first organization entry, with "null" title which may be + // added via handleTitleValue(). + addNewOrganization(type, companyName, departmentName, null, isPrimary); + return; + } + for (OrganizationData organizationData : mOrganizationList) { + // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty. + // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null. + if (organizationData.companyName == null && + organizationData.departmentName == null) { + // Probably the "TITLE" property comes before the "ORG" property via + // handleTitleLine(). + organizationData.companyName = companyName; + organizationData.departmentName = departmentName; + organizationData.isPrimary = isPrimary; + return; + } + } + // No OrganizatioData is available. Create another one, with "null" title, which may be + // added via handleTitleValue(). + addNewOrganization(type, companyName, departmentName, null, isPrimary); + } + + private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; + + /** + * Set "title" value to the appropriate data. If there's more than one + * OrganizationData objects, this input is attached to the last one which does not + * have valid title value (not including empty but only null). If there's no + * OrganizationData object, a new OrganizationData is created, whose company name is + * set to null. + */ + private void handleTitleValue(final String title) { + if (mOrganizationList == null) { + // Create new first organization entry, with "null" other info, which may be + // added via handleOrgValue(). + addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); + return; + } + for (OrganizationData organizationData : mOrganizationList) { + if (organizationData.titleName == null) { + organizationData.titleName = title; + return; + } + } + // No Organization is available. Create another one, with "null" other info, which may be + // added via handleOrgValue(). + addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); + } + + private void addIm(int protocol, String customProtocol, int type, + String propValue, boolean isPrimary) { if (mImList == null) { mImList = new ArrayList<ImData>(); } - mImList.add(new ImData(type, data, label, isPrimary)); + mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary)); } private void addNote(final String note) { @@ -693,43 +633,14 @@ public class ContactStruct { mNoteList.add(note); } - private void addPhotoBytes(String formatName, byte[] photoBytes) { + private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { if (mPhotoList == null) { mPhotoList = new ArrayList<PhotoData>(1); } - final PhotoData photoData = new PhotoData(0, null, photoBytes); + final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary); mPhotoList.add(photoData); } - /** - * Set "position" value to the appropriate data. If there's more than one - * OrganizationData objects, the value is set to the last one. If there's no - * OrganizationData object, a new OrganizationData is created, whose company name is - * empty. - * - * TODO: incomplete logic. fix this: - * - * e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not - * know how to handle it in general cases... - * ---- - * TITLE:Software Engineer - * ORG:Google - * ---- - */ - private void setPosition(String positionValue) { - if (mOrganizationList == null) { - mOrganizationList = new ArrayList<OrganizationData>(); - } - int size = mOrganizationList.size(); - if (size == 0) { - addOrganization(ContactsContract.CommonDataKinds.Organization.TYPE_OTHER, - "", null, false); - size = 1; - } - OrganizationData lastData = mOrganizationList.get(size - 1); - lastData.positionName = positionValue; - } - @SuppressWarnings("fallthrough") private void handleNProperty(List<String> elems) { // Family, Given, Middle, Prefix, Suffix. (1 - 5) @@ -755,7 +666,7 @@ public class ContactStruct { mFamilyName = elems.get(0); } } - + /** * Some Japanese mobile phones use this field for phonetic name, * since vCard 2.1 does not have "SORT-STRING" type. @@ -796,28 +707,36 @@ public class ContactStruct { } final String propValue = listToString(propValueList).trim(); - if (propName.equals("VERSION")) { + if (propName.equals(Constants.PROPERTY_VERSION)) { // vCard version. Ignore this. - } else if (propName.equals("FN")) { + } else if (propName.equals(Constants.PROPERTY_FN)) { mFullName = propValue; - } else if (propName.equals("NAME") && mFullName == null) { + } else if (propName.equals(Constants.PROPERTY_NAME) && mFullName == null) { // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not // actually exist in the real vCard data, does not exist. mFullName = propValue; - } else if (propName.equals("N")) { + } else if (propName.equals(Constants.PROPERTY_N)) { handleNProperty(propValueList); - } else if (propName.equals("SORT-STRING")) { + } else if (propName.equals(Constants.PROPERTY_SORT_STRING)) { mPhoneticFullName = propValue; - } else if (propName.equals("NICKNAME") || propName.equals("X-NICKNAME")) { + } else if (propName.equals(Constants.PROPERTY_NICKNAME) || + propName.equals(Constants.ImportOnly.PROPERTY_X_NICKNAME)) { addNickName(propValue); - } else if (propName.equals("SOUND")) { - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); - if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_X_IRMC_N)) { - handlePhoneticNameFromSound(propValueList); + } else if (propName.equals(Constants.PROPERTY_SOUND)) { + Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); + if (typeCollection != null && typeCollection.contains(Constants.PARAM_TYPE_X_IRMC_N)) { + // As of 2009-10-08, Parser side does not split a property value into separated + // values using ';' (in other words, propValueList.size() == 1), + // which is correct behavior from the view of vCard 2.1. + // But we want it to be separated, so do the separation here. + final List<String> phoneticNameList = + VCardUtils.constructListFromValue(propValue, + VCardConfig.isV30(mVCardType)); + handlePhoneticNameFromSound(phoneticNameList); } else { // Ignore this field since Android cannot understand what it is. } - } else if (propName.equals("ADR")) { + } else if (propName.equals(Constants.PROPERTY_ADR)) { boolean valuesAreAllEmpty = true; for (String value : propValueList) { if (value.length() > 0) { @@ -832,27 +751,25 @@ public class ContactStruct { int type = -1; String label = ""; boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Address) { - // Only first "PREF" is considered. - mPrefIsSet_Address = true; + if (typeString.equals(Constants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + } else if (typeString.equals(Constants.PARAM_TYPE_HOME)) { type = StructuredPostal.TYPE_HOME; label = ""; - } else if (typeString.equals(Constants.ATTR_TYPE_WORK) || - typeString.equalsIgnoreCase("COMPANY")) { + } else if (typeString.equals(Constants.PARAM_TYPE_WORK) || + typeString.equalsIgnoreCase(Constants.PARAM_EXTRA_TYPE_COMPANY)) { // "COMPANY" seems emitted by Windows Mobile, which is not // specifically supported by vCard 2.1. We assume this is same // as "WORK". type = StructuredPostal.TYPE_WORK; label = ""; - } else if (typeString.equals("PARCEL") || - typeString.equals("DOM") || - typeString.equals("INTL")) { + } else if (typeString.equals(Constants.PARAM_ADR_TYPE_PARCEL) || + typeString.equals(Constants.PARAM_ADR_TYPE_DOM) || + typeString.equals(Constants.PARAM_ADR_TYPE_INTL)) { // We do not have any appropriate way to store this information. } else { if (typeString.startsWith("X-") && type < 0) { @@ -871,23 +788,21 @@ public class ContactStruct { } addPostal(type, propValueList, label, isPrimary); - } else if (propName.equals("EMAIL")) { + } else if (propName.equals(Constants.PROPERTY_EMAIL)) { int type = -1; String label = null; boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Email) { - // Only first "PREF" is considered. - mPrefIsSet_Email = true; + if (typeString.equals(Constants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + } else if (typeString.equals(Constants.PARAM_TYPE_HOME)) { type = Email.TYPE_HOME; - } else if (typeString.equals(Constants.ATTR_TYPE_WORK)) { + } else if (typeString.equals(Constants.PARAM_TYPE_WORK)) { type = Email.TYPE_WORK; - } else if (typeString.equals(Constants.ATTR_TYPE_CELL)) { + } else if (typeString.equals(Constants.PARAM_TYPE_CELL)) { type = Email.TYPE_MOBILE; } else { if (typeString.startsWith("X-") && type < 0) { @@ -905,50 +820,48 @@ public class ContactStruct { type = Email.TYPE_OTHER; } addEmail(type, propValue, label, isPrimary); - } else if (propName.equals("ORG")) { + } else if (propName.equals(Constants.PROPERTY_ORG)) { // vCard specification does not specify other types. - int type = Organization.TYPE_WORK; + final int type = Organization.TYPE_WORK; boolean isPrimary = false; - - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Organization) { - // vCard specification officially does not have PREF in ORG. - // This is just for safety. - mPrefIsSet_Organization = true; + if (typeString.equals(Constants.PARAM_TYPE_PREF)) { isPrimary = true; } } } - - StringBuilder builder = new StringBuilder(); - for (Iterator<String> iter = propValueList.iterator(); iter.hasNext();) { - builder.append(iter.next()); - if (iter.hasNext()) { - builder.append(' '); - } - } - addOrganization(type, builder.toString(), "", isPrimary); - } else if (propName.equals("TITLE")) { - setPosition(propValue); - } else if (propName.equals("ROLE")) { - setPosition(propValue); - } else if (propName.equals("PHOTO") || propName.equals("LOGO")) { - String formatName = null; - Collection<String> typeCollection = paramMap.get("TYPE"); - if (typeCollection != null) { - formatName = typeCollection.iterator().next(); - } + handleOrgValue(type, propValueList, isPrimary); + } else if (propName.equals(Constants.PROPERTY_TITLE)) { + handleTitleValue(propValue); + } else if (propName.equals(Constants.PROPERTY_ROLE)) { + // This conflicts with TITLE. Ignore for now... + // handleTitleValue(propValue); + } else if (propName.equals(Constants.PROPERTY_PHOTO) || + propName.equals(Constants.PROPERTY_LOGO)) { Collection<String> paramMapValue = paramMap.get("VALUE"); if (paramMapValue != null && paramMapValue.contains("URL")) { // Currently we do not have appropriate example for testing this case. } else { - addPhotoBytes(formatName, propBytes); + final Collection<String> typeCollection = paramMap.get("TYPE"); + String formatName = null; + boolean isPrimary = false; + if (typeCollection != null) { + for (String typeValue : typeCollection) { + if (Constants.PARAM_TYPE_PREF.equals(typeValue)) { + isPrimary = true; + } else if (formatName == null){ + formatName = typeValue; + } + } + } + addPhotoBytes(formatName, propBytes, isPrimary); } - } else if (propName.equals("TEL")) { - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); - Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection); + } else if (propName.equals(Constants.PROPERTY_TEL)) { + final Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); + final Object typeObject = + VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue); final int type; final String label; if (typeObject instanceof Integer) { @@ -960,9 +873,7 @@ public class ContactStruct { } final boolean isPrimary; - if (!mPrefIsSet_Phone && typeCollection != null && - typeCollection.contains(Constants.ATTR_TYPE_PREF)) { - mPrefIsSet_Phone = true; + if (typeCollection != null && typeCollection.contains(Constants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; @@ -970,53 +881,59 @@ public class ContactStruct { addPhone(type, propValue, label, isPrimary); } else if (propName.equals(Constants.PROPERTY_X_SKYPE_PSTNNUMBER)) { // The phone number available via Skype. - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); // XXX: should use TYPE_CUSTOM + the label "Skype"? (which may need localization) int type = Phone.TYPE_OTHER; final String label = null; final boolean isPrimary; - if (!mPrefIsSet_Phone && typeCollection != null && - typeCollection.contains(Constants.ATTR_TYPE_PREF)) { - mPrefIsSet_Phone = true; + if (typeCollection != null && typeCollection.contains(Constants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; } addPhone(type, propValue, label, isPrimary); - } else if (sImMap.containsKey(propName)){ - int type = sImMap.get(propName); + } else if (sImMap.containsKey(propName)) { + final int protocol = sImMap.get(propName); boolean isPrimary = false; - final Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + int type = -1; + final Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + if (typeString.equals(Constants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_HOME)) { - type = Phone.TYPE_HOME; - } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_WORK)) { - type = Phone.TYPE_WORK; + } else if (type < 0) { + if (typeString.equalsIgnoreCase(Constants.PARAM_TYPE_HOME)) { + type = Im.TYPE_HOME; + } else if (typeString.equalsIgnoreCase(Constants.PARAM_TYPE_WORK)) { + type = Im.TYPE_WORK; + } } } } if (type < 0) { type = Phone.TYPE_HOME; } - addIm(type, propValue, null, isPrimary); - } else if (propName.equals("NOTE")) { + addIm(protocol, null, type, propValue, isPrimary); + } else if (propName.equals(Constants.PROPERTY_NOTE)) { addNote(propValue); - } else if (propName.equals("URL")) { + } else if (propName.equals(Constants.PROPERTY_URL)) { if (mWebsiteList == null) { mWebsiteList = new ArrayList<String>(1); } mWebsiteList.add(propValue); - } else if (propName.equals("X-PHONETIC-FIRST-NAME")) { + } else if (propName.equals(Constants.PROPERTY_BDAY)) { + mBirthday = propValue; + } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_FIRST_NAME)) { mPhoneticGivenName = propValue; - } else if (propName.equals("X-PHONETIC-MIDDLE-NAME")) { + } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { mPhoneticMiddleName = propValue; - } else if (propName.equals("X-PHONETIC-LAST-NAME")) { + } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_LAST_NAME)) { mPhoneticFamilyName = propValue; - } else if (propName.equals("BDAY")) { - mBirthday = propValue; + } else if (propName.equals(Constants.PROPERTY_X_ANDROID_CUSTOM)) { + final List<String> customPropertyList = + VCardUtils.constructListFromValue(propValue, + VCardConfig.isV30(mVCardType)); + handleAndroidCustomProperty(customPropertyList); /*} else if (propName.equals("REV")) { // Revision of this VCard entry. I think we can ignore this. } else if (propName.equals("UID")) { @@ -1044,11 +961,21 @@ public class ContactStruct { } } + private void handleAndroidCustomProperty(final List<String> customPropertyList) { + if (mAndroidCustomPropertyList == null) { + mAndroidCustomPropertyList = new ArrayList<List<String>>(); + } + mAndroidCustomPropertyList.add(customPropertyList); + } + /** * Construct the display name. The constructed data must not be null. */ private void constructDisplayName() { - if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { + // FullName (created via "FN" or "NAME" field) is prefered. + if (!TextUtils.isEmpty(mFullName)) { + mDisplayName = mFullName; + } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { StringBuilder builder = new StringBuilder(); List<String> nameList; switch (VCardConfig.getNameOrderType(mVCardType)) { @@ -1079,8 +1006,6 @@ public class ContactStruct { } } mDisplayName = builder.toString(); - } else if (!TextUtils.isEmpty(mFullName)) { - mDisplayName = mFullName; } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && TextUtils.isEmpty(mPhoneticGivenName))) { mDisplayName = VCardUtils.constructNameFromElements(mVCardType, @@ -1097,33 +1022,18 @@ public class ContactStruct { mDisplayName = ""; } } - + /** * Consolidate several fielsds (like mName) using name candidates, */ public void consolidateFields() { constructDisplayName(); - + if (mPhoneticFullName != null) { mPhoneticFullName = mPhoneticFullName.trim(); } - - // If there is no "PREF", we choose the first entries as primary. - if (!mPrefIsSet_Phone && mPhoneList != null && mPhoneList.size() > 0) { - mPhoneList.get(0).isPrimary = true; - } - - if (!mPrefIsSet_Address && mPostalList != null && mPostalList.size() > 0) { - mPostalList.get(0).isPrimary = true; - } - if (!mPrefIsSet_Email && mEmailList != null && mEmailList.size() > 0) { - mEmailList.get(0).isPrimary = true; - } - if (!mPrefIsSet_Organization && mOrganizationList != null && mOrganizationList.size() > 0) { - mOrganizationList.get(0).isPrimary = true; - } } - + // From GoogleSource.java in Contacts app. private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; @@ -1161,7 +1071,7 @@ public class ContactStruct { } operationList.add(builder.build()); - { + if (!nameFieldsAreEmpty()) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); @@ -1172,31 +1082,31 @@ public class ContactStruct { builder.withValue(StructuredName.PREFIX, mPrefix); builder.withValue(StructuredName.SUFFIX, mSuffix); - builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); - builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); - builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); + if (!(TextUtils.isEmpty(mPhoneticGivenName) + && TextUtils.isEmpty(mPhoneticFamilyName) + && TextUtils.isEmpty(mPhoneticMiddleName))) { + builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); + builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); + builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); + } else if (!TextUtils.isEmpty(mPhoneticFullName)) { + builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName); + } builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); operationList.add(builder.build()); } if (mNickNameList != null && mNickNameList.size() > 0) { - boolean first = true; for (String nickName : mNickNameList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); - builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); builder.withValue(Nickname.NAME, nickName); - if (first) { - builder.withValue(Data.IS_PRIMARY, 1); - first = false; - } operationList.add(builder.build()); } } - + if (mPhoneList != null) { for (PhoneData phoneData : mPhoneList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); @@ -1209,30 +1119,34 @@ public class ContactStruct { } builder.withValue(Phone.NUMBER, phoneData.data); if (phoneData.isPrimary) { - builder.withValue(Data.IS_PRIMARY, 1); + builder.withValue(Phone.IS_PRIMARY, 1); } operationList.add(builder.build()); } } - + if (mOrganizationList != null) { - boolean first = true; for (OrganizationData organizationData : mOrganizationList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); - - // Currently, we do not use TYPE_CUSTOM. builder.withValue(Organization.TYPE, organizationData.type); - builder.withValue(Organization.COMPANY, organizationData.companyName); - builder.withValue(Organization.TITLE, organizationData.positionName); - if (first) { - builder.withValue(Data.IS_PRIMARY, 1); + if (organizationData.companyName != null) { + builder.withValue(Organization.COMPANY, organizationData.companyName); + } + if (organizationData.departmentName != null) { + builder.withValue(Organization.DEPARTMENT, organizationData.departmentName); + } + if (organizationData.titleName != null) { + builder.withValue(Organization.TITLE, organizationData.titleName); + } + if (organizationData.isPrimary) { + builder.withValue(Organization.IS_PRIMARY, 1); } operationList.add(builder.build()); } } - + if (mEmailList != null) { for (EmailData emailData : mEmailList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); @@ -1265,12 +1179,11 @@ public class ContactStruct { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); - builder.withValue(Im.TYPE, imData.type); - if (imData.type == Im.TYPE_CUSTOM) { - builder.withValue(Im.LABEL, imData.label); + builder.withValue(Im.PROTOCOL, imData.protocol); + if (imData.protocol == Im.PROTOCOL_CUSTOM) { + builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol); } - builder.withValue(Im.DATA, imData.data); if (imData.isPrimary) { builder.withValue(Data.IS_PRIMARY, 1); } @@ -1282,22 +1195,19 @@ public class ContactStruct { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); - builder.withValue(Note.NOTE, note); operationList.add(builder.build()); } } - + if (mPhotoList != null) { - boolean first = true; for (PhotoData photoData : mPhotoList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); builder.withValue(Photo.PHOTO, photoData.photoBytes); - if (first) { - builder.withValue(Data.IS_PRIMARY, 1); - first = false; + if (photoData.isPrimary) { + builder.withValue(Photo.IS_PRIMARY, 1); } operationList.add(builder.build()); } @@ -1310,12 +1220,12 @@ public class ContactStruct { builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); builder.withValue(Website.URL, website); // There's no information about the type of URL in vCard. - // We use TYPE_HOME for safety. - builder.withValue(Website.TYPE, Website.TYPE_HOME); + // We use TYPE_HOMEPAGE for safety. + builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE); operationList.add(builder.build()); } } - + if (!TextUtils.isEmpty(mBirthday)) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); @@ -1325,6 +1235,36 @@ public class ContactStruct { operationList.add(builder.build()); } + if (mAndroidCustomPropertyList != null) { + for (List<String> customPropertyList : mAndroidCustomPropertyList) { + int size = customPropertyList.size(); + if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) { + continue; + } else if (size > Constants.MAX_DATA_COLUMN + 1) { + size = Constants.MAX_DATA_COLUMN + 1; + customPropertyList = + customPropertyList.subList(0, Constants.MAX_DATA_COLUMN + 2); + } + + int i = 0; + for (final String customPropertyValue : customPropertyList) { + if (i == 0) { + final String mimeType = customPropertyValue; + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, mimeType); + } else { // 1 <= i && i <= MAX_DATA_COLUMNS + if (!TextUtils.isEmpty(customPropertyValue)) { + builder.withValue("data" + i, customPropertyValue); + } + } + + i++; + } + operationList.add(builder.build()); + } + } + if (myGroupsId != null) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); @@ -1342,6 +1282,19 @@ public class ContactStruct { } } + private boolean nameFieldsAreEmpty() { + return (TextUtils.isEmpty(mFamilyName) + && TextUtils.isEmpty(mMiddleName) + && TextUtils.isEmpty(mGivenName) + && TextUtils.isEmpty(mPrefix) + && TextUtils.isEmpty(mSuffix) + && TextUtils.isEmpty(mFullName) + && TextUtils.isEmpty(mPhoneticFamilyName) + && TextUtils.isEmpty(mPhoneticMiddleName) + && TextUtils.isEmpty(mPhoneticGivenName) + && TextUtils.isEmpty(mPhoneticFullName)); + } + public boolean isIgnorable() { return getDisplayName().length() == 0; } @@ -1364,4 +1317,99 @@ public class ContactStruct { return ""; } } + + // All getter methods should be used carefully, since they may change + // in the future as of 2009-10-05, on which I cannot be sure this structure + // is completely consolidated. + // + // Also note that these getter methods should be used only after + // all properties being pushed into this object. If not, incorrect + // value will "be stored in the local cache and" be returned to you. + + public String getFamilyName() { + return mFamilyName; + } + + public String getGivenName() { + return mGivenName; + } + + public String getMiddleName() { + return mMiddleName; + } + + public String getPrefix() { + return mPrefix; + } + + public String getSuffix() { + return mSuffix; + } + + public String getFullName() { + return mFullName; + } + + public String getPhoneticFamilyName() { + return mPhoneticFamilyName; + } + + public String getPhoneticGivenName() { + return mPhoneticGivenName; + } + + public String getPhoneticMiddleName() { + return mPhoneticMiddleName; + } + + public String getPhoneticFullName() { + return mPhoneticFullName; + } + + public final List<String> getNickNameList() { + return mNickNameList; + } + + public String getBirthday() { + return mBirthday; + } + + public final List<String> getNotes() { + return mNoteList; + } + + public final List<PhoneData> getPhoneList() { + return mPhoneList; + } + + public final List<EmailData> getEmailList() { + return mEmailList; + } + + public final List<PostalData> getPostalList() { + return mPostalList; + } + + public final List<OrganizationData> getOrganizationList() { + return mOrganizationList; + } + + public final List<ImData> getImList() { + return mImList; + } + + public final List<PhotoData> getPhotoList() { + return mPhotoList; + } + + public final List<String> getWebsiteList() { + return mWebsiteList; + } + + public String getDisplayName() { + if (mDisplayName == null) { + constructDisplayName(); + } + return mDisplayName; + } } diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index 9638262..5a14f5f 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -41,6 +41,7 @@ import android.provider.ContactsContract.CommonDataKinds.Photo; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; +import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.text.format.Time; import android.util.CharsetUtils; @@ -53,6 +54,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -72,19 +74,38 @@ import java.util.Set; * Usually, this class should be used like this. * </p> * - * <pre class="prettyprint"> VCardComposer composer = null; try { composer = new - * VCardComposer(context); composer.addHandler(composer.new - * HandlerForOutputStream(outputStream)); if (!composer.init()) { // Do - * something handling the situation. return; } while (!composer.isAfterLast()) { - * if (mCanceled) { // Assume a user may cancel this operation during the - * export. return; } if (!composer.createOneEntry()) { // Do something handling - * the error situation. return; } } } finally { if (composer != null) { - * composer.terminate(); } } </pre> + * <pre class="prettyprint">VCardComposer composer = null; + * try { + * composer = new VCardComposer(context); + * composer.addHandler( + * composer.new HandlerForOutputStream(outputStream)); + * if (!composer.init()) { + * // Do something handling the situation. + * return; + * } + * while (!composer.isAfterLast()) { + * if (mCanceled) { + * // Assume a user may cancel this operation during the export. + * return; + * } + * if (!composer.createOneEntry()) { + * // Do something handling the error situation. + * return; + * } + * } + * } finally { + * if (composer != null) { + * composer.terminate(); + * } + * } </pre> */ public class VCardComposer { private static final String LOG_TAG = "vcard.VCardComposer"; - private static final String DEFAULT_EMAIL_TYPE = Constants.ATTR_TYPE_INTERNET; + // TODO: Should be configurable? + public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; + public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; + public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = "Failed to get database information"; @@ -95,28 +116,84 @@ public class VCardComposer { public static final String FAILURE_REASON_NOT_INITIALIZED = "The vCard composer object is not correctly initialized"; + /** Should be visible only from developers... (no need to translate, hopefully) */ + public static final String FAILURE_REASON_UNSUPPORTED_URI = + "The Uri vCard composer received is not supported by the composer."; + public static final String NO_ERROR = "No error"; + public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + + // Property for call log entry + private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; + private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; + private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; + private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; + + private static final String VCARD_DATA_VCARD = "VCARD"; + private static final String VCARD_DATA_PUBLIC = "PUBLIC"; + + private static final String VCARD_PARAM_SEPARATOR = ";"; + private static final String VCARD_END_OF_LINE = "\r\n"; + private static final String VCARD_DATA_SEPARATOR = ":"; + private static final String VCARD_ITEM_SEPARATOR = ";"; + private static final String VCARD_WS = " "; + private static final String VCARD_PARAM_EQUAL = "="; + + private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; + + private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; + private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; + + private static final String SHIFT_JIS = "SHIFT_JIS"; + private static final String UTF_8 = "UTF-8"; + + /** + * Special URI for testing. + */ + public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; + public static final Uri VCARD_TEST_AUTHORITY_URI = + Uri.parse("content://" + VCARD_TEST_AUTHORITY); + public static final Uri CONTACTS_TEST_CONTENT_URI = + Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); + private static final Uri sDataRequestUri; + private static final Map<Integer, String> sImMap; + + /** + * See the comment in {@link VCardConfig#FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES}. + */ + private static final Set<String> sPrimaryPropertyNameSet; static { Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); sDataRequestUri = builder.build(); + sImMap = new HashMap<Integer, String>(); + sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); + sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); + sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); + sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); + sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); + sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); + // Google talk is a special case. + + // TODO: incomplete. Implement properly + sPrimaryPropertyNameSet = new HashSet<String>(); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_N); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_FN); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_SOUND); } public static interface OneEntryHandler { public boolean onInit(Context context); - public boolean onEntryCreated(String vcard); - public void onTerminate(); } /** * <p> - * An useful example handler, which emits VCard String to outputstream one - * by one. + * An useful example handler, which emits VCard String to outputstream one by one. * </p> * <p> * The input OutputStream object is closed() on {{@link #onTerminate()}. @@ -211,65 +288,6 @@ public class VCardComposer { } } - public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; - - private static final String VCARD_PROPERTY_ADR = "ADR"; - private static final String VCARD_PROPERTY_BEGIN = "BEGIN"; - private static final String VCARD_PROPERTY_EMAIL = "EMAIL"; - private static final String VCARD_PROPERTY_END = "END"; - private static final String VCARD_PROPERTY_NAME = "N"; - private static final String VCARD_PROPERTY_FULL_NAME = "FN"; - private static final String VCARD_PROPERTY_NOTE = "NOTE"; - private static final String VCARD_PROPERTY_ORG = "ORG"; - private static final String VCARD_PROPERTY_SOUND = "SOUND"; - private static final String VCARD_PROPERTY_SORT_STRING = "SORT-STRING"; - private static final String VCARD_PROPERTY_NICKNAME = "NICKNAME"; - private static final String VCARD_PROPERTY_TEL = "TEL"; - private static final String VCARD_PROPERTY_TITLE = "TITLE"; - private static final String VCARD_PROPERTY_PHOTO = "PHOTO"; - private static final String VCARD_PROPERTY_VERSION = "VERSION"; - private static final String VCARD_PROPERTY_URL = "URL"; - private static final String VCARD_PROPERTY_BIRTHDAY = "BDAY"; - - private static final String VCARD_PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; - private static final String VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; - private static final String VCARD_PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; - - // Android specific properties - // TODO: ues extra MIME-TYPE instead of adding this kind of inflexible fields - private static final String VCARD_PROPERTY_X_NICKNAME = "X-NICKNAME"; - - // Property for call log entry - private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; - private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; - private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; - private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; - - // Properties for DoCoMo vCard. - private static final String VCARD_PROPERTY_X_CLASS = "X-CLASS"; - private static final String VCARD_PROPERTY_X_REDUCTION = "X-REDUCTION"; - private static final String VCARD_PROPERTY_X_NO = "X-NO"; - private static final String VCARD_PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; - - private static final String VCARD_DATA_VCARD = "VCARD"; - private static final String VCARD_DATA_PUBLIC = "PUBLIC"; - - private static final String VCARD_ATTR_SEPARATOR = ";"; - private static final String VCARD_COL_SEPARATOR = "\r\n"; - private static final String VCARD_DATA_SEPARATOR = ":"; - private static final String VCARD_ITEM_SEPARATOR = ";"; - private static final String VCARD_WS = " "; - private static final String VCARD_ATTR_EQUAL = "="; - - // Type strings are now in VCardConstants.java. - - private static final String VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; - - private static final String VCARD_ATTR_ENCODING_BASE64_V21 = "ENCODING=BASE64"; - private static final String VCARD_ATTR_ENCODING_BASE64_V30 = "ENCODING=b"; - - private static final String SHIFT_JIS = "SHIFT_JIS"; - private final Context mContext; private final int mVCardType; private final boolean mCareHandlerErrors; @@ -286,34 +304,21 @@ public class VCardComposer { private final boolean mUsesDefactProperty; private final boolean mUsesUtf8; private final boolean mUsesShiftJis; - private final boolean mUsesQPToPrimaryProperties; + private final boolean mAppendTypeParamName; + private final boolean mRefrainsQPToPrimaryProperties; + private final boolean mNeedsToConvertPhoneticString; private Cursor mCursor; private int mIdColumn; private final String mCharsetString; - private final String mVCardAttributeCharset; + private final String mVCardCharsetParameter; private boolean mTerminateIsCalled; final private List<OneEntryHandler> mHandlerList; private String mErrorReason = NO_ERROR; - private static final Map<Integer, String> sImMap; - - static { - sImMap = new HashMap<Integer, String>(); - sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); - sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); - sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); - sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); - sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); - sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); - // Google talk is a special case. - } - - private boolean mIsCallLogComposer = false; - - private boolean mNeedPhotoForVCard = true; + private boolean mIsCallLogComposer; private static final String[] sContactsProjection = new String[] { Contacts._ID, @@ -334,108 +339,98 @@ public class VCardComposer { private static final String FLAG_TIMEZONE_UTC = "Z"; public VCardComposer(Context context) { - this(context, VCardConfig.VCARD_TYPE_DEFAULT, true, false, true); + this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); } - public VCardComposer(Context context, String vcardTypeStr, - boolean careHandlerErrors) { - this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), - careHandlerErrors, false, true); + public VCardComposer(Context context, int vcardType) { + this(context, vcardType, true); } - public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { - this(context, vcardType, careHandlerErrors, false, true); + public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { + this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); } /** * Construct for supporting call log entry vCard composing. - * - * @param isCallLogComposer true if this composer is for creating Call Log vCard. */ - public VCardComposer(Context context, int vcardType, boolean careHandlerErrors, - boolean isCallLogComposer, boolean needPhotoInVCard) { + public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { mContext = context; mVCardType = vcardType; mCareHandlerErrors = careHandlerErrors; - mIsCallLogComposer = isCallLogComposer; - mNeedPhotoForVCard = needPhotoInVCard; mContentResolver = context.getContentResolver(); mIsV30 = VCardConfig.isV30(vcardType); mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType); mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mIsJapaneseMobilePhone = VCardConfig - .needsToConvertPhoneticString(vcardType); - mOnlyOneNoteFieldIsAvailable = VCardConfig - .onlyOneNoteFieldIsAvailable(vcardType); - mUsesAndroidProperty = VCardConfig - .usesAndroidSpecificProperty(vcardType); + mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType); + mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); + mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); mUsesUtf8 = VCardConfig.usesUtf8(vcardType); mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); - mUsesQPToPrimaryProperties = VCardConfig.usesQPToPrimaryProperties(vcardType); + mRefrainsQPToPrimaryProperties = VCardConfig.refrainsQPToPrimaryProperties(vcardType); + mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); + mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); if (mIsDoCoMo) { - mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + String charset; + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + mCharsetString = charset; // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in // Android, not shown to the public). - mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; } else if (mUsesShiftJis) { - mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + String charset; + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + mCharsetString = charset; + mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; } else { - mCharsetString = "UTF-8"; - mVCardAttributeCharset = "CHARSET=UTF-8"; + mCharsetString = UTF_8; + mVCardCharsetParameter = "CHARSET=" + UTF_8; } } /** - * This static function is to compose vCard for phone own number + * Must call before {{@link #init()}. */ - public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, - String phoneNumber, boolean vcardVer21) { - final StringBuilder builder = new StringBuilder(); - appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (!vcardVer21) { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); - } else { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); - } - - boolean needCharset = false; - if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { - needCharset = true; + public void addHandler(OneEntryHandler handler) { + if (handler != null) { + mHandlerList.add(handler); } - // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, phoneName, needCharset, false); - appendVCardLine(builder, VCARD_PROPERTY_NAME, phoneName, needCharset, false); - - String label = Integer.toString(phonetype); - appendVCardTelephoneLine(builder, phonetype, label, phoneNumber); - - appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); - - return builder.toString(); } /** - * Must call before {{@link #init()}. + * @return Returns true when initialization is successful and all the other + * methods are available. Returns false otherwise. */ - public void addHandler(OneEntryHandler handler) { - mHandlerList.add(handler); - } - public boolean init() { return init(null, null); } + public boolean init(final String selection, final String[] selectionArgs) { + return init(Contacts.CONTENT_URI, selection, selectionArgs, null); + } + /** - * @return Returns true when initialization is successful and all the other - * methods are available. Returns false otherwise. + * Note that this is unstable interface, may be deleted in the future. */ - public boolean init(final String selection, final String[] selectionArgs) { + public boolean init(final Uri contentUri, final String selection, + final String[] selectionArgs, final String sortOrder) { + if (contentUri == null) { + return false; + } if (mCareHandlerErrors) { List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); @@ -454,13 +449,19 @@ public class VCardComposer { } } - if (mIsCallLogComposer) { - mCursor = mContentResolver.query(CallLog.Calls.CONTENT_URI, sCallLogProjection, - selection, selectionArgs, null); + final String[] projection; + if (CallLog.Calls.CONTENT_URI.equals(contentUri)) { + projection = sCallLogProjection; + mIsCallLogComposer = true; + } else if (Contacts.CONTENT_URI.equals(contentUri) || + CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { + projection = sContactsProjection; } else { - mCursor = mContentResolver.query(Contacts.CONTENT_URI, sContactsProjection, - selection, selectionArgs, null); + mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; + return false; } + mCursor = mContentResolver.query( + contentUri, projection, selection, selectionArgs, sortOrder); if (mCursor == null) { mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; @@ -537,89 +538,6 @@ public class VCardComposer { return true; } - /** - * Format according to RFC 2445 DATETIME type. - * The format is: ("%Y%m%dT%H%M%SZ"). - */ - private final String toRfc2455Format(final long millSecs) { - Time startDate = new Time(); - startDate.set(millSecs); - String date = startDate.format2445(); - return date + FLAG_TIMEZONE_UTC; - } - - /** - * Try to append the property line for a call history time stamp field if possible. - * Do nothing if the call log type gotton from the database is invalid. - */ - private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) { - // Extension for call history as defined in - // in the Specification for Ic Mobile Communcation - ver 1.1, - // Oct 2000. This is used to send the details of the call - // history - missed, incoming, outgoing along with date and time - // to the requesting device (For example, transferring phone book - // when connected over bluetooth) - // - // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" - final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); - final String callLogTypeStr; - switch (callLogType) { - case Calls.INCOMING_TYPE: { - callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; - break; - } - case Calls.OUTGOING_TYPE: { - callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; - break; - } - case Calls.MISSED_TYPE: { - callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; - break; - } - default: { - Log.w(LOG_TAG, "Call log type not correct."); - return; - } - } - - final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); - builder.append(VCARD_PROPERTY_X_TIMESTAMP); - builder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(builder, callLogTypeStr); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(toRfc2455Format(dateAsLong)); - builder.append(VCARD_COL_SEPARATOR); - } - - private String createOneCallLogEntryInternal() { - final StringBuilder builder = new StringBuilder(); - appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); - } else { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); - } - String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); - if (TextUtils.isEmpty(name)) { - name = mCursor.getString(NUMBER_COLUMN_INDEX); - } - final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); - // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, name, needCharset, false); - appendVCardLine(builder, VCARD_PROPERTY_NAME, name, needCharset, false); - - String number = mCursor.getString(NUMBER_COLUMN_INDEX); - int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); - String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); - if (TextUtils.isEmpty(label)) { - label = Integer.toString(type); - } - appendVCardTelephoneLine(builder, type, label, number); - tryAppendCallHistoryTimeStampField(builder); - appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); - return builder.toString(); - } - private String createOneEntryInternal(final String contactId) { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); @@ -636,8 +554,7 @@ public class VCardComposer { dataExists = entityIterator.hasNext(); while (entityIterator.hasNext()) { Entity entity = entityIterator.next(); - for (NamedContentValues namedContentValues : entity - .getSubValues()) { + for (NamedContentValues namedContentValues : entity.getSubValues()) { ContentValues contentValues = namedContentValues.values; String key = contentValues.getAsString(Data.MIMETYPE); if (key != null) { @@ -666,11 +583,11 @@ public class VCardComposer { } final StringBuilder builder = new StringBuilder(); - appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); } else { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); } appendStructuredNames(builder, contentValuesListMap); @@ -682,20 +599,18 @@ public class VCardComposer { appendWebsites(builder, contentValuesListMap); appendBirthday(builder, contentValuesListMap); appendOrganizations(builder, contentValuesListMap); - if (mNeedPhotoForVCard) { - appendPhotos(builder, contentValuesListMap); - } + appendPhotos(builder, contentValuesListMap); appendNotes(builder, contentValuesListMap); - // TODO: GroupMembership + // TODO: GroupMembership, Relation, Event other than birthday. if (mIsDoCoMo) { - appendVCardLine(builder, VCARD_PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); - appendVCardLine(builder, VCARD_PROPERTY_X_REDUCTION, ""); - appendVCardLine(builder, VCARD_PROPERTY_X_NO, ""); - appendVCardLine(builder, VCARD_PROPERTY_X_DCM_HMN_MODE, ""); + appendVCardLine(builder, Constants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); + appendVCardLine(builder, Constants.PROPERTY_X_REDUCTION, ""); + appendVCardLine(builder, Constants.PROPERTY_X_NO, ""); + appendVCardLine(builder, Constants.PROPERTY_X_DCM_HMN_MODE, ""); } - appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); return builder.toString(); } @@ -753,11 +668,11 @@ public class VCardComposer { if (contentValuesList != null && contentValuesList.size() > 0) { appendStructuredNamesInternal(builder, contentValuesList); } else if (mIsDoCoMo) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + appendVCardLine(builder, Constants.PROPERTY_N, ""); } else if (mIsV30) { // vCard 3.0 requires "N" and "FN" properties. - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + appendVCardLine(builder, Constants.PROPERTY_N, ""); + appendVCardLine(builder, Constants.PROPERTY_FN, ""); } } @@ -767,51 +682,80 @@ public class VCardComposer { // may get confused with multiple "N", "FN", etc. properties, though it is valid in // vCard spec. ContentValues primaryContentValues = null; + ContentValues subprimaryContentValues = null; for (ContentValues contentValues : contentValuesList) { + if (contentValues == null){ + continue; + } Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); - if (isSuperPrimary != null && isSuperPrimary != 0) { + if (isSuperPrimary != null && isSuperPrimary > 0) { // We choose "super primary" ContentValues. primaryContentValues = contentValues; break; - } else if (primaryContentValues == null && contentValues != null) { - // We choose the first ContentValues if "super primary" ContentValues does not exist. - primaryContentValues = contentValues; + } else if (primaryContentValues == null) { + // We choose the first "primary" ContentValues + // if "super primary" ContentValues does not exist. + Integer primary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); + if (primary != null && primary > 0) { + primaryContentValues = contentValues; + // Do not break, since there may be ContentValues with "super primary" + // afterword. + } else if (subprimaryContentValues == null) { + subprimaryContentValues = contentValues; + } } } if (primaryContentValues == null) { - Log.e(LOG_TAG, "All ContentValues given from database is empty."); - primaryContentValues = new ContentValues(); - } - - final String familyName = primaryContentValues - .getAsString(StructuredName.FAMILY_NAME); - final String middleName = primaryContentValues - .getAsString(StructuredName.MIDDLE_NAME); - final String givenName = primaryContentValues - .getAsString(StructuredName.GIVEN_NAME); - final String prefix = primaryContentValues - .getAsString(StructuredName.PREFIX); - final String suffix = primaryContentValues - .getAsString(StructuredName.SUFFIX); - final String displayName = primaryContentValues - .getAsString(StructuredName.DISPLAY_NAME); + if (subprimaryContentValues != null) { + // We choose the first ContentValues if any "primary" ContentValues does not exist. + primaryContentValues = subprimaryContentValues; + } else { + Log.e(LOG_TAG, "All ContentValues given from database is empty."); + primaryContentValues = new ContentValues(); + } + } + + final String familyName = primaryContentValues.getAsString(StructuredName.FAMILY_NAME); + final String middleName = primaryContentValues.getAsString(StructuredName.MIDDLE_NAME); + final String givenName = primaryContentValues.getAsString(StructuredName.GIVEN_NAME); + final String prefix = primaryContentValues.getAsString(StructuredName.PREFIX); + final String suffix = primaryContentValues.getAsString(StructuredName.SUFFIX); + final String displayName = primaryContentValues.getAsString(StructuredName.DISPLAY_NAME); if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { + final boolean shouldAppendCharsetParameterToName = + !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && + shouldAppendCharsetParameters(Arrays.asList( + familyName, givenName, middleName, prefix, suffix)); + final boolean reallyUseQuotedPrintableToName = + (!mRefrainsQPToPrimaryProperties && + !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); + + final String formattedName; + if (!TextUtils.isEmpty(displayName)) { + formattedName = displayName; + } else { + formattedName = VCardUtils.constructNameFromElements( + VCardConfig.getNameOrderType(mVCardType), + familyName, middleName, givenName, prefix, suffix); + } + final boolean shouldAppendCharsetParameterToFN = + !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && + shouldAppendCharsetParameter(formattedName); + final boolean reallyUseQuotedPrintableToFN = + !mRefrainsQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); + final String encodedFamily; final String encodedGiven; final String encodedMiddle; final String encodedPrefix; final String encodedSuffix; - - final boolean reallyUseQuotedPrintableToName = - (mUsesQPToPrimaryProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); - if (reallyUseQuotedPrintableToName) { encodedFamily = encodeQuotedPrintable(familyName); encodedGiven = encodeQuotedPrintable(givenName); @@ -826,72 +770,79 @@ public class VCardComposer { encodedSuffix = escapeCharacters(suffix); } - // N property. This order is specified by vCard spec and does not depend on countries. - builder.append(VCARD_PROPERTY_NAME); - if (shouldAppendCharsetAttribute(Arrays.asList( - familyName, givenName, middleName, prefix, suffix))) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintableToName) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedFamily); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedGiven); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedMiddle); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedPrefix); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedSuffix); - builder.append(VCARD_COL_SEPARATOR); - - final String fullname = VCardUtils.constructNameFromElements( - VCardConfig.getNameOrderType(mVCardType), - encodedFamily, encodedMiddle, encodedGiven, encodedPrefix, encodedSuffix); - final boolean reallyUseQuotedPrintableToFullname = - mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname); + final String encodedFormattedname = + (reallyUseQuotedPrintableToFN ? + encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); - final String encodedFullname = - reallyUseQuotedPrintableToFullname ? - encodeQuotedPrintable(fullname) : - escapeCharacters(fullname); + builder.append(Constants.PROPERTY_N); + if (mIsDoCoMo) { + if (shouldAppendCharsetParameterToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + // DoCoMo phones require that all the elements in the "family name" field. + builder.append(formattedName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + } else { + if (shouldAppendCharsetParameterToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedFamily); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedGiven); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedMiddle); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPrefix); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedSuffix); + } + builder.append(VCARD_END_OF_LINE); // FN property - builder.append(VCARD_PROPERTY_FULL_NAME); - if (shouldAppendCharsetAttribute(encodedFullname)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_FN); + if (shouldAppendCharsetParameterToFN) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } - if (reallyUseQuotedPrintableToFullname) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + if (reallyUseQuotedPrintableToFN) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedFullname); - builder.append(VCARD_COL_SEPARATOR); + builder.append(encodedFormattedname); + builder.append(VCARD_END_OF_LINE); } else if (!TextUtils.isEmpty(displayName)) { final boolean reallyUseQuotedPrintableToDisplayName = - (mUsesQPToPrimaryProperties && + (!mRefrainsQPToPrimaryProperties && !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); final String encodedDisplayName = reallyUseQuotedPrintableToDisplayName ? encodeQuotedPrintable(displayName) : escapeCharacters(displayName); - builder.append(VCARD_PROPERTY_NAME); - if (shouldAppendCharsetAttribute(encodedDisplayName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_N); + if (shouldAppendCharsetParameter(displayName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintableToDisplayName) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedDisplayName); @@ -899,68 +850,85 @@ public class VCardComposer { builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); - } else if (mIsDoCoMo) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + builder.append(VCARD_END_OF_LINE); + if (mIsV30) { + builder.append(Constants.PROPERTY_FN); + // TODO: Not allowed formally... + if (shouldAppendCharsetParameter(displayName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedDisplayName); + builder.append(VCARD_END_OF_LINE); + } } else if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + // vCard 3.0 specification requires these fields. + appendVCardLine(builder, Constants.PROPERTY_N, ""); + appendVCardLine(builder, Constants.PROPERTY_FN, ""); + } else if (mIsDoCoMo) { + appendVCardLine(builder, Constants.PROPERTY_N, ""); } - String phoneticFamilyName = primaryContentValues - .getAsString(StructuredName.PHONETIC_FAMILY_NAME); - String phoneticMiddleName = primaryContentValues - .getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - String phoneticGivenName = primaryContentValues - .getAsString(StructuredName.PHONETIC_GIVEN_NAME); - if (!(TextUtils.isEmpty(phoneticFamilyName) - && TextUtils.isEmpty(phoneticMiddleName) && - TextUtils.isEmpty(phoneticGivenName))) { // if not empty - if (mIsJapaneseMobilePhone) { - phoneticFamilyName = VCardUtils - .toHalfWidthString(phoneticFamilyName); - phoneticMiddleName = VCardUtils - .toHalfWidthString(phoneticMiddleName); - phoneticGivenName = VCardUtils - .toHalfWidthString(phoneticGivenName); + final String phoneticFamilyName; + final String phoneticMiddleName; + final String phoneticGivenName; + { + String tmpPhoneticFamilyName = + primaryContentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); + String tmpPhoneticMiddleName = + primaryContentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + String tmpPhoneticGivenName = + primaryContentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); + if (mNeedsToConvertPhoneticString) { + phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); + phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); + phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); + } else { + phoneticFamilyName = tmpPhoneticFamilyName; + phoneticMiddleName = tmpPhoneticMiddleName; + phoneticGivenName = tmpPhoneticGivenName; } + } + if (!(TextUtils.isEmpty(phoneticFamilyName) + && TextUtils.isEmpty(phoneticMiddleName) + && TextUtils.isEmpty(phoneticGivenName))) { if (mIsV30) { final String sortString = VCardUtils .constructNameFromElements(mVCardType, - phoneticFamilyName, - phoneticMiddleName, - phoneticGivenName); - builder.append(VCARD_PROPERTY_SORT_STRING); + phoneticFamilyName, phoneticMiddleName, phoneticGivenName); + builder.append(Constants.PROPERTY_SORT_STRING); // Do not need to care about QP, since vCard 3.0 does not allow it. final String encodedSortString = escapeCharacters(sortString); - if (shouldAppendCharsetAttribute(encodedSortString)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + if (shouldAppendCharsetParameter(encodedSortString)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedSortString); - builder.append(VCARD_COL_SEPARATOR); - } else { + builder.append(VCARD_END_OF_LINE); + } else if (mIsJapaneseMobilePhone) { // Note: There is no appropriate property for expressing // phonetic name in vCard 2.1, while there is in // vCard 3.0 (SORT-STRING). - // We chose to use DoCoMo's way since it is supported by + // We chose to use DoCoMo's way when the device is Japanese one + // since it is supported by // a lot of Japanese mobile phones. This is "X-" property, so // any parser hopefully would not get confused with this. - builder.append(VCARD_PROPERTY_SOUND); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(Constants.PROPERTY_SOUND); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(Constants.PARAM_TYPE_X_IRMC_N); boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticFamilyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticMiddleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticGivenName))); + (!mRefrainsQPToPrimaryProperties + && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticFamilyName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticMiddleName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticGivenName))); final String encodedPhoneticFamilyName; final String encodedPhoneticMiddleName; @@ -975,11 +943,11 @@ public class VCardComposer { encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); } - if (shouldAppendCharsetAttribute(Arrays.asList( + if (shouldAppendCharsetParameters(Arrays.asList( encodedPhoneticFamilyName, encodedPhoneticMiddleName, encodedPhoneticGivenName))) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticFamilyName); @@ -989,89 +957,86 @@ public class VCardComposer { builder.append(encodedPhoneticMiddleName); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } } else if (mIsDoCoMo) { - builder.append(VCARD_PROPERTY_SOUND); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(Constants.PROPERTY_SOUND); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(Constants.PARAM_TYPE_X_IRMC_N); builder.append(VCARD_DATA_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } if (mUsesDefactProperty) { if (!TextUtils.isEmpty(phoneticGivenName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName); final String encodedPhoneticGivenName; if (reallyUseQuotedPrintable) { encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); } else { encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); } - builder.append(VCARD_PROPERTY_X_PHONETIC_FIRST_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticGivenName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_X_PHONETIC_FIRST_NAME); + if (shouldAppendCharsetParameter(encodedPhoneticGivenName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticGivenName); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } if (!TextUtils.isEmpty(phoneticMiddleName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName); final String encodedPhoneticMiddleName; if (reallyUseQuotedPrintable) { encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); } else { encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); } - builder.append(VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticMiddleName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME); + if (shouldAppendCharsetParameter(encodedPhoneticMiddleName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticMiddleName); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } if (!TextUtils.isEmpty(phoneticFamilyName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName); final String encodedPhoneticFamilyName; if (reallyUseQuotedPrintable) { encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); } else { encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); } - builder.append(VCARD_PROPERTY_X_PHONETIC_LAST_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticFamilyName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_X_PHONETIC_LAST_NAME); + if (shouldAppendCharsetParameter(encodedPhoneticFamilyName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticFamilyName); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } } } @@ -1080,45 +1045,31 @@ public class VCardComposer { final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Nickname.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - final String propertyNickname; - if (mIsV30) { - propertyNickname = VCARD_PROPERTY_NICKNAME; - } else if (mUsesAndroidProperty) { - propertyNickname = VCARD_PROPERTY_X_NICKNAME; - } else { - // There's no way to add this field. - return; - } - - for (ContentValues contentValues : contentValuesList) { - final String nickname = contentValues.getAsString(Nickname.NAME); - if (TextUtils.isEmpty(nickname)) { - continue; - } + if (contentValuesList == null) { + return; + } - final String encodedNickname; - final boolean reallyUseQuotedPrintable = - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); - if (reallyUseQuotedPrintable) { - encodedNickname = encodeQuotedPrintable(nickname); - } else { - encodedNickname = escapeCharacters(nickname); - } + final boolean useAndroidProperty; + if (mIsV30) { + useAndroidProperty = false; + } else if (mUsesAndroidProperty) { + useAndroidProperty = true; + } else { + // There's no way to add this field. + return; + } - builder.append(propertyNickname); - if (shouldAppendCharsetAttribute(propertyNickname)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedNickname); - builder.append(VCARD_COL_SEPARATOR); + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues.getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } + if (useAndroidProperty) { + appendAndroidSpecificProperty(builder, Nickname.CONTENT_ITEM_TYPE, + contentValues); + } else { + appendVCardLineWithCharsetAndQPDetection(builder, + Constants.PROPERTY_NICKNAME, nickname); } } } @@ -1133,6 +1084,9 @@ public class VCardComposer { for (ContentValues contentValues : contentValuesList) { final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); final String label = contentValues.getAsString(Phone.LABEL); + final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); String phoneNumber = contentValues.getAsString(Phone.NUMBER); if (phoneNumber != null) { phoneNumber = phoneNumber.trim(); @@ -1140,34 +1094,75 @@ public class VCardComposer { if (TextUtils.isEmpty(phoneNumber)) { continue; } - phoneLineExists = true; - int type = (typeAsObject != null ? typeAsObject : Phone.TYPE_HOME); - // TODO: Premature, since this allows two phone numbers which are - // same from the view of phone number format (e.g. "100" v.s. "1-0-0") - if (!phoneSet.contains(phoneNumber)) { - phoneSet.add(phoneNumber); - appendVCardTelephoneLine(builder, type, label, phoneNumber); + int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); + if (type == Phone.TYPE_PAGER) { + phoneLineExists = true; + if (!phoneSet.contains(phoneNumber)) { + phoneSet.add(phoneNumber); + appendVCardTelephoneLine(builder, type, label, phoneNumber, isPrimary); + } + } else { + // The entry "may" have several phone numbers when the contact entry is + // corrupted because of its original source. + // + // e.g. I encountered the entry like the following. + // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..." + // This kind of entry is not able to be inserted via Android devices, but + // possible if the source of the data is already corrupted. + List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber); + if (phoneNumberList.isEmpty()) { + continue; + } + phoneLineExists = true; + for (String actualPhoneNumber : phoneNumberList) { + if (!phoneSet.contains(actualPhoneNumber)) { + final int format = VCardUtils.getPhoneNumberFormat(mVCardType); + final String formattedPhoneNumber = + PhoneNumberUtils.formatNumber(actualPhoneNumber, format); + phoneSet.add(actualPhoneNumber); + appendVCardTelephoneLine(builder, type, label, + formattedPhoneNumber, isPrimary); + } + } } } } if (!phoneLineExists && mIsDoCoMo) { - appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); + appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", "", false); + } + } + + private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) { + List<String> phoneList = new ArrayList<String>(); + + StringBuilder builder = new StringBuilder(); + final int length = phoneNumber.length(); + for (int i = 0; i < length; i++) { + final char ch = phoneNumber.charAt(i); + if (Character.isDigit(ch)) { + builder.append(ch); + } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { + phoneList.add(builder.toString()); + builder = new StringBuilder(); + } } + if (builder.length() > 0) { + phoneList.add(builder.toString()); + } + + return phoneList; } private void appendEmails(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Email.CONTENT_ITEM_TYPE); + boolean emailAddressExists = false; if (contentValuesList != null) { - Set<String> addressSet = new HashSet<String>(); + final Set<String> addressSet = new HashSet<String>(); for (ContentValues contentValues : contentValuesList) { - Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); - final int type = (typeAsObject != null ? - typeAsObject : Email.TYPE_OTHER); - final String label = contentValues.getAsString(Email.LABEL); String emailAddress = contentValues.getAsString(Email.DATA); if (emailAddress != null) { emailAddress = emailAddress.trim(); @@ -1175,16 +1170,23 @@ public class VCardComposer { if (TextUtils.isEmpty(emailAddress)) { continue; } + Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); + final int type = (typeAsObject != null ? + typeAsObject : DEFAULT_EMAIL_TYPE); + final String label = contentValues.getAsString(Email.LABEL); + Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); emailAddressExists = true; if (!addressSet.contains(emailAddress)) { addressSet.add(emailAddress); - appendVCardEmailLine(builder, type, label, emailAddress); + appendVCardEmailLine(builder, type, label, emailAddress, isPrimary); } } } if (!emailAddressExists && mIsDoCoMo) { - appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); + appendVCardEmailLine(builder, Email.TYPE_HOME, "", "", false); } } @@ -1199,11 +1201,11 @@ public class VCardComposer { appendPostalsForGeneric(builder, contentValuesList); } } else if (mIsDoCoMo) { - builder.append(VCARD_PROPERTY_ADR); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_HOME); + builder.append(Constants.PROPERTY_ADR); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(Constants.PARAM_TYPE_HOME); builder.append(VCARD_DATA_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } } @@ -1241,7 +1243,10 @@ public class VCardComposer { final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); final String label = contentValues.getAsString(StructuredPostal.LABEL); if (type == preferedType) { - appendVCardPostalLine(builder, type, label, contentValues); + // Note: Not sure why we need to emit "empty" line even when actual + // data does not exist. There may be some reason or may not. + // We keep safer side since the previous implementation did so. + appendVCardPostalLine(builder, type, label, contentValues, true, true); return true; } } @@ -1251,11 +1256,18 @@ public class VCardComposer { private void appendPostalsForGeneric(final StringBuilder builder, final List<ContentValues> contentValuesList) { for (ContentValues contentValues : contentValuesList) { - final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); - final String label = contentValues.getAsString(StructuredPostal.LABEL); - if (type != null) { - appendVCardPostalLine(builder, type, label, contentValues); + if (contentValues == null) { + continue; } + final Integer typeAsObject = contentValues.getAsInteger(StructuredPostal.TYPE); + final int type = (typeAsObject != null ? + typeAsObject : DEFAULT_POSTAL_TYPE); + final String label = contentValues.getAsString(StructuredPostal.LABEL); + final Integer isPrimaryAsInteger = + contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + appendVCardPostalLine(builder, type, label, contentValues, isPrimary, false); } } @@ -1263,24 +1275,63 @@ public class VCardComposer { final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Im.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - Integer protocol = contentValues.getAsInteger(Im.PROTOCOL); - String data = contentValues.getAsString(Im.DATA); - if (data != null) { - data = data.trim(); - } - if (TextUtils.isEmpty(data)) { - continue; - } - - if (protocol != null && protocol == Im.PROTOCOL_GOOGLE_TALK) { - if (VCardConfig.usesAndroidSpecificProperty(mVCardType)) { - appendVCardLine(builder, Constants.PROPERTY_X_GOOGLE_TALK, data); + if (contentValuesList == null) { + return; + } + for (ContentValues contentValues : contentValuesList) { + final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); + if (protocolAsObject == null) { + continue; + } + final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); + if (propertyName == null) { + continue; + } + String data = contentValues.getAsString(Im.DATA); + if (data != null) { + data = data.trim(); + } + if (TextUtils.isEmpty(data)) { + continue; + } + final String typeAsString; + { + final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); + switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { + case Im.TYPE_HOME: { + typeAsString = Constants.PARAM_TYPE_HOME; + break; + } + case Im.TYPE_WORK: { + typeAsString = Constants.PARAM_TYPE_WORK; + break; + } + case Im.TYPE_CUSTOM: { + final String label = contentValues.getAsString(Im.LABEL); + typeAsString = (label != null ? "X-" + label : null); + break; + } + case Im.TYPE_OTHER: // Ignore + default: { + typeAsString = null; + break; } - // TODO: add "X-GOOGLE TALK" case... } } + + List<String> parameterList = new ArrayList<String>(); + if (!TextUtils.isEmpty(typeAsString)) { + parameterList.add(typeAsString); + } + final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + if (isPrimary) { + parameterList.add(Constants.PARAM_TYPE_PREF); + } + + appendVCardLineWithCharsetAndQPDetection( + builder, propertyName, parameterList, data); } } @@ -1288,39 +1339,84 @@ public class VCardComposer { final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Website.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String website = contentValues.getAsString(Website.URL); - if (website != null) { - website = website.trim(); - } - if (!TextUtils.isEmpty(website)) { - appendVCardLine(builder, VCARD_PROPERTY_URL, website); - } + if (contentValuesList == null) { + return; + } + for (ContentValues contentValues : contentValuesList) { + String website = contentValues.getAsString(Website.URL); + if (website != null) { + website = website.trim(); + } + + // Note: vCard 3.0 does not allow any parameter addition toward "URL" + // property, while there's no document in vCard 2.1. + // + // TODO: Should we allow adding it when appropriate? + // (Actually, we drop some data. Using "group.X-URL-TYPE" or something + // may help) + if (!TextUtils.isEmpty(website)) { + appendVCardLine(builder, Constants.PROPERTY_URL, website); } } } + /** + * Theoretically, there must be only one birthday for each vCard entry. + * Also, we are afraid of some importer's parse error during its import. + * We emit only one birthday entry even when there are more than one. + */ private void appendBirthday(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Event.CONTENT_ITEM_TYPE); - if (contentValuesList != null && contentValuesList.size() > 0) { - Integer eventType = contentValuesList.get(0).getAsInteger(Event.TYPE); - if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { - return; + final List<ContentValues> contentValuesList = + contentValuesListMap.get(Event.CONTENT_ITEM_TYPE); + if (contentValuesList == null) { + return; + } + String primaryBirthday = null; + String secondaryBirthday = null; + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; } - // Theoretically, there must be only one birthday for each vCard data and - // we are afraid of some parse error occuring in some devices, so - // we emit only one birthday entry for now. - String birthday = contentValuesList.get(0).getAsString(Event.START_DATE); - if (birthday != null) { - birthday = birthday.trim(); + final Integer eventType = contentValues.getAsInteger(Event.TYPE); + if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { + continue; + } + final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); + if (birthdayCandidate == null) { + continue; + } + final Integer isSuperPrimaryAsInteger = + contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); + final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? + (isSuperPrimaryAsInteger > 0) : false); + if (isSuperPrimary) { + // "super primary" birthday should the prefered one. + primaryBirthday = birthdayCandidate; + break; } - if (!TextUtils.isEmpty(birthday)) { - appendVCardLine(builder, VCARD_PROPERTY_BIRTHDAY, birthday); + final Integer isPrimaryAsInteger = + contentValues.getAsInteger(Event.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + if (isPrimary) { + // We don't break here since "super primary" birthday may exist later. + primaryBirthday = birthdayCandidate; + } else if (secondaryBirthday == null) { + // First entry is set to the "secondary" candidate. + secondaryBirthday = birthdayCandidate; } } + + final String birthday; + if (primaryBirthday != null) { + birthday = primaryBirthday.trim(); + } else if (secondaryBirthday != null){ + birthday = secondaryBirthday.trim(); + } else { + return; + } + appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_BDAY, birthday); } private void appendOrganizations(final StringBuilder builder, @@ -1329,25 +1425,37 @@ public class VCardComposer { .get(Organization.CONTENT_ITEM_TYPE); if (contentValuesList != null) { for (ContentValues contentValues : contentValuesList) { - String company = contentValues - .getAsString(Organization.COMPANY); + String company = contentValues.getAsString(Organization.COMPANY); if (company != null) { company = company.trim(); } - String title = contentValues - .getAsString(Organization.TITLE); + String department = contentValues.getAsString(Organization.DEPARTMENT); + if (department != null) { + department = department.trim(); + } + String title = contentValues.getAsString(Organization.TITLE); if (title != null) { title = title.trim(); } + StringBuilder orgBuilder = new StringBuilder(); if (!TextUtils.isEmpty(company)) { - appendVCardLine(builder, VCARD_PROPERTY_ORG, company, - !VCardUtils.containsOnlyPrintableAscii(company), - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(company))); + orgBuilder.append(company); + } + if (!TextUtils.isEmpty(department)) { + if (orgBuilder.length() > 0) { + orgBuilder.append(';'); + } + orgBuilder.append(department); } + final String orgline = orgBuilder.toString(); + appendVCardLine(builder, Constants.PROPERTY_ORG, orgline, + !VCardUtils.containsOnlyPrintableAscii(orgline), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); + if (!TextUtils.isEmpty(title)) { - appendVCardLine(builder, VCARD_PROPERTY_TITLE, title, + appendVCardLine(builder, Constants.PROPERTY_TITLE, title, !VCardUtils.containsOnlyPrintableAscii(title), (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); @@ -1374,11 +1482,9 @@ public class VCardComposer { photoType = "GIF"; } else if (data.length >= 4 && data[0] == (byte) 0x89 && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') { - // Note: vCard 2.1 officially does not support PNG, but we - // may have it - // and using X- word like "X-PNG" may not let importers know - // it is - // PNG. So we use the String "PNG" as is... + // Note: vCard 2.1 officially does not support PNG, but we may + // have it and using X- word like "X-PNG" may not let importers + // know it is PNG. So we use the String "PNG" as is... photoType = "PNG"; } else if (data.length >= 2 && data[0] == (byte) 0xff && data[1] == (byte) 0xd8) { @@ -1425,7 +1531,7 @@ public class VCardComposer { final boolean reallyUseQuotedPrintable = (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr, shouldAppendCharsetInfo, reallyUseQuotedPrintable); } else { for (ContentValues contentValues : contentValuesList) { @@ -1436,7 +1542,7 @@ public class VCardComposer { final boolean reallyUseQuotedPrintable = (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr, shouldAppendCharsetInfo, reallyUseQuotedPrintable); } } @@ -1444,6 +1550,33 @@ public class VCardComposer { } } + private void appendAndroidSpecificProperty(final StringBuilder builder, + final String mimeType, ContentValues contentValues) { + List<String> rawDataList = new ArrayList<String>(); + rawDataList.add(mimeType); + final List<String> columnNameList; + if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { + + } else { + // If you add the other field, please check all the columns are able to be + // converted to String. + // + // e.g. BLOB is not what we can handle here now. + return; + } + + for (int i = 1; i <= Constants.MAX_DATA_COLUMN; i++) { + String value = contentValues.getAsString("data" + i); + if (value == null) { + value = ""; + } + rawDataList.add(value); + } + + appendVCardLineWithCharsetAndQPDetection(builder, + Constants.PROPERTY_X_ANDROID_CUSTOM, rawDataList); + } + /** * Append '\' to the characters which should be escaped. The character set is different * not only between vCard 2.1 and vCard 3.0 but also among each device. @@ -1459,7 +1592,7 @@ public class VCardComposer { final StringBuilder tmpBuilder = new StringBuilder(); final int length = unescaped.length(); for (int i = 0; i < length; i++) { - char ch = unescaped.charAt(i); + final char ch = unescaped.charAt(i); switch (ch) { case ';': { tmpBuilder.append('\\'); @@ -1470,7 +1603,7 @@ public class VCardComposer { if (i + 1 < length) { char nextChar = unescaped.charAt(i); if (nextChar == '\n') { - continue; + break; } else { // fall through } @@ -1522,15 +1655,15 @@ public class VCardComposer { private void appendVCardPhotoLine(final StringBuilder builder, final String encodedData, final String photoType) { StringBuilder tmpBuilder = new StringBuilder(); - tmpBuilder.append(VCARD_PROPERTY_PHOTO); - tmpBuilder.append(VCARD_ATTR_SEPARATOR); + tmpBuilder.append(Constants.PROPERTY_PHOTO); + tmpBuilder.append(VCARD_PARAM_SEPARATOR); if (mIsV30) { - tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30); + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); } else { - tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21); + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); } - tmpBuilder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(tmpBuilder, photoType); + tmpBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameter(tmpBuilder, photoType); tmpBuilder.append(VCARD_DATA_SEPARATOR); tmpBuilder.append(encodedData); @@ -1542,69 +1675,136 @@ public class VCardComposer { tmpBuilder.append(tmpStr.charAt(i)); lineCount++; if (lineCount > 72) { - tmpBuilder.append(VCARD_COL_SEPARATOR); + tmpBuilder.append(VCARD_END_OF_LINE); tmpBuilder.append(VCARD_WS); lineCount = 0; } } builder.append(tmpBuilder.toString()); - builder.append(VCARD_COL_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); + builder.append(VCARD_END_OF_LINE); } - private void appendVCardPostalLine(final StringBuilder builder, - final Integer typeAsObject, final String label, - final ContentValues contentValues) { - builder.append(VCARD_PROPERTY_ADR); - builder.append(VCARD_ATTR_SEPARATOR); + private class PostalStruct { + final boolean reallyUseQuotedPrintable; + final boolean appendCharset; + final String addressData; + public PostalStruct(final boolean reallyUseQuotedPrintable, + final boolean appendCharset, final String addressData) { + this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; + this.appendCharset = appendCharset; + this.addressData = addressData; + } + } - // Note: Not sure why we need to emit "empty" line even when actual data does not exist. - // There may be some reason or may not be any. We keep safer side. - // TODO: investigate this. - boolean dataExists = false; + /** + * @return null when there's no information available to construct the data. + */ + private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { + boolean reallyUseQuotedPrintable = false; + boolean appendCharset = false; + + boolean dataArrayExists = false; String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); - boolean actuallyUseQuotedPrintable = false; - boolean shouldAppendCharset = false; for (String data : dataArray) { if (!TextUtils.isEmpty(data)) { - dataExists = true; - if (!shouldAppendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { - shouldAppendCharset = true; + dataArrayExists = true; + if (!appendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { + appendCharset = true; } if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { - actuallyUseQuotedPrintable = true; + reallyUseQuotedPrintable = true; break; } } } - int length = dataArray.length; - for (int i = 0; i < length; i++) { - String data = dataArray[i]; - if (!TextUtils.isEmpty(data)) { - if (actuallyUseQuotedPrintable) { - dataArray[i] = encodeQuotedPrintable(data); + if (dataArrayExists) { + StringBuffer addressBuffer = new StringBuffer(); + boolean first = true; + for (String data : dataArray) { + if (first) { + first = false; } else { - dataArray[i] = escapeCharacters(data); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + } + if (!TextUtils.isEmpty(data)) { + if (reallyUseQuotedPrintable) { + addressBuffer.append(encodeQuotedPrintable(data)); + } else { + addressBuffer.append(escapeCharacters(data)); + } } } + return new PostalStruct(reallyUseQuotedPrintable, appendCharset, + addressBuffer.toString()); } - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = StructuredPostal.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; + String formattedAddress = + contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); + if (!TextUtils.isEmpty(formattedAddress)) { + reallyUseQuotedPrintable = + !VCardUtils.containsOnlyPrintableAscii(formattedAddress); + appendCharset = + !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedAddress); + if (reallyUseQuotedPrintable) { + formattedAddress = encodeQuotedPrintable(formattedAddress); + } else { + formattedAddress = escapeCharacters(formattedAddress); + } + // We use the second value ("Extended Address"). + // + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name + StringBuffer addressBuffer = new StringBuffer(); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(formattedAddress); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + return new PostalStruct( + reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); + } + return null; // There's no data available. + } + + private void appendVCardPostalLine(final StringBuilder builder, + final int type, final String label, final ContentValues contentValues, + final boolean isPrimary, final boolean emitLineEveryTime) { + final boolean reallyUseQuotedPrintable; + final boolean appendCharset; + final String addressData; + { + PostalStruct postalStruct = tryConstructPostalStruct(contentValues); + if (postalStruct == null) { + if (emitLineEveryTime) { + reallyUseQuotedPrintable = false; + appendCharset = false; + addressData = ""; + } else { + return; + } + } else { + reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; + appendCharset = postalStruct.appendCharset; + addressData = postalStruct.addressData; + } } - String typeAsString = null; - switch (typeAsPrimitive) { + List<String> parameterList = new ArrayList<String>(); + if (isPrimary) { + parameterList.add(Constants.PARAM_TYPE_PREF); + } + switch (type) { case StructuredPostal.TYPE_HOME: { - typeAsString = Constants.ATTR_TYPE_HOME; + parameterList.add(Constants.PARAM_TYPE_HOME); break; } case StructuredPostal.TYPE_WORK: { - typeAsString = Constants.ATTR_TYPE_WORK; + parameterList.add(Constants.PARAM_TYPE_WORK); break; } case StructuredPostal.TYPE_CUSTOM: { @@ -1614,9 +1814,7 @@ public class VCardComposer { // ("IANA-token" in the vCard 3.0 is unclear...) // Just for safety, we add "X-" at the beggining of each label. // Also checks the label obeys with vCard 3.0 spec. - builder.append("X-"); - builder.append(label); - builder.append(VCARD_DATA_SEPARATOR); + parameterList.add("X-" + label); } break; } @@ -1624,132 +1822,111 @@ public class VCardComposer { break; } default: { - Log.e(LOG_TAG, "Unknown StructuredPostal type: " + typeAsPrimitive); + Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); break; } } - // Attribute(s). + // Actual data construction starts from here. + // TODO: add a new version of appendVCardLine() for this purpose. + + builder.append(Constants.PROPERTY_ADR); + builder.append(VCARD_PARAM_SEPARATOR); + // Parameters { - boolean shouldAppendAttrSeparator = false; - if (typeAsString != null) { - appendTypeAttribute(builder, typeAsString); - shouldAppendAttrSeparator = true; - } - - if (dataExists) { - if (shouldAppendCharset) { - // Strictly, vCard 3.0 does not allow exporters to emit charset information, - // but we will add it since the information should be useful for importers, - // - // Assume no parser does not emit error with this attribute in vCard 3.0. - if (shouldAppendAttrSeparator) { - builder.append(VCARD_ATTR_SEPARATOR); - } - builder.append(mVCardAttributeCharset); - shouldAppendAttrSeparator = true; + boolean shouldAppendParamSeparator = false; + if (!parameterList.isEmpty()) { + appendTypeParameters(builder, parameterList); + shouldAppendParamSeparator = true; + } + + if (appendCharset) { + // Strictly, vCard 3.0 does not allow exporters to emit charset information, + // but we will add it since the information should be useful for importers, + // + // Assume no parser does not emit error with this parameter in vCard 3.0. + if (shouldAppendParamSeparator) { + builder.append(VCARD_PARAM_SEPARATOR); } + builder.append(mVCardCharsetParameter); + shouldAppendParamSeparator = true; + } - if (actuallyUseQuotedPrintable) { - if (shouldAppendAttrSeparator) { - builder.append(VCARD_ATTR_SEPARATOR); - } - builder.append(VCARD_ATTR_ENCODING_QP); - shouldAppendAttrSeparator = true; + if (reallyUseQuotedPrintable) { + if (shouldAppendParamSeparator) { + builder.append(VCARD_PARAM_SEPARATOR); } + builder.append(VCARD_PARAM_ENCODING_QP); + shouldAppendParamSeparator = true; } } - // Property values. - builder.append(VCARD_DATA_SEPARATOR); - if (dataExists) { - // The elements in dataArray are already encoded to quoted printable - // if needed. - // See above. - // - // TODO: in vCard 3.0, one line may become too huge. Fix this. - builder.append(dataArray[0]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[1]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[2]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[3]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[4]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[5]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[6]); - } - builder.append(VCARD_COL_SEPARATOR); + builder.append(addressData); + builder.append(VCARD_END_OF_LINE); } private void appendVCardEmailLine(final StringBuilder builder, - final Integer typeAsObject, final String label, final String data) { - builder.append(VCARD_PROPERTY_EMAIL); - - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = Email.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; - } - + final int type, final String label, + final String rawData, final boolean isPrimary) { final String typeAsString; - switch (typeAsPrimitive) { + switch (type) { case Email.TYPE_CUSTOM: { // For backward compatibility. // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. // To support mobile type at that time, this custom label had been used. if (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME .equals(label)) { - typeAsString = Constants.ATTR_TYPE_CELL; + typeAsString = Constants.PARAM_TYPE_CELL; } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { typeAsString = "X-" + label; } else { - typeAsString = DEFAULT_EMAIL_TYPE; + typeAsString = null; } break; } case Email.TYPE_HOME: { - typeAsString = Constants.ATTR_TYPE_HOME; + typeAsString = Constants.PARAM_TYPE_HOME; break; } case Email.TYPE_WORK: { - typeAsString = Constants.ATTR_TYPE_WORK; + typeAsString = Constants.PARAM_TYPE_WORK; break; } case Email.TYPE_OTHER: { - typeAsString = DEFAULT_EMAIL_TYPE; + typeAsString = null; break; } case Email.TYPE_MOBILE: { - typeAsString = Constants.ATTR_TYPE_CELL; + typeAsString = Constants.PARAM_TYPE_CELL; break; } default: { - Log.e(LOG_TAG, "Unknown Email type: " + typeAsPrimitive); - typeAsString = DEFAULT_EMAIL_TYPE; + Log.e(LOG_TAG, "Unknown Email type: " + type); + typeAsString = null; break; } } - builder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(builder, typeAsString); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(data); - builder.append(VCARD_COL_SEPARATOR); + final List<String> parameterList = new ArrayList<String>(); + if (isPrimary) { + parameterList.add(Constants.PARAM_TYPE_PREF); + } + if (!TextUtils.isEmpty(typeAsString)) { + parameterList.add(typeAsString); + } + + appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_EMAIL, + parameterList, rawData); } private void appendVCardTelephoneLine(final StringBuilder builder, final Integer typeAsObject, final String label, - String encodedData) { - builder.append(VCARD_PROPERTY_TEL); - builder.append(VCARD_ATTR_SEPARATOR); + final String encodedData, boolean isPrimary) { + builder.append(Constants.PROPERTY_TEL); + builder.append(VCARD_PARAM_SEPARATOR); final int typeAsPrimitive; if (typeAsObject == null) { @@ -1758,56 +1935,105 @@ public class VCardComposer { typeAsPrimitive = typeAsObject; } + ArrayList<String> parameterList = new ArrayList<String>(); switch (typeAsPrimitive) { case Phone.TYPE_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_HOME)); break; case Phone.TYPE_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_WORK)); break; case Phone.TYPE_FAX_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_HOME, Constants.PARAM_TYPE_FAX)); break; case Phone.TYPE_FAX_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_FAX)); break; case Phone.TYPE_MOBILE: - builder.append(Constants.ATTR_TYPE_CELL); + parameterList.add(Constants.PARAM_TYPE_CELL); break; case Phone.TYPE_PAGER: if (mIsDoCoMo) { // Not sure about the reason, but previous implementation had // used "VOICE" instead of "PAGER" - // Also, refrain from using appendType() so that "TYPE=" is never be appended. - builder.append(Constants.ATTR_TYPE_VOICE); + parameterList.add(Constants.PARAM_TYPE_VOICE); } else { - appendTypeAttribute(builder, Constants.ATTR_TYPE_PAGER); + parameterList.add(Constants.PARAM_TYPE_PAGER); } break; case Phone.TYPE_OTHER: - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + parameterList.add(Constants.PARAM_TYPE_VOICE); + break; + case Phone.TYPE_CAR: + parameterList.add(Constants.PARAM_TYPE_CAR); + break; + case Phone.TYPE_COMPANY_MAIN: + // There's no relevant field in vCard (at least 2.1). + parameterList.add(Constants.PARAM_TYPE_WORK); + isPrimary = true; + break; + case Phone.TYPE_ISDN: + parameterList.add(Constants.PARAM_TYPE_ISDN); + break; + case Phone.TYPE_MAIN: + isPrimary = true; + break; + case Phone.TYPE_OTHER_FAX: + parameterList.add(Constants.PARAM_TYPE_FAX); + break; + case Phone.TYPE_TELEX: + parameterList.add(Constants.PARAM_TYPE_TLX); + break; + case Phone.TYPE_WORK_MOBILE: + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_CELL)); + break; + case Phone.TYPE_WORK_PAGER: + parameterList.add(Constants.PARAM_TYPE_WORK); + // See above. + if (mIsDoCoMo) { + parameterList.add(Constants.PARAM_TYPE_VOICE); + } else { + parameterList.add(Constants.PARAM_TYPE_PAGER); + } + break; + case Phone.TYPE_MMS: + parameterList.add(Constants.PARAM_TYPE_MSG); break; case Phone.TYPE_CUSTOM: if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - appendTypeAttribute(builder, "X-" + label); + // Note: Strictly, vCard 2.1 does not allow "X-" parameter without + // "TYPE=" string. + parameterList.add("X-" + label); } else { // Just ignore the custom type. - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + parameterList.add(Constants.PARAM_TYPE_VOICE); } break; + case Phone.TYPE_RADIO: + case Phone.TYPE_TTY_TDD: default: - appendUncommonPhoneType(builder, typeAsPrimitive); break; } + if (isPrimary) { + parameterList.add(Constants.PARAM_TYPE_PREF); + } + + if (parameterList.isEmpty()) { + appendUncommonPhoneType(builder, typeAsPrimitive); + } else { + appendTypeParameters(builder, parameterList); + } + builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedData); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } /** @@ -1817,35 +2043,65 @@ public class VCardComposer { if (mIsDoCoMo) { // The previous implementation for DoCoMo had been conservative // about miscellaneous types. - builder.append(Constants.ATTR_TYPE_VOICE); + builder.append(Constants.PARAM_TYPE_VOICE); } else { - String phoneAttribute = VCardUtils.getPhoneAttributeString(type); - if (phoneAttribute != null) { - appendTypeAttribute(builder, phoneAttribute); + String phoneType = VCardUtils.getPhoneTypeString(type); + if (phoneType != null) { + appendTypeParameter(builder, phoneType); } else { Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); } } } + // appendVCardLine() variants accepting one String. + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, final String rawData) { + appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawData); + } + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, final String rawData) { + final boolean needCharset = + (mUsesQuotedPrintable && !VCardUtils.containsOnlyPrintableAscii(rawData)); + final boolean reallyUseQuotedPrintable = + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData); + appendVCardLine(builder, propertyName, parameterList, + rawData, needCharset, reallyUseQuotedPrintable); + } + private void appendVCardLine(final StringBuilder builder, final String propertyName, final String rawData) { appendVCardLine(builder, propertyName, rawData, false, false); } private void appendVCardLine(final StringBuilder builder, - final String field, final String rawData, final boolean needCharset, + final String propertyName, final String rawData, final boolean needCharset, boolean needQuotedPrintable) { - builder.append(field); + appendVCardLine(builder, propertyName, null, rawData, needCharset, needQuotedPrintable); + } + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, + final String rawData, final boolean needCharset, + boolean needQuotedPrintable) { + builder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + builder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(builder, parameterList); + } if (needCharset) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } final String encodedData; if (needQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); encodedData = encodeQuotedPrintable(rawData); } else { // TODO: one line may be too huge, which may be invalid in vCard spec, though @@ -1855,10 +2111,94 @@ public class VCardComposer { builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedData); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); + } + + // appendVCardLine() variants accepting List<String>. + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, final List<String> rawDataList) { + appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawDataList); + } + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, final List<String> rawDataList) { + boolean needCharset = false; + boolean reallyUseQuotedPrintable = false; + for (String rawData : rawDataList) { + if (!needCharset && mUsesQuotedPrintable && + !VCardUtils.containsOnlyPrintableAscii(rawData)) { + needCharset = true; + } + if (!reallyUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData)) { + reallyUseQuotedPrintable = true; + } + if (needCharset && reallyUseQuotedPrintable) { + break; + } + } + + appendVCardLine(builder, propertyName, parameterList, + rawDataList, needCharset, reallyUseQuotedPrintable); + } + + /* + private void appendVCardLine(final StringBuilder builder, + final String propertyName, final List<String> rawDataList) { + appendVCardLine(builder, propertyName, rawDataList, false, false); } - private void appendTypeAttributes(final StringBuilder builder, + private void appendVCardLine(final StringBuilder builder, + final String propertyName, final List<String> rawDataList, + final boolean needCharset, boolean needQuotedPrintable) { + appendVCardLine(builder, propertyName, null, rawDataList, needCharset, needQuotedPrintable); + }*/ + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, + final List<String> rawDataList, final boolean needCharset, + boolean needQuotedPrintable) { + builder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + builder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(builder, parameterList); + } + if (needCharset) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + + builder.append(VCARD_DATA_SEPARATOR); + boolean first = true; + for (String rawData : rawDataList) { + final String encodedData; + if (needQuotedPrintable) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); + encodedData = encodeQuotedPrintable(rawData); + } else { + // TODO: one line may be too huge, which may be invalid in vCard spec, though + // several (even well-known) applications do not care this. + encodedData = escapeCharacters(rawData); + } + + if (first) { + first = false; + } else { + builder.append(VCARD_ITEM_SEPARATOR); + } + builder.append(encodedData); + } + builder.append(VCARD_END_OF_LINE); + } + + /** + * VCARD_PARAM_SEPARATOR must be appended before this method being called. + */ + private void appendTypeParameters(final StringBuilder builder, final List<String> types) { // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. @@ -1867,47 +2207,54 @@ public class VCardComposer { if (first) { first = false; } else { - builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_PARAM_SEPARATOR); } - appendTypeAttribute(builder, type); + appendTypeParameter(builder, type); } } - private void appendTypeAttribute(final StringBuilder builder, final String type) { + /** + * VCARD_PARAM_SEPARATOR must be appended before this method being called. + */ + private void appendTypeParameter(final StringBuilder builder, final String type) { + // Refrain from using appendType() so that "TYPE=" is not be appended when the + // device is DoCoMo's (just for safety). + // // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" - if (mIsV30) { - builder.append(Constants.ATTR_TYPE).append(VCARD_ATTR_EQUAL); + if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { + builder.append(Constants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); } builder.append(type); } /** - * Returns true when the property line should contain charset attribute + * Returns true when the property line should contain charset parameter * information. This method may return true even when vCard version is 3.0. * * Strictly, adding charset information is invalid in VCard 3.0. - * However we'll add the info only when used charset is not UTF-8 + * However we'll add the info only when charset we use is not UTF-8 * in vCard 3.0 format, since parser side may be able to use the charset - * via this field, though we may encounter another problem by adding it... + * via this field, though we may encounter another problem by adding it. * * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 * recommends UTF-8. By adding this field, parsers may be able * to know this text is NOT UTF-8 but Shift_Jis. */ - private boolean shouldAppendCharsetAttribute(final String propertyValue) { + private boolean shouldAppendCharsetParameter(final String propertyValue) { return (!VCardUtils.containsOnlyPrintableAscii(propertyValue) && (!mIsV30 || !mUsesUtf8)); } - private boolean shouldAppendCharsetAttribute(final List<String> propertyValueList) { - boolean shouldAppendBasically = false; + private boolean shouldAppendCharsetParameters(final List<String> propertyValueList) { + if (mIsV30 && mUsesUtf8) { + return false; + } for (String propertyValue : propertyValueList) { if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { - shouldAppendBasically = true; - break; + return true; } } - return shouldAppendBasically && (!mIsV30 || !mUsesUtf8); + return false; } private String encodeQuotedPrintable(String str) { @@ -1966,4 +2313,116 @@ public class VCardComposer { return tmpBuilder.toString(); } + + //// The methods bellow are for call log history //// + + /** + * This static function is to compose vCard for phone own number + */ + public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, + String phoneNumber, boolean vcardVer21) { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (!vcardVer21) { + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); + } + + boolean needCharset = false; + if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { + needCharset = true; + } + appendVCardLine(builder, Constants.PROPERTY_FN, phoneName, needCharset, false); + appendVCardLine(builder, Constants.PROPERTY_N, phoneName, needCharset, false); + + String label = Integer.toString(phonetype); + appendVCardTelephoneLine(builder, phonetype, label, phoneNumber, false); + + appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); + + return builder.toString(); + } + + /** + * Format according to RFC 2445 DATETIME type. + * The format is: ("%Y%m%dT%H%M%SZ"). + */ + private final String toRfc2455Format(final long millSecs) { + Time startDate = new Time(); + startDate.set(millSecs); + String date = startDate.format2445(); + return date + FLAG_TIMEZONE_UTC; + } + + /** + * Try to append the property line for a call history time stamp field if possible. + * Do nothing if the call log type gotton from the database is invalid. + */ + private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) { + // Extension for call history as defined in + // in the Specification for Ic Mobile Communcation - ver 1.1, + // Oct 2000. This is used to send the details of the call + // history - missed, incoming, outgoing along with date and time + // to the requesting device (For example, transferring phone book + // when connected over bluetooth) + // + // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" + final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); + final String callLogTypeStr; + switch (callLogType) { + case Calls.INCOMING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; + break; + } + case Calls.OUTGOING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; + break; + } + case Calls.MISSED_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; + break; + } + default: { + Log.w(LOG_TAG, "Call log type not correct."); + return; + } + } + + final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); + builder.append(VCARD_PROPERTY_X_TIMESTAMP); + builder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameter(builder, callLogTypeStr); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(toRfc2455Format(dateAsLong)); + builder.append(VCARD_END_OF_LINE); + } + + private String createOneCallLogEntryInternal() { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); + } + String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); + if (TextUtils.isEmpty(name)) { + name = mCursor.getString(NUMBER_COLUMN_INDEX); + } + final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); + appendVCardLine(builder, Constants.PROPERTY_FN, name, needCharset, false); + appendVCardLine(builder, Constants.PROPERTY_N, name, needCharset, false); + + String number = mCursor.getString(NUMBER_COLUMN_INDEX); + int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); + String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); + if (TextUtils.isEmpty(label)) { + label = Integer.toString(type); + } + appendVCardTelephoneLine(builder, type, label, number, false); + tryAppendCallHistoryTimeStampField(builder); + appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); + return builder.toString(); + } } diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java index 68cd0df..545c09b 100644 --- a/core/java/android/pim/vcard/VCardConfig.java +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -15,14 +15,20 @@ */ package android.pim.vcard; +import android.util.Log; + import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * The class representing VCard related configurations. Useful static methods are not in this class * but in VCardUtils. */ public class VCardConfig { + private static final String LOG_TAG = "vcard.VCardConfig"; + // TODO: may be better to make the instance of this available and stop using static methods and // one integer. @@ -41,8 +47,8 @@ public class VCardConfig { // TODO: make the other codes use this flag public static final boolean IGNORE_CASE_EXCEPT_VALUE = true; - private static final int FLAG_V21 = 0; - private static final int FLAG_V30 = 1; + public static final int FLAG_V21 = 0; + public static final int FLAG_V30 = 1; // 0x2 is reserved for the future use ... @@ -95,96 +101,187 @@ public class VCardConfig { private static final int FLAG_DOCOMO = 0x20000000; /** - * The flag indicating the vCard composer use Quoted-Printable toward even "primary" types. - * In this context, "primary" types means "N", "FN", etc. which are usually "not" encoded - * into Quoted-Printable format in external exporters. - * This flag is useful when some target importer does not accept "primary" property values - * without Quoted-Printable encoding. + * <P> + * The flag indicating the vCard composer does "NOT" use Quoted-Printable toward "primary" + * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0). + * </P> + * <P> + * We actually cannot define what is the "primary" property. Note that this is NOT defined + * in vCard specification either. Also be aware that it is NOT related to "primary" notion + * used in {@link android.provider.ContactsContract}. + * This notion is just for vCard composition in Android. + * </P> + * <P> + * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1 + * do NOT use Quoted-Printable encoding toward some properties like "N", "FN", etc. even when + * their values contain non-ascii or/and CR/LF, while they use the encoding in the other + * properties like "ADR", "ORG", etc. + * <P> + * We are afraid of the case where some vCard importer also forget handling QP presuming QP is + * not used in such fields. + * </P> + * <P> + * This flag is useful when some target importer you are going to focus on does not accept + * such "primary" property values with Quoted-Printable encoding. + * </P> + * <P> + * Again, we should not use this flag at all for complying vCard 2.1 spec. + * </P> + * <P> + * We will change the behavior around this flag in the future, after understanding the other + * real vCard cases around this problem. Please use this flag with extreme caution even when + * needed. + * </P> + * <P> + * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this + * kind of problem (hopefully). + * </P> + */ + public static final int FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES = 0x10000000; + + /** + * <P> + * The flag indicating that phonetic name related fields must be converted to + * appropriate form. Note that "appropriate" is not defined in any vCard specification. + * This is Android-specific. + * </P> + * <P> + * One typical (and currently sole) example where we need this flag is the time when + * we need to emit Japanese phonetic names into vCard entries. The property values + * should be encoded into half-width katakana when the target importer is Japanese mobile + * phones', which are probably not able to parse full-width hiragana/katakana for + * historical reasons, while the vCard importers embedded to softwares for PC should be + * able to parse them as we expect. + * </P> + */ + public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x0800000; + + /** + * <P> + * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string every time + * possible. The default behavior does not emit it and is valid, while adding "TYPE=" + * is also valid. In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in + * vCard 3.0 specification. + * + * If you are targeting to some importer which cannot accept type parameters + * without "TYPE=" string (which should be rare though), please use this flag. * - * @hide Temporaly made public. We don't strictly define "primary", so we may change the - * behavior around this flag in the future. Do not use this flag without any reason. + * XXX: Really rare? + * + * e.g. int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM); */ - public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000; - - // VCard types + public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; + + //// The followings are VCard types available from importer/exporter. //// /** + * <P> * General vCard format with the version 2.1. Uses UTF-8 for the charset. - * When composing a vCard entry, the US convension will be used. - * + * When composing a vCard entry, the US convension will be used toward formatting + * some values + * </P> + * <P> * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", - * while in Japan, it should be "Prefix Family Middle Given Suffix". + * while it should be "Prefix Family Middle Given Suffix" in Japan. + * </P> */ - public static final int VCARD_TYPE_V21_GENERIC = + public static final int VCARD_TYPE_V21_GENERIC_UTF8 = (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic"; + /* package */ static String VCARD_TYPE_V21_GENERIC_UTF8_STR = "v21_generic"; /** + * <P> * General vCard format with the version 3.0. Uses UTF-8 for the charset. - * - * Note that this type is not fully implemented, so probably some bugs remain both in - * parsing and composing. - * - * TODO: implement this type correctly. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_GENERIC = + public static final int VCARD_TYPE_V30_GENERIC_UTF8 = (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic"; + /* package */ static final String VCARD_TYPE_V30_GENERIC_UTF8_STR = "v30_generic"; /** - * General vCard format with the version 2.1 with some Europe convension. Uses Utf-8. + * <P> + * General vCard format for the vCard 2.1 with some Europe convension. Uses Utf-8. * Currently, only name order is considered ("Prefix Middle Given Family Suffix") + * </P> */ - public static final int VCARD_TYPE_V21_EUROPE = + public static final int VCARD_TYPE_V21_EUROPE_UTF8 = (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V21_EUROPE_STR = "v21_europe"; + /* package */ static final String VCARD_TYPE_V21_EUROPE_UTF8_STR = "v21_europe"; /** + * <P> * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8 + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_EUROPE = + public static final int VCARD_TYPE_V30_EUROPE_UTF8 = (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; - - /** - * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for - * parsing/composing the vCard data. - */ - public static final int VCARD_TYPE_V21_JAPANESE = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese"; - /** - * vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * <P> + * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; + + /** + * <P> + * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for + * parsing/composing the vCard data. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> + */ + public static final int VCARD_TYPE_V21_JAPANESE_SJIS = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_SJIS_STR = "v21_japanese_sjis"; /** + * <P> * vCard format for miscellaneous Japanese devices, using Shift_Jis for * parsing/composing the vCard data. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_JAPANESE = + public static final int VCARD_TYPE_V30_JAPANESE_SJIS = (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese"; + /* package */ static final String VCARD_TYPE_V30_JAPANESE_SJIS_STR = "v30_japanese_sjis"; /** - * vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * <P> + * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | @@ -193,38 +290,72 @@ public class VCardConfig { /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; /** - * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. - * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. - * No Android-specific property nor defact property is included. + * <P> + * The vCard 2.1 based format which (partially) considers the convention in Japanese + * mobile phones, where phonetic names are translated to half-width katakana if + * possible, etc. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> + */ + public static final int VCARD_TYPE_V21_JAPANESE_MOBILE = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_CONVERT_PHONETIC_NAME_STRINGS | + FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES); + + public static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile"; + + /** + * <P> + * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. + * </p> + * <P> + * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. + * No Android-specific property nor defact property is included. The "Primary" properties + * are NOT encoded to Quoted-Printable. + * </P> */ public static final int VCARD_TYPE_DOCOMO = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_DOCOMO); + (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO); private static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC_UTF8; - private static final Map<String, Integer> VCARD_TYPES_MAP; + private static final Map<String, Integer> sVCardTypeMap; + private static final Set<Integer> sJapaneseMobileTypeSet; static { - VCARD_TYPES_MAP = new HashMap<String, Integer>(); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); - VCARD_TYPES_MAP.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + sVCardTypeMap = new HashMap<String, Integer>(); + sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_UTF8_STR, VCARD_TYPE_V21_GENERIC_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_UTF8_STR, VCARD_TYPE_V30_GENERIC_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_UTF8_STR, VCARD_TYPE_V21_EUROPE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_SJIS_STR, VCARD_TYPE_V21_JAPANESE_SJIS); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_SJIS_STR, VCARD_TYPE_V30_JAPANESE_SJIS); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE); + sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + + sJapaneseMobileTypeSet = new HashSet<Integer>(); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE); + sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO); } public static int getVCardTypeFromString(String vcardTypeString) { String loweredKey = vcardTypeString.toLowerCase(); - if (VCARD_TYPES_MAP.containsKey(loweredKey)) { - return VCARD_TYPES_MAP.get(loweredKey); + if (sVCardTypeMap.containsKey(loweredKey)) { + return sVCardTypeMap.get(loweredKey); } else { // XXX: should return the value indicating the input is invalid? + Log.e(LOG_TAG, "Unknown vCard type String: \"" + vcardTypeString + "\""); return VCARD_TYPE_DEFAULT; } } @@ -237,22 +368,6 @@ public class VCardConfig { return !isV30(vcardType); } - public static boolean isDoCoMo(int vcardType) { - return ((vcardType & FLAG_DOCOMO) != 0); - } - - /** - * @return true if the device is Japanese and some Japanese convension is - * applied to creating "formatted" something like FORMATTED_ADDRESS. - */ - public static boolean isJapaneseDevice(int vcardType) { - return ((vcardType == VCARD_TYPE_V21_JAPANESE) || - (vcardType == VCARD_TYPE_V21_JAPANESE_UTF8) || - (vcardType == VCARD_TYPE_V30_JAPANESE) || - (vcardType == VCARD_TYPE_V30_JAPANESE_UTF8) || - (vcardType == VCARD_TYPE_DOCOMO)); - } - public static boolean usesUtf8(int vcardType) { return ((vcardType & FLAG_CHARSET_UTF8) != 0); } @@ -261,17 +376,6 @@ public class VCardConfig { return ((vcardType & FLAG_CHARSET_SHIFT_JIS) != 0); } - /** - * @return true when Japanese phonetic string must be converted to a string - * containing only half-width katakana. This method exists since Japanese mobile - * phones usually use only half-width katakana for expressing phonetic names and - * some devices are not ready for parsing other phonetic strings like hiragana and - * full-width katakana. - */ - public static boolean needsToConvertPhoneticString(int vcardType) { - return (vcardType == VCARD_TYPE_DOCOMO); - } - public static int getNameOrderType(int vcardType) { return vcardType & NAME_ORDER_MASK; } @@ -284,20 +388,37 @@ public class VCardConfig { return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); } - public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { - return vcardType == VCARD_TYPE_DOCOMO; - } - public static boolean showPerformanceLog() { return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; } + public static boolean refrainsQPToPrimaryProperties(int vcardType) { + return (!usesQuotedPrintable(vcardType) || + ((vcardType & FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES) != 0)); + } + + public static boolean appendTypeParamName(int vcardType) { + return (isV30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0)); + } + /** - * @hide + * @return true if the device is Japanese and some Japanese convension is + * applied to creating "formatted" something like FORMATTED_ADDRESS. */ - public static boolean usesQPToPrimaryProperties(int vcardType) { - return (usesQuotedPrintable(vcardType) && - ((vcardType & FLAG_USE_QP_TO_PRIMARY_PROPERTIES) != 0)); + public static boolean isJapaneseDevice(int vcardType) { + return sJapaneseMobileTypeSet.contains(vcardType); + } + + public static boolean needsToConvertPhoneticString(int vcardType) { + return ((vcardType & FLAG_CONVERT_PHONETIC_NAME_STRINGS) != 0); + } + + public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { + return vcardType == VCARD_TYPE_DOCOMO; + } + + public static boolean isDoCoMo(int vcardType) { + return ((vcardType & FLAG_DOCOMO) != 0); } private VCardConfig() { diff --git a/core/java/android/pim/vcard/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardDataBuilder.java index d2026d0..76ad482 100644 --- a/core/java/android/pim/vcard/VCardDataBuilder.java +++ b/core/java/android/pim/vcard/VCardDataBuilder.java @@ -69,7 +69,7 @@ public class VCardDataBuilder implements VCardBuilder { private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>(); public VCardDataBuilder() { - this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC, null); + this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null); } /** @@ -86,7 +86,7 @@ public class VCardDataBuilder implements VCardBuilder { boolean strictLineBreakParsing, int vcardType, Account account) { this(null, charset, strictLineBreakParsing, vcardType, account); } - + /** * @hide */ @@ -127,6 +127,18 @@ public class VCardDataBuilder implements VCardBuilder { } /** + * Called when the parse failed between startRecord() and endRecord(). + * Currently it happens only when the vCard format is 3.0. + * (VCardVersionException is thrown by VCardParser_V21 and this object is reused by + * VCardParser_V30. At that time, startRecord() is called twice before endRecord() is called.) + * TODO: Should this be in VCardBuilder interface? + */ + public void clear() { + mCurrentContactStruct = null; + mCurrentProperty = new ContactStruct.Property(); + } + + /** * Assume that VCard is not nested. In other words, this code does not accept */ public void startRecord(String type) { diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java index b5e5049..462e22c 100644 --- a/core/java/android/pim/vcard/VCardParser.java +++ b/core/java/android/pim/vcard/VCardParser.java @@ -21,9 +21,23 @@ import java.io.IOException; import java.io.InputStream; public abstract class VCardParser { + public static final int PARSER_MODE_DEFAULT = 0; + /** + * The parser should ignore "AGENT" properties and nested vCard structure. + */ + public static final int PARSER_MODE_SCAN = 1; + protected final int mParserMode; protected boolean mCanceled; - + + public VCardParser() { + mParserMode = PARSER_MODE_DEFAULT; + } + + public VCardParser(int parserMode) { + mParserMode = parserMode; + } + /** * Parses the given stream and send the VCard data into VCardBuilderBase object. * @@ -35,7 +49,7 @@ public abstract class VCardParser { * * In order to avoid "misunderstanding" charset as much as possible, this method * use "ISO-8859-1" for reading the stream. When charset is specified in some property - * (with "CHARSET=..." attribute), the string is decoded to raw bytes and encoded to + * (with "CHARSET=..." parameter), the string is decoded to raw bytes and encoded to * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit * characters, which is not completely sure. In some cases, this "decoding-encoding" * scheme may fail. To avoid the case, diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java index 974fca8..251db68 100644 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -15,9 +15,11 @@ */ package android.pim.vcard; +import android.pim.vcard.exception.VCardAgentNotSupportedException; import android.pim.vcard.exception.VCardException; +import android.pim.vcard.exception.VCardInvalidCommentLineException; +import android.pim.vcard.exception.VCardInvalidLineException; import android.pim.vcard.exception.VCardNestedException; -import android.pim.vcard.exception.VCardNotSupportedException; import android.pim.vcard.exception.VCardVersionException; import android.util.Log; @@ -52,7 +54,7 @@ public class VCardParser_V21 extends VCardParser { Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); /** Store the property names available in vCard 2.1 */ - private static final HashSet<String> sAvailablePropertyNameV21 = + private static final HashSet<String> sAvailablePropertyNameSetV21 = new HashSet<String>(Arrays.asList( "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", @@ -89,8 +91,15 @@ public class VCardParser_V21 extends VCardParser { // In order to reduce warning message as much as possible, we hold the value which made Logger // emit a warning message. - protected HashSet<String> mWarningValueMap = new HashSet<String>(); - + protected HashSet<String> mUnknownTypeMap = new HashSet<String>(); + protected HashSet<String> mUnknownValueMap = new HashSet<String>(); + + // It seems Windows Mobile 6.5 uses "AGENT" property with completely wrong usage. + // We should just ignore just one line. + // e.g. + // "AGENT;CHARSET=SHIFT_JIS:some text" + private boolean mIgnoreAgentLine = false; + // Just for debugging private long mTimeTotal; private long mTimeReadStartRecord; @@ -104,21 +113,41 @@ public class VCardParser_V21 extends VCardParser { private long mTimeHandleMiscPropertyValue; private long mTimeHandleQuotedPrintable; private long mTimeHandleBase64; - + /** * Create a new VCard parser. */ public VCardParser_V21() { - super(); + this(null, PARSER_MODE_DEFAULT); + } + + public VCardParser_V21(int parserMode) { + this(null, parserMode); } public VCardParser_V21(VCardSourceDetector detector) { - super(); - if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) { - mNestCount = 1; + this(detector, PARSER_MODE_DEFAULT); + } + + /** + * TODO: Merge detector and parser mode. + */ + public VCardParser_V21(VCardSourceDetector detector, int parserMode) { + super(parserMode); + if (detector != null) { + final int type = detector.getType(); + if (type == VCardSourceDetector.TYPE_FOMA) { + mNestCount = 1; + } else if (type == VCardSourceDetector.TYPE_JAPANESE_MOBILE_PHONE) { + mIgnoreAgentLine = true; + } + } + + if (parserMode == PARSER_MODE_SCAN) { + mIgnoreAgentLine = true; } } - + /** * Parse the file at the given position * vcard_file = [wsls] vcard [wsls] @@ -144,18 +173,22 @@ public class VCardParser_V21 extends VCardParser { } } - protected String getVersion() { - return "2.1"; + protected int getVersion() { + return VCardConfig.FLAG_V21; } - + + protected String getVersionString() { + return Constants.VERSION_V21; + } + /** * @return true when the propertyName is a valid property name. */ protected boolean isValidPropertyName(String propertyName) { - if (!(sAvailablePropertyNameV21.contains(propertyName.toUpperCase()) || + if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || propertyName.startsWith("X-")) && - !mWarningValueMap.contains(propertyName)) { - mWarningValueMap.add(propertyName); + !mUnknownTypeMap.contains(propertyName)) { + mUnknownTypeMap.add(propertyName); Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); } return true; @@ -342,7 +375,12 @@ public class VCardParser_V21 extends VCardParser { mBuilder.startProperty(); mTimeStartProperty += System.currentTimeMillis() - start; } - ended = parseItem(); + try { + ended = parseItem(); + } catch (VCardInvalidCommentLineException e) { + Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); + ended = false; + } if (mBuilder != null && !ended) { long start = System.currentTimeMillis(); mBuilder.endProperty(); @@ -356,7 +394,7 @@ public class VCardParser_V21 extends VCardParser { * / [groups "."] "ADR" [params] ":" addressparts CRLF * / [groups "."] "ORG" [params] ":" orgparts CRLF * / [groups "."] "N" [params] ":" nameparts CRLF - * / [groups "."] "AGENT" [params] ":" vcard CRLF + * / [groups "."] "AGENT" [params] ":" vcard CRLF */ protected boolean parseItem() throws IOException, VCardException { mEncoding = sDefaultEncoding; @@ -369,7 +407,7 @@ public class VCardParser_V21 extends VCardParser { return true; } if (propertyNameAndValue.length != 2) { - throw new VCardException("Invalid line \"" + line + "\""); + throw new VCardInvalidLineException("Invalid line \"" + line + "\""); } String propertyName = propertyNameAndValue[0].toUpperCase(); String propertyValue = propertyNameAndValue[1]; @@ -392,9 +430,10 @@ public class VCardParser_V21 extends VCardParser { } else { throw new VCardException("Unknown BEGIN type: " + propertyValue); } - } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) { + } else if (propertyName.equals("VERSION") && + !propertyValue.equals(getVersionString())) { throw new VCardVersionException("Incompatible version: " + - propertyValue + " != " + getVersion()); + propertyValue + " != " + getVersionString()); } start = System.currentTimeMillis(); handlePropertyValue(propertyName, propertyValue); @@ -418,7 +457,11 @@ public class VCardParser_V21 extends VCardParser { int nameIndex = 0; String[] propertyNameAndValue = new String[2]; - + + if (length > 0 && line.charAt(0) == '#') { + throw new VCardInvalidCommentLineException(); + } + for (int i = 0; i < length; i++) { char ch = line.charAt(i); switch (state) { @@ -483,7 +526,7 @@ public class VCardParser_V21 extends VCardParser { } } - throw new VCardException("Invalid line: \"" + line + "\""); + throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); } @@ -520,19 +563,27 @@ public class VCardParser_V21 extends VCardParser { throw new VCardException("Unknown type \"" + paramName + "\""); } } else { - handleType(strArray[0]); + handleParamWithoutName(strArray[0]); } } /** + * vCard 3.0 parser may throw VCardException. + */ + @SuppressWarnings("unused") + protected void handleParamWithoutName(final String paramValue) throws VCardException { + handleType(paramValue); + } + + /** * ptypeval = knowntype / "X-" word */ - protected void handleType(String ptypeval) { + protected void handleType(final String ptypeval) { String upperTypeValue = ptypeval; if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && - !mWarningValueMap.contains(ptypeval)) { - mWarningValueMap.add(ptypeval); - Log.w(LOG_TAG, "Type unsupported by vCard 2.1: " + ptypeval); + !mUnknownTypeMap.contains(ptypeval)) { + mUnknownTypeMap.add(ptypeval); + Log.w(LOG_TAG, "TYPE unsupported by vCard 2.1: " + ptypeval); } if (mBuilder != null) { mBuilder.propertyParamType("TYPE"); @@ -543,15 +594,16 @@ public class VCardParser_V21 extends VCardParser { /** * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word */ - protected void handleValue(String pvalueval) throws VCardException { - if (sKnownValueSet.contains(pvalueval.toUpperCase()) || - pvalueval.startsWith("X-")) { - if (mBuilder != null) { - mBuilder.propertyParamType("VALUE"); - mBuilder.propertyParamValue(pvalueval); - } - } else { - throw new VCardException("Unknown value \"" + pvalueval + "\""); + protected void handleValue(final String pvalueval) { + if (!sKnownValueSet.contains(pvalueval.toUpperCase()) && + pvalueval.startsWith("X-") && + !mUnknownValueMap.contains(pvalueval)) { + mUnknownValueMap.add(pvalueval); + Log.w(LOG_TAG, "VALUE unsupported by vCard 2.1: " + pvalueval); + } + if (mBuilder != null) { + mBuilder.propertyParamType("VALUE"); + mBuilder.propertyParamValue(pvalueval); } } @@ -761,32 +813,11 @@ public class VCardParser_V21 extends VCardParser { } if (mBuilder != null) { - StringBuilder builder = new StringBuilder(); - ArrayList<String> list = new ArrayList<String>(); - int length = propertyValue.length(); - for (int i = 0; i < length; i++) { - char ch = propertyValue.charAt(i); - if (ch == '\\' && i < length - 1) { - char nextCh = propertyValue.charAt(i + 1); - String unescapedString = maybeUnescapeCharacter(nextCh); - if (unescapedString != null) { - builder.append(unescapedString); - i++; - } else { - builder.append(ch); - } - } else if (ch == ';') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else { - builder.append(ch); - } - } - list.add(builder.toString()); - mBuilder.propertyValues(list); + mBuilder.propertyValues(VCardUtils.constructListFromValue( + propertyValue, (getVersion() == VCardConfig.FLAG_V30))); } } - + /** * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. * @@ -797,9 +828,14 @@ public class VCardParser_V21 extends VCardParser { * items *CRLF "END" [ws] ":" [ws] "VCARD" * */ - protected void handleAgent(String propertyValue) throws VCardException { - throw new VCardNotSupportedException("AGENT Property is not supported now."); - /* This is insufficient support. Also, AGENT Property is very rare. + protected void handleAgent(final String propertyValue) throws VCardException { + if (mIgnoreAgentLine) { + return; + } else { + throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); + } + /* This is insufficient support. Also, AGENT Property is very rare and really hard to + understand the content. Ignore it for now. String[] strArray = propertyValue.split(":", 2); @@ -816,15 +852,19 @@ public class VCardParser_V21 extends VCardParser { /** * For vCard 3.0. */ - protected String maybeUnescapeText(String text) { + protected String maybeUnescapeText(final String text) { return text; } - + /** * Returns unescaped String if the character should be unescaped. Return null otherwise. * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. */ - protected String maybeUnescapeCharacter(char ch) { + protected String maybeUnescapeCharacter(final char ch) { + return unescapeCharacter(ch); + } + + public static String unescapeCharacter(final char ch) { // Original vCard 2.1 specification does not allow transformation // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of // this class allowed them, so keep it as is. @@ -836,7 +876,7 @@ public class VCardParser_V21 extends VCardParser { } @Override - public boolean parse(InputStream is, VCardBuilder builder) + public boolean parse(final InputStream is, final VCardBuilder builder) throws IOException, VCardException { return parse(is, VCardConfig.DEFAULT_CHARSET, builder); } @@ -844,6 +884,9 @@ public class VCardParser_V21 extends VCardParser { @Override public boolean parse(InputStream is, String charset, VCardBuilder builder) throws IOException, VCardException { + if (charset == null) { + charset = VCardConfig.DEFAULT_CHARSET; + } final InputStreamReader tmpReader = new InputStreamReader(is, charset); if (VCardConfig.showPerformanceLog()) { mReader = new CustomBufferedReader(tmpReader); diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java index 384649a..3dd467c 100644 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -46,31 +46,64 @@ public class VCardParser_V30 extends VCardParser_V21 { private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); private String mPreviousLine; - + private boolean mEmittedAgentWarning = false; - + + /** + * True when the caller wants the parser to be strict about the input. + * Currently this is only for testing. + */ + private final boolean mStrictParsing; + + public VCardParser_V30() { + super(); + mStrictParsing = false; + } + + /** + * @param strictParsing when true, this object throws VCardException when the vcard is not + * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class + * is not fully yet for being used with this flag and may not notice invalid line(s). + * + * @hide currently only for testing! + */ + public VCardParser_V30(boolean strictParsing) { + super(); + mStrictParsing = strictParsing; + } + + public VCardParser_V30(int parseMode) { + super(parseMode); + mStrictParsing = false; + } + @Override - protected String getVersion() { + protected int getVersion() { + return VCardConfig.FLAG_V30; + } + + @Override + protected String getVersionString() { return Constants.VERSION_V30; } - + @Override protected boolean isValidPropertyName(String propertyName) { if (!(sAcceptablePropsWithParam.contains(propertyName) || acceptablePropsWithoutParam.contains(propertyName) || propertyName.startsWith("X-")) && - !mWarningValueMap.contains(propertyName)) { - mWarningValueMap.add(propertyName); + !mUnknownTypeMap.contains(propertyName)) { + mUnknownTypeMap.add(propertyName); Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); } return true; } - + @Override protected boolean isValidEncoding(String encoding) { return sAcceptableEncodingV30.contains(encoding.toUpperCase()); } - + @Override protected String getLine() throws IOException { if (mPreviousLine != null) { @@ -199,7 +232,16 @@ public class VCardParser_V30 extends VCardParser_V21 { // TODO: fix this. super.handleAnyParam(paramName, paramValue); } - + + @Override + protected void handleParamWithoutName(final String paramValue) throws VCardException { + if (mStrictParsing) { + throw new VCardException("Parameter without name is not acceptable in vCard 3.0"); + } else { + super.handleParamWithoutName(paramValue); + } + } + /** * vCard 3.0 defines * @@ -284,6 +326,10 @@ public class VCardParser_V30 extends VCardParser_V21 { */ @Override protected String maybeUnescapeText(String text) { + return unescapeText(text); + } + + public static String unescapeText(String text) { StringBuilder builder = new StringBuilder(); int length = text.length(); for (int i = 0; i < length; i++) { @@ -299,15 +345,19 @@ public class VCardParser_V30 extends VCardParser_V21 { builder.append(ch); } } - return builder.toString(); + return builder.toString(); } @Override protected String maybeUnescapeCharacter(char ch) { + return unescapeCharacter(ch); + } + + public static String unescapeCharacter(char ch) { if (ch == 'n' || ch == 'N') { return "\n"; } else { return String.valueOf(ch); - } + } } } diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java index 4f50103..9e5dbb5 100644 --- a/core/java/android/pim/vcard/VCardUtils.java +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -18,13 +18,17 @@ package android.pim.vcard; import android.content.ContentProviderOperation; import android.content.ContentValues; import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -37,53 +41,69 @@ public class VCardUtils { */ // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is - // converted to two attribute Strings. These only contain some minor fields valid in both + // converted to two parameter Strings. These only contain some minor fields valid in both // vCard and current (as of 2009-08-07) Contacts structure. private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS; private static final Set<String> sPhoneTypesSetUnknownToContacts; - - private static final Map<String, Integer> sKnownPhoneTypesMap_StoI; - + + private static final Map<String, Integer> sKnownPhoneTypeMap_StoI; + + private static final Map<Integer, String> sKnownImPropNameMap_ItoS; + static { sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>(); - sKnownPhoneTypesMap_StoI = new HashMap<String, Integer>(); - - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, Constants.ATTR_TYPE_CAR); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CAR, Phone.TYPE_CAR); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.ATTR_TYPE_PAGER); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PAGER, Phone.TYPE_PAGER); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.ATTR_TYPE_ISDN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_ISDN, Phone.TYPE_ISDN); + sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>(); + + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, Constants.PARAM_TYPE_CAR); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.PARAM_TYPE_PAGER); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.PARAM_TYPE_ISDN); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_HOME, Phone.TYPE_HOME); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_WORK, Phone.TYPE_WORK); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CELL, Phone.TYPE_MOBILE); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_OTHER, Phone.TYPE_OTHER); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_CALLBACK, Phone.TYPE_CALLBACK); - sKnownPhoneTypesMap_StoI.put( - Constants.ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_RADIO, Phone.TYPE_RADIO); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TELEX, Phone.TYPE_TELEX); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TTY_TDD, Phone.TYPE_TTY_TDD); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_ASSISTANT, Phone.TYPE_ASSISTANT); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, Phone.TYPE_CALLBACK); + sKnownPhoneTypeMap_StoI.put( + Constants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, Phone.TYPE_TTY_TDD); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, + Phone.TYPE_ASSISTANT); sPhoneTypesSetUnknownToContacts = new HashSet<String>(); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MODEM); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MSG); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_BBS); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_VIDEO); + sPhoneTypesSetUnknownToContacts.add(Constants.PARAM_TYPE_MODEM); + sPhoneTypesSetUnknownToContacts.add(Constants.PARAM_TYPE_BBS); + sPhoneTypesSetUnknownToContacts.add(Constants.PARAM_TYPE_VIDEO); + + sKnownImPropNameMap_ItoS = new HashMap<Integer, String>(); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, Constants.PROPERTY_X_GOOGLE_TALK); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, Constants.PROPERTY_X_QQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, Constants.PROPERTY_X_NETMEETING); } - - public static String getPhoneAttributeString(Integer type) { + + public static String getPhoneTypeString(Integer type) { return sKnownPhoneTypesMap_ItoS.get(type); } - + /** * Returns Interger when the given types can be parsed as known type. Returns String object * when not, which should be set to label. */ - public static Object getPhoneTypeFromStrings(Collection<String> types) { + public static Object getPhoneTypeFromStrings(Collection<String> types, + String number) { + if (number == null) { + number = ""; + } int type = -1; String label = null; boolean isFax = false; @@ -92,17 +112,30 @@ public class VCardUtils { if (types != null) { for (String typeString : types) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + if (typeString.equals(Constants.PARAM_TYPE_PREF)) { hasPref = true; - } else if (typeString.equals(Constants.ATTR_TYPE_FAX)) { + } else if (typeString.equals(Constants.PARAM_TYPE_FAX)) { isFax = true; } else { if (typeString.startsWith("X-") && type < 0) { typeString = typeString.substring(2); } - Integer tmp = sKnownPhoneTypesMap_StoI.get(typeString); + Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString); if (tmp != null) { - type = tmp; + final int typeCandidate = tmp; + // TYPE_PAGER is prefered when the number contains @ surronded by + // a pager number and a domain name. + // e.g. + // o 1111@domain.com + // x @domain.com + // x 1111@ + final int indexOfAt = number.indexOf("@"); + if ((typeCandidate == Phone.TYPE_PAGER + && 0 < indexOfAt && indexOfAt < number.length() - 1) + || type < 0 + || type == Phone.TYPE_CUSTOM) { + type = tmp; + } } else if (type < 0) { type = Phone.TYPE_CUSTOM; label = typeString; @@ -133,15 +166,19 @@ public class VCardUtils { return type; } } - - public static boolean isValidPhoneAttribute(String phoneAttribute, int vcardType) { + + public static String getPropertyNameForIm(int protocol) { + return sKnownImPropNameMap_ItoS.get(protocol); + } + + public static boolean isValidPhoneType(String phoneType, int vcardType) { // TODO: check the following. // - it may violate vCard spec // - it may contain non-ASCII characters // // TODO: use vcardType - return (phoneAttribute.startsWith("X-") || phoneAttribute.startsWith("x-") || - sPhoneTypesSetUnknownToContacts.contains(phoneAttribute)); + return (phoneType.startsWith("X-") || phoneType.startsWith("x-") || + sPhoneTypesSetUnknownToContacts.contains(phoneType)); } public static String[] sortNameElements(int vcardType, @@ -168,6 +205,14 @@ public class VCardUtils { return list; } + public static int getPhoneNumberFormat(final int vcardType) { + if (VCardConfig.isJapaneseDevice(vcardType)) { + return PhoneNumberUtils.FORMAT_JAPAN; + } else { + return PhoneNumberUtils.FORMAT_NANP; + } + } + /** * Inserts postal data into the builder object. * @@ -187,7 +232,10 @@ public class VCardUtils { } builder.withValue(StructuredPostal.POBOX, postalData.pobox); - // Extended address is dropped since there's no relevant entry in ContactsContract. + // TODO: Japanese phone seems to use this field for expressing all the address including + // region, city, etc. Not sure we're ok to store them into NEIGHBORHOOD, while it would be + // better than dropping them all. + builder.withValue(StructuredPostal.NEIGHBORHOOD, postalData.extendedAddress); builder.withValue(StructuredPostal.STREET, postalData.street); builder.withValue(StructuredPostal.CITY, postalData.localty); builder.withValue(StructuredPostal.REGION, postalData.region); @@ -200,12 +248,12 @@ public class VCardUtils { builder.withValue(Data.IS_PRIMARY, 1); } } - + /** * Returns String[] containing address information based on vCard spec * (PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name). * All String objects are non-null ("" is used when the relevant data is empty). - * + * * Note that the data structure of ContactsContract is different from that defined in vCard. * So some conversion may be performed in this method. See also * {{@link #insertStructuredPostalDataUsingContactsStruct(int, @@ -213,13 +261,20 @@ public class VCardUtils { * android.pim.vcard.ContactStruct.PostalData)} */ public static String[] getVCardPostalElements(ContentValues contentValues) { + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name String[] dataArray = new String[7]; dataArray[0] = contentValues.getAsString(StructuredPostal.POBOX); if (dataArray[0] == null) { dataArray[0] = ""; } - // Extended addr. There's no relevant data in ContactsContract. - dataArray[1] = ""; + // We keep all the data in StructuredPostal, presuming NEIGHBORHOOD is + // similar to "Extended Address". + dataArray[1] = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); + if (dataArray[1] == null) { + dataArray[1] = ""; + } dataArray[2] = contentValues.getAsString(StructuredPostal.STREET); if (dataArray[2] == null) { dataArray[2] = ""; @@ -282,12 +337,41 @@ public class VCardUtils { } return builder.toString(); } - + + public static List<String> constructListFromValue(final String value, + final boolean isV30) { + final List<String> list = new ArrayList<String>(); + StringBuilder builder = new StringBuilder(); + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '\\' && i < length - 1) { + char nextCh = value.charAt(i + 1); + final String unescapedString = + (isV30 ? VCardParser_V30.unescapeCharacter(nextCh) : + VCardParser_V21.unescapeCharacter(nextCh)); + if (unescapedString != null) { + builder.append(unescapedString); + i++; + } else { + builder.append(ch); + } + } else if (ch == ';') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else { + builder.append(ch); + } + } + list.add(builder.toString()); + return list; + } + public static boolean containsOnlyPrintableAscii(String str) { if (TextUtils.isEmpty(str)) { return true; } - + final int length = str.length(); final int asciiFirst = 0x20; final int asciiLast = 0x126; diff --git a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java new file mode 100644 index 0000000..e72c7df --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard.exception; + +public class VCardAgentNotSupportedException extends VCardNotSupportedException { + public VCardAgentNotSupportedException() { + super(); + } + + public VCardAgentNotSupportedException(String message) { + super(message); + } + +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java new file mode 100644 index 0000000..67db62c --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.pim.vcard.exception; + +/** + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. + */ +public class VCardInvalidCommentLineException extends VCardInvalidLineException { + public VCardInvalidCommentLineException() { + super(); + } + + public VCardInvalidCommentLineException(final String message) { + super(message); + } +} diff --git a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java new file mode 100644 index 0000000..330153e --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.pim.vcard.exception; + +/** + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. + */ +public class VCardInvalidLineException extends VCardException { + public VCardInvalidLineException() { + super(); + } + + public VCardInvalidLineException(final String message) { + super(message); + } +} diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 08a2a9f..197d976 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -188,17 +188,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis mContext = context; TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Preference); - if (a.hasValue(com.android.internal.R.styleable.Preference_layout) || - a.hasValue(com.android.internal.R.styleable.Preference_widgetLayout)) { - // This preference has a custom layout defined (not one taken from - // the default style) - mHasSpecifiedLayout = true; - } - a.recycle(); - - a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Preference, - defStyle, 0); + com.android.internal.R.styleable.Preference, defStyle, 0); for (int i = a.getIndexCount(); i >= 0; i--) { int attr = a.getIndex(i); switch (attr) { @@ -252,6 +242,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } } a.recycle(); + + if (!getClass().getName().startsWith("android.preference")) { + // For subclasses not in this package, assume the worst and don't cache views + mHasSpecifiedLayout = true; + } } /** @@ -332,11 +327,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * @see #setWidgetLayoutResource(int) */ public void setLayoutResource(int layoutResId) { - - if (!mHasSpecifiedLayout) { + if (layoutResId != mLayoutResId) { + // Layout changed mHasSpecifiedLayout = true; } - + mLayoutResId = layoutResId; } @@ -360,6 +355,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * @see #setLayoutResource(int) */ public void setWidgetLayoutResource(int widgetLayoutResId) { + if (widgetLayoutResId != mWidgetLayoutResId) { + // Layout changed + mHasSpecifiedLayout = true; + } mWidgetLayoutResId = widgetLayoutResId; } diff --git a/core/java/android/preference/PreferenceGroupAdapter.java b/core/java/android/preference/PreferenceGroupAdapter.java index 14c0054..a908ecd 100644 --- a/core/java/android/preference/PreferenceGroupAdapter.java +++ b/core/java/android/preference/PreferenceGroupAdapter.java @@ -69,7 +69,9 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn * count once--when the adapter is being set). We will not recycle views for * Preference subclasses seen after the count has been returned. */ - private List<String> mPreferenceClassNames; + private ArrayList<PreferenceLayout> mPreferenceLayouts; + + private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout(); /** * Blocks the mPreferenceClassNames from being changed anymore. @@ -86,14 +88,37 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn } }; + private static class PreferenceLayout implements Comparable<PreferenceLayout> { + private int resId; + private int widgetResId; + private String name; + + public int compareTo(PreferenceLayout other) { + int compareNames = name.compareTo(other.name); + if (compareNames == 0) { + if (resId == other.resId) { + if (widgetResId == other.widgetResId) { + return 0; + } else { + return widgetResId - other.widgetResId; + } + } else { + return resId - other.resId; + } + } else { + return compareNames; + } + } + } + public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) { mPreferenceGroup = preferenceGroup; // If this group gets or loses any children, let us know mPreferenceGroup.setOnPreferenceChangeInternalListener(this); - + mPreferenceList = new ArrayList<Preference>(); - mPreferenceClassNames = new ArrayList<String>(); - + mPreferenceLayouts = new ArrayList<PreferenceLayout>(); + syncMyPreferences(); } @@ -102,7 +127,7 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn if (mIsSyncing) { return; } - + mIsSyncing = true; } @@ -128,7 +153,7 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn preferences.add(preference); - if (!mHasReturnedViewTypeCount) { + if (!mHasReturnedViewTypeCount && !preference.hasSpecifiedLayout()) { addPreferenceClassName(preference); } @@ -143,15 +168,28 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn } } + /** + * Creates a string that includes the preference name, layout id and widget layout id. + * If a particular preference type uses 2 different resources, they will be treated as + * different view types. + */ + private PreferenceLayout createPreferenceLayout(Preference preference, PreferenceLayout in) { + PreferenceLayout pl = in != null? in : new PreferenceLayout(); + pl.name = preference.getClass().getName(); + pl.resId = preference.getLayoutResource(); + pl.widgetResId = preference.getWidgetLayoutResource(); + return pl; + } + private void addPreferenceClassName(Preference preference) { - final String name = preference.getClass().getName(); - int insertPos = Collections.binarySearch(mPreferenceClassNames, name); - + final PreferenceLayout pl = createPreferenceLayout(preference, null); + int insertPos = Collections.binarySearch(mPreferenceLayouts, pl); + // Only insert if it doesn't exist (when it is negative). if (insertPos < 0) { // Convert to insert index insertPos = insertPos * -1 - 1; - mPreferenceClassNames.add(insertPos, name); + mPreferenceLayouts.add(insertPos, pl); } } @@ -171,19 +209,15 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn public View getView(int position, View convertView, ViewGroup parent) { final Preference preference = this.getItem(position); - - if (preference.hasSpecifiedLayout()) { - // If the preference had specified a layout (as opposed to the - // default), don't use convert views. + // Build a PreferenceLayout to compare with known ones that are cacheable. + mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout); + + // If it's not one of the cached ones, set the convertView to null so that + // the layout gets re-created by the Preference. + if (Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout) < 0) { convertView = null; - } else { - // TODO: better way of doing this - final String name = preference.getClass().getName(); - if (Collections.binarySearch(mPreferenceClassNames, name) < 0) { - convertView = null; - } } - + return preference.getView(convertView, parent); } @@ -225,8 +259,9 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn return IGNORE_ITEM_VIEW_TYPE; } - final String name = preference.getClass().getName(); - int viewType = Collections.binarySearch(mPreferenceClassNames, name); + mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout); + + int viewType = Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout); if (viewType < 0) { // This is a class that was seen after we returned the count, so // don't recycle it. @@ -242,7 +277,7 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn mHasReturnedViewTypeCount = true; } - return Math.max(1, mPreferenceClassNames.size()); + return Math.max(1, mPreferenceLayouts.size()); } } diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index f046cef..b8cf6c2 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -76,6 +76,15 @@ public final class Calendar { Uri.parse("content://" + AUTHORITY); /** + * An optional insert, update or delete URI parameter that allows the caller + * to specify that it is a sync adapter. The default value is false. If true + * the dirty flag is not automatically set and the "syncToNetwork" parameter + * is set to false when calling + * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}. + */ + public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + + /** * Columns from the Calendars table that other tables join into themselves. */ public interface CalendarsColumns @@ -341,11 +350,11 @@ public final class Calendar { * This field is copied here so that we can efficiently filter out * events that are declined without having to look in the Attendees * table. - * + * * <P>Type: INTEGER (int)</P> */ public static final String SELF_ATTENDEE_STATUS = "selfAttendeeStatus"; - + /** * The comments feed uri. * <P>Type: TEXT</P> @@ -514,6 +523,12 @@ public final class Calendar { * <P>Type: String</P> */ public static final String OWNER_ACCOUNT = "ownerAccount"; + + /** + * Whether the row has been deleted. A deleted row should be ignored. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String DELETED = "deleted"; } /** @@ -862,10 +877,10 @@ public final class Calendar { */ public static final String MAX_BUSYBITS = "maxBusyBits"; } - + public static final class CalendarMetaData implements CalendarMetaDataColumns { } - + public interface BusyBitsColumns { /** * The Julian day number. @@ -889,22 +904,22 @@ public final class Calendar { */ public static final String ALL_DAY_COUNT = "allDayCount"; } - + public static final class BusyBits implements BusyBitsColumns { public static final Uri CONTENT_URI = Uri.parse("content://calendar/busybits/when"); public static final String[] PROJECTION = { DAY, BUSYBITS, ALL_DAY_COUNT }; - + // The number of minutes represented by one busy bit public static final int MINUTES_PER_BUSY_INTERVAL = 60; - + // The number of intervals in a day public static final int INTERVALS_PER_DAY = 24 * 60 / MINUTES_PER_BUSY_INTERVAL; /** * Retrieves the busy bits for the Julian days starting at "startDay" * for "numDays". - * + * * @param cr the ContentResolver * @param startDay the first Julian day in the range * @param numDays the number of days to load (must be at least 1) @@ -1032,14 +1047,14 @@ public final class Calendar { CalendarAlertsColumns, EventsColumns, CalendarsColumns { public static final String TABLE_NAME = "CalendarAlerts"; public static final Uri CONTENT_URI = Uri.parse("content://calendar/calendar_alerts"); - + /** * This URI is for grouping the query results by event_id and begin * time. This will return one result per instance of an event. So * events with multiple alarms will appear just once, but multiple * instances of a repeating event will show up multiple times. */ - public static final Uri CONTENT_URI_BY_INSTANCE = + public static final Uri CONTENT_URI_BY_INSTANCE = Uri.parse("content://calendar/calendar_alerts/by_instance"); public static final Uri insert(ContentResolver cr, long eventId, @@ -1063,11 +1078,11 @@ public final class Calendar { return cr.query(CONTENT_URI, projection, selection, selectionArgs, DEFAULT_SORT_ORDER); } - + /** * Finds the next alarm after (or equal to) the given time and returns * the time of that alarm or -1 if no such alarm exists. - * + * * @param cr the ContentResolver * @param millis the time in UTC milliseconds * @return the next alarm time greater than or equal to "millis", or -1 @@ -1091,13 +1106,13 @@ public final class Calendar { } return alarmTime; } - + /** * Searches the CalendarAlerts table for alarms that should have fired * but have not and then reschedules them. This method can be called * at boot time to restore alarms that may have been lost due to a * phone reboot. - * + * * @param cr the ContentResolver * @param context the Context * @param manager the AlarmManager @@ -1125,7 +1140,7 @@ public final class Calendar { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "missed alarms found: " + cursor.getCount()); } - + try { while (cursor.moveToNext()) { long id = cursor.getLong(0); @@ -1146,14 +1161,14 @@ public final class Calendar { } finally { cursor.close(); } - + } - + /** * Searches for an entry in the CalendarAlerts table that matches * the given event id, begin time and alarm time. If one is found * then this alarm already exists and this method returns true. - * + * * @param cr the ContentResolver * @param eventId the event id to match * @param begin the start time of the event in UTC millis diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 062080d..a796fe9 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -238,6 +238,29 @@ public final class MediaStore { private static final int FULL_SCREEN_KIND = 2; private static final int MICRO_KIND = 3; private static final String[] PROJECTION = new String[] {_ID, MediaColumns.DATA}; + static final int DEFAULT_GROUP_ID = 0; + + private static Bitmap getMiniThumbFromFile(Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) { + Bitmap bitmap = null; + Uri thumbUri = null; + try { + long thumbId = c.getLong(0); + String filePath = c.getString(1); + thumbUri = ContentUris.withAppendedId(baseUri, thumbId); + ParcelFileDescriptor pfdInput = cr.openFileDescriptor(thumbUri, "r"); + bitmap = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + pfdInput.close(); + } catch (FileNotFoundException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (IOException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (OutOfMemoryError ex) { + Log.e(TAG, "failed to allocate memory for thumbnail " + + thumbUri + "; " + ex); + } + return bitmap; + } /** * This method cancels the thumbnail request so clients waiting for getThumbnail will be @@ -246,11 +269,14 @@ public final class MediaStore { * * @param cr ContentResolver * @param origId original image or video id. use -1 to cancel all requests. + * @param groupId the same groupId used in getThumbnail * @param baseUri the base URI of requested thumbnails */ - static void cancelThumbnailRequest(ContentResolver cr, long origId, Uri baseUri) { + static void cancelThumbnailRequest(ContentResolver cr, long origId, Uri baseUri, + long groupId) { Uri cancelUri = baseUri.buildUpon().appendQueryParameter("cancel", "1") - .appendQueryParameter("orig_id", String.valueOf(origId)).build(); + .appendQueryParameter("orig_id", String.valueOf(origId)) + .appendQueryParameter("group_id", String.valueOf(groupId)).build(); Cursor c = null; try { c = cr.query(cancelUri, PROJECTION, null, null, null); @@ -271,18 +297,20 @@ public final class MediaStore { * @param kind could be MINI_KIND or MICRO_KIND * @param options this is only used for MINI_KIND when decoding the Bitmap * @param baseUri the base URI of requested thumbnails + * @param groupId the id of group to which this request belongs * @return Bitmap bitmap of specified thumbnail kind */ - static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, int kind, BitmapFactory.Options options, Uri baseUri, boolean isVideo) { Bitmap bitmap = null; String filePath = null; // Log.v(TAG, "getThumbnail: origId="+origId+", kind="+kind+", isVideo="+isVideo); - // some optimization for MICRO_KIND: if the magic is non-zero, we don't bother + // If the magic is non-zero, we simply return thumbnail if it does exist. // querying MediaProvider and simply return thumbnail. - if (kind == MICRO_KIND) { - MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); - if (thumbFile.getMagic(origId) != 0) { + MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); + long magic = thumbFile.getMagic(origId); + if (magic != 0) { + if (kind == MICRO_KIND) { byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; if (thumbFile.getMiniThumbFromFile(origId, data) != null) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); @@ -291,20 +319,34 @@ public final class MediaStore { } } return bitmap; + } else if (kind == MINI_KIND) { + String column = isVideo ? "video_id=" : "image_id="; + Cursor c = null; + try { + c = cr.query(baseUri, PROJECTION, column + origId, null, null); + if (c != null && c.moveToFirst()) { + bitmap = getMiniThumbFromFile(c, baseUri, cr, options); + if (bitmap != null) { + return bitmap; + } + } + } finally { + if (c != null) c.close(); + } } } Cursor c = null; try { Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1") - .appendQueryParameter("orig_id", String.valueOf(origId)).build(); + .appendQueryParameter("orig_id", String.valueOf(origId)) + .appendQueryParameter("group_id", String.valueOf(groupId)).build(); c = cr.query(blockingUri, PROJECTION, null, null, null); // This happens when original image/video doesn't exist. if (c == null) return null; // Assuming thumbnail has been generated, at least original image exists. if (kind == MICRO_KIND) { - MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; if (thumbFile.getMiniThumbFromFile(origId, data) != null) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); @@ -314,24 +356,7 @@ public final class MediaStore { } } else if (kind == MINI_KIND) { if (c.moveToFirst()) { - ParcelFileDescriptor pfdInput; - Uri thumbUri = null; - try { - long thumbId = c.getLong(0); - filePath = c.getString(1); - thumbUri = ContentUris.withAppendedId(baseUri, thumbId); - pfdInput = cr.openFileDescriptor(thumbUri, "r"); - bitmap = BitmapFactory.decodeFileDescriptor( - pfdInput.getFileDescriptor(), null, options); - pfdInput.close(); - } catch (FileNotFoundException ex) { - Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); - } catch (IOException ex) { - Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); - } catch (OutOfMemoryError ex) { - Log.e(TAG, "failed to allocate memory for thumbnail " - + thumbUri + "; " + ex); - } + bitmap = getMiniThumbFromFile(c, baseUri, cr, options); } } else { throw new IllegalArgumentException("Unsupported kind: " + kind); @@ -354,7 +379,7 @@ public final class MediaStore { } if (isVideo) { bitmap = ThumbnailUtil.createVideoThumbnail(filePath); - if (kind == MICRO_KIND) { + if (kind == MICRO_KIND && bitmap != null) { bitmap = ThumbnailUtil.extractMiniThumb(bitmap, ThumbnailUtil.MINI_THUMB_TARGET_SIZE, ThumbnailUtil.MINI_THUMB_TARGET_SIZE, @@ -669,7 +694,8 @@ public final class MediaStore { * @param origId original image id */ public static void cancelThumbnailRequest(ContentResolver cr, long origId) { - InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI); + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, + InternalThumbnails.DEFAULT_GROUP_ID); } /** @@ -685,7 +711,39 @@ public final class MediaStore { */ public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, BitmapFactory.Options options) { - return InternalThumbnails.getThumbnail(cr, origId, kind, options, + return InternalThumbnails.getThumbnail(cr, origId, + InternalThumbnails.DEFAULT_GROUP_ID, kind, options, + EXTERNAL_CONTENT_URI, false); + } + + /** + * This method cancels the thumbnail request so clients waiting for getThumbnail will be + * interrupted and return immediately. Only the original process which made the getThumbnail + * requests can cancel their own requests. + * + * @param cr ContentResolver + * @param origId original image id + * @param groupId the same groupId used in getThumbnail. + */ + public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) { + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, groupId); + } + + /** + * This method checks if the thumbnails of the specified image (origId) has been created. + * It will be blocked until the thumbnails are generated. + * + * @param cr ContentResolver used to dispatch queries to MediaProvider. + * @param origId Original image id associated with thumbnail of interest. + * @param groupId the id of group to which this request belongs + * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. + * @param options this is only used for MINI_KIND when decoding the Bitmap + * @return A Bitmap instance. It could be null if the original image + * associated with origId doesn't exist or memory is not enough. + */ + public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, + int kind, BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, groupId, kind, options, EXTERNAL_CONTENT_URI, false); } @@ -1598,7 +1656,8 @@ public final class MediaStore { * @param origId original video id */ public static void cancelThumbnailRequest(ContentResolver cr, long origId) { - InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI); + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, + InternalThumbnails.DEFAULT_GROUP_ID); } /** @@ -1607,18 +1666,50 @@ public final class MediaStore { * * @param cr ContentResolver used to dispatch queries to MediaProvider. * @param origId Original image id associated with thumbnail of interest. + * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. + * @param options this is only used for MINI_KIND when decoding the Bitmap + * @return A Bitmap instance. It could be null if the original image + * associated with origId doesn't exist or memory is not enough. + */ + public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, + InternalThumbnails.DEFAULT_GROUP_ID, kind, options, + EXTERNAL_CONTENT_URI, true); + } + + /** + * This method checks if the thumbnails of the specified image (origId) has been created. + * It will be blocked until the thumbnails are generated. + * + * @param cr ContentResolver used to dispatch queries to MediaProvider. + * @param origId Original image id associated with thumbnail of interest. + * @param groupId the id of group to which this request belongs * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND * @param options this is only used for MINI_KIND when decoding the Bitmap * @return A Bitmap instance. It could be null if the original image associated with * origId doesn't exist or memory is not enough. */ - public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, - BitmapFactory.Options options) { - return InternalThumbnails.getThumbnail(cr, origId, kind, options, + public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, + int kind, BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, groupId, kind, options, EXTERNAL_CONTENT_URI, true); } /** + * This method cancels the thumbnail request so clients waiting for getThumbnail will be + * interrupted and return immediately. Only the original process which made the getThumbnail + * requests can cancel their own requests. + * + * @param cr ContentResolver + * @param origId original video id + * @param groupId the same groupId used in getThumbnail. + */ + public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) { + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, groupId); + } + + /** * Get the content:// style URI for the image media table on the * given volume. * diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index cb3dc16..6086c66 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -43,8 +43,10 @@ import android.util.Log; import java.net.URISyntaxException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Map; /** @@ -476,7 +478,10 @@ public final class Settings { private static class NameValueCache { private final String mVersionSystemProperty; - private final HashMap<String, String> mValues = Maps.newHashMap(); + // the following needs synchronization because this structure is accessed from different + // threads and they could be performing clear(), get(), put() at the same time. + private final Map<String, String> mValues = + Collections.synchronizedMap(new HashMap<String, String>()); private long mValuesVersion = 0; private final Uri mUri; @@ -491,8 +496,29 @@ public final class Settings { mValues.clear(); mValuesVersion = newValuesVersion; } - if (!mValues.containsKey(name)) { - String value = null; + /* + * don't look for the key using containsKey() method because (key, object) mapping + * could be removed from mValues before get() is done like so: + * + * say, mValues contains mapping for "foo" + * Thread# 1 + * performs containsKey("foo") + * receives true + * Thread #2 + * triggers mValues.clear() + * Thread#1 + * since containsKey("foo") = true, performs get("foo") + * receives null + * thats incorrect! + * + * to avoid the above, thread#1 should do get("foo") instead of containsKey("foo") + * since mValues is synchronized, get() will get a consistent value. + * + * we don't want to make this method synchronized tho - because + * holding mutex is not desirable while a call could be made to database. + */ + String value = mValues.get(name); + if (value == null) { Cursor c = null; try { c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE }, @@ -505,10 +531,8 @@ public final class Settings { } finally { if (c != null) c.close(); } - return value; - } else { - return mValues.get(name); } + return value; } } @@ -2502,6 +2526,13 @@ public final class Settings { public static final String OVERRIDE_ACTION = "com.google.gservices.intent.action.GSERVICES_OVERRIDE"; + /** + * Intent action to set Gservices with new values. (Requires WRITE_GSERVICES permission.) + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String UPDATE_ACTION = + "com.google.gservices.intent.action.GSERVICES_UPDATE"; + private static volatile NameValueCache mNameValueCache = null; private static final Object mNameValueCacheLock = new Object(); @@ -2809,6 +2840,11 @@ public final class Settings { "gmail_wait_time_retry_uphill_op"; /** + * Controls if Gmail should delay sending operations that have previously failed. + */ + public static final String GMAIL_DELAY_BAD_OP = "gmail_delay_bad_op"; + + /** * Controls if the protocol buffer version of the protocol will use a multipart request for * attachment uploads. Value must be an integer where non-zero means true. Defaults to 0. */ @@ -3110,6 +3146,13 @@ public final class Settings { = "google_login_generic_auth_service"; /** + * Duration in milliseconds after setup at which market does not reconcile applications + * which are installed during restore. + */ + public static final String VENDING_RESTORE_WINDOW_MS = "vending_restore_window_ms"; + + + /** * Frequency in milliseconds at which we should sync the locally installed Vending Machine * content with the server. */ @@ -3623,7 +3666,6 @@ public final class Settings { */ public static final String SEARCH_PER_SOURCE_CONCURRENT_QUERY_LIMIT = "search_per_source_concurrent_query_limit"; - /** * Flag for allowing ActivityManagerService to send ACTION_APP_ERROR intents * on application crashes and ANRs. If this is disabled, the crash/ANR dialog @@ -3638,6 +3680,32 @@ public final class Settings { public static final String LAST_KMSG_KB = "last_kmsg_kb"; /** + * Maximum age of entries kept by {@link android.os.IDropBox}. + */ + public static final String DROPBOX_AGE_SECONDS = + "dropbox_age_seconds"; + /** + * Maximum amount of disk space used by {@link android.os.IDropBox} no matter what. + */ + public static final String DROPBOX_QUOTA_KB = + "dropbox_quota_kb"; + /** + * Percent of free disk (excluding reserve) which {@link android.os.IDropBox} will use. + */ + public static final String DROPBOX_QUOTA_PERCENT = + "dropbox_quota_percent"; + /** + * Percent of total disk which {@link android.os.IDropBox} will never dip into. + */ + public static final String DROPBOX_RESERVE_PERCENT = + "dropbox_reserve_percent"; + /** + * Prefix for per-tag dropbox disable/enable settings. + */ + public static final String DROPBOX_TAG_PREFIX = + "dropbox:"; + + /** * @deprecated * @hide */ diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index d8c5a53..9a72d93 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -152,6 +152,12 @@ public final class Telephony { * <P>Type: INTEGER (boolean)</P> */ public static final String LOCKED = "locked"; + + /** + * Error code associated with sending or receiving this message + * <P>Type: INTEGER</P> + */ + public static final String ERROR_CODE = "error_code"; } /** @@ -243,7 +249,7 @@ public final class Telephony { * @return true if the operation succeeded */ public static boolean moveMessageToFolder(Context context, - Uri uri, int folder) { + Uri uri, int folder, int error) { if (uri == null) { return false; } @@ -266,7 +272,7 @@ public final class Telephony { return false; } - ContentValues values = new ContentValues(2); + ContentValues values = new ContentValues(3); values.put(TYPE, folder); if (markAsUnread) { @@ -274,6 +280,7 @@ public final class Telephony { } else if (markAsRead) { values.put(READ, Integer.valueOf(1)); } + values.put(ERROR_CODE, error); return 1 == SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null); diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index d1dd311..b32aacc 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -61,7 +61,7 @@ import java.util.Map; public class BluetoothService extends IBluetooth.Stub { private static final String TAG = "BluetoothService"; - private static final boolean DBG = false; + private static final boolean DBG = true; private int mNativeData; private BluetoothEventLoop mEventLoop; diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index a92800d..afc6864 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -294,7 +294,12 @@ public abstract class Layout { lbaseline, lbottom, buf, start, end, par, this); - left += margin.getLeadingMargin(par); + boolean useMargin = par; + if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) { + int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount(); + useMargin = count > i; + } + left += margin.getLeadingMargin(useMargin); } } } @@ -1293,7 +1298,13 @@ public abstract class Layout { LeadingMarginSpan.class); for (int i = 0; i < spans.length; i++) { - left += spans[i].getLeadingMargin(par); + boolean margin = par; + LeadingMarginSpan span = spans[i]; + if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) { + int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount(); + margin = count >= line; + } + left += span.getLeadingMargin(margin); } } } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index f0a5ffd..fbf1261 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -161,6 +161,7 @@ extends Layout else end++; + int firstWidthLineCount = 1; int firstwidth = outerwidth; int restwidth = outerwidth; @@ -171,8 +172,12 @@ extends Layout sp = spanned.getSpans(start, end, LeadingMarginSpan.class); for (int i = 0; i < sp.length; i++) { + LeadingMarginSpan lms = sp[i]; firstwidth -= sp[i].getLeadingMargin(true); restwidth -= sp[i].getLeadingMargin(false); + if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) { + firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount(); + } } chooseht = spanned.getSpans(start, end, LineHeightSpan.class); @@ -750,7 +755,9 @@ extends Layout fitascent = fitdescent = fittop = fitbottom = 0; okascent = okdescent = oktop = okbottom = 0; - width = restwidth; + if (--firstWidthLineCount <= 0) { + width = restwidth; + } } } } diff --git a/core/java/android/text/style/LeadingMarginSpan.java b/core/java/android/text/style/LeadingMarginSpan.java index 8e212e3..cb55329 100644 --- a/core/java/android/text/style/LeadingMarginSpan.java +++ b/core/java/android/text/style/LeadingMarginSpan.java @@ -33,6 +33,11 @@ extends ParagraphStyle CharSequence text, int start, int end, boolean first, Layout layout); + + public interface LeadingMarginSpan2 extends LeadingMarginSpan, WrapTogetherSpan { + public int getLeadingMarginLineCount(); + }; + public static class Standard implements LeadingMarginSpan, ParcelableSpan { private final int mFirst, mRest; diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index bef3e58..ed2139d 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -68,6 +68,7 @@ public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks { private static final String TAG = "ViewRoot"; private static final boolean DBG = false; + private static final boolean SHOW_FPS = false; @SuppressWarnings({"ConstantConditionalExpression"}) private static final boolean LOCAL_LOGV = false ? Config.LOGD : Config.LOGV; /** @noinspection PointlessBooleanExpression*/ @@ -1244,7 +1245,7 @@ public final class ViewRoot extends Handler implements ViewParent, mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); checkEglErrors(); - if (Config.DEBUG && ViewDebug.showFps) { + if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) { int now = (int)SystemClock.elapsedRealtime(); if (sDrawTime != 0) { nativeShowFPS(canvas, now - sDrawTime); @@ -1356,7 +1357,7 @@ public final class ViewRoot extends Handler implements ViewParent, mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING); } - if (Config.DEBUG && ViewDebug.showFps) { + if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) { int now = (int)SystemClock.elapsedRealtime(); if (sDrawTime != 0) { nativeShowFPS(canvas, now - sDrawTime); diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index 9456ae1..e6e26fa 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -19,17 +19,21 @@ package android.webkit; import android.app.ActivityManager; import android.content.Context; import android.content.res.AssetManager; +import android.database.Cursor; import android.graphics.Bitmap; import android.net.ParseException; +import android.net.Uri; import android.net.WebAddress; import android.net.http.SslCertificate; import android.os.Handler; import android.os.Message; +import android.provider.OpenableColumns; import android.util.Log; import android.util.TypedValue; import junit.framework.Assert; +import java.io.InputStream; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; @@ -463,6 +467,63 @@ class BrowserFrame extends Handler { } /** + * Called by JNI. Given a URI, find the associated file and return its size + * @param uri A String representing the URI of the desired file. + * @return int The size of the given file. + */ + private int getFileSize(String uri) { + int size = 0; + Cursor cursor = mContext.getContentResolver().query(Uri.parse(uri), + new String[] { OpenableColumns.SIZE }, + null, + null, + null); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + size = cursor.getInt(0); + } + } finally { + cursor.close(); + } + } + return size; + } + + /** + * Called by JNI. Given a URI, a buffer, and an offset into the buffer, + * copy the resource into buffer. + * @param uri A String representing the URI of the desired file. + * @param buffer The byte array to copy the data into. + * @param offset The offet into buffer to place the data. + * @param expectSize The size that the buffer has allocated for this file. + * @return int The size of the given file, or zero if it fails. + */ + private int getFile(String uri, byte[] buffer, int offset, + int expectedSize) { + int size = 0; + try { + InputStream stream = mContext.getContentResolver() + .openInputStream(Uri.parse(uri)); + size = stream.available(); + if (size <= expectedSize && buffer != null + && buffer.length - offset >= size) { + stream.read(buffer, offset, size); + } else { + size = 0; + } + stream.close(); + } catch (java.io.FileNotFoundException e) { + Log.e(LOGTAG, "FileNotFoundException:" + e); + size = 0; + } catch (java.io.IOException e2) { + Log.e(LOGTAG, "IOException: " + e2); + size = 0; + } + return size; + } + + /** * Start loading a resource. * @param loaderHandle The native ResourceLoader that is the target of the * data. @@ -480,6 +541,7 @@ class BrowserFrame extends Handler { String method, HashMap headers, byte[] postData, + long postDataIdentifier, int cacheMode, boolean synchronous) { PerfChecker checker = new PerfChecker(); @@ -551,8 +613,9 @@ class BrowserFrame extends Handler { } // Create a LoadListener - LoadListener loadListener = LoadListener.getLoadListener(mContext, this, url, - loaderHandle, synchronous, isMainFramePage); + LoadListener loadListener = LoadListener.getLoadListener(mContext, + this, url, loaderHandle, synchronous, isMainFramePage, + postDataIdentifier); mCallbackProxy.onLoadResource(url); diff --git a/core/java/android/webkit/ByteArrayBuilder.java b/core/java/android/webkit/ByteArrayBuilder.java index 145411c..334526b 100644 --- a/core/java/android/webkit/ByteArrayBuilder.java +++ b/core/java/android/webkit/ByteArrayBuilder.java @@ -16,6 +16,8 @@ package android.webkit; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; import java.util.LinkedList; import java.util.ListIterator; @@ -23,47 +25,37 @@ import java.util.ListIterator; them back out. It does not optimize for returning the result in a single array, though this is supported in the API. It is fastest if the retrieval can be done via iterating through chunks. - - Things to add: - - consider dynamically increasing our min_capacity, - as we see mTotalSize increase */ class ByteArrayBuilder { private static final int DEFAULT_CAPACITY = 8192; - private LinkedList<Chunk> mChunks; - - /** free pool */ - private LinkedList<Chunk> mPool; + // Global pool of chunks to be used by other ByteArrayBuilders. + private static final LinkedList<SoftReference<Chunk>> sPool = + new LinkedList<SoftReference<Chunk>>(); + // Reference queue for processing gc'd entries. + private static final ReferenceQueue<Chunk> sQueue = + new ReferenceQueue<Chunk>(); - private int mMinCapacity; + private LinkedList<Chunk> mChunks; public ByteArrayBuilder() { - init(0); - } - - public ByteArrayBuilder(int minCapacity) { - init(minCapacity); - } - - private void init(int minCapacity) { mChunks = new LinkedList<Chunk>(); - mPool = new LinkedList<Chunk>(); - - if (minCapacity <= 0) { - minCapacity = DEFAULT_CAPACITY; - } - mMinCapacity = minCapacity; - } - - public void append(byte[] array) { - append(array, 0, array.length); } public synchronized void append(byte[] array, int offset, int length) { while (length > 0) { - Chunk c = appendChunk(length); + Chunk c = null; + if (mChunks.isEmpty()) { + c = obtainChunk(length); + mChunks.addLast(c); + } else { + c = mChunks.getLast(); + if (c.mLength == c.mArray.length) { + c = obtainChunk(length); + mChunks.addLast(c); + } + } int amount = Math.min(length, c.mArray.length - c.mLength); System.arraycopy(array, offset, c.mArray, c.mLength, amount); c.mLength += amount; @@ -75,7 +67,7 @@ class ByteArrayBuilder { /** * The fastest way to retrieve the data is to iterate through the * chunks. This returns the first chunk. Note: this pulls the - * chunk out of the queue. The caller must call releaseChunk() to + * chunk out of the queue. The caller must call Chunk.release() to * dispose of it. */ public synchronized Chunk getFirstChunk() { @@ -83,23 +75,11 @@ class ByteArrayBuilder { return mChunks.removeFirst(); } - /** - * recycles chunk - */ - public synchronized void releaseChunk(Chunk c) { - c.mLength = 0; - mPool.addLast(c); - } - - public boolean isEmpty() { + public synchronized boolean isEmpty() { return mChunks.isEmpty(); } - public int size() { - return mChunks.size(); - } - - public int getByteSize() { + public synchronized int getByteSize() { int total = 0; ListIterator<Chunk> it = mChunks.listIterator(0); while (it.hasNext()) { @@ -112,37 +92,40 @@ class ByteArrayBuilder { public synchronized void clear() { Chunk c = getFirstChunk(); while (c != null) { - releaseChunk(c); + c.release(); c = getFirstChunk(); } } - private Chunk appendChunk(int length) { - if (length < mMinCapacity) { - length = mMinCapacity; - } - - Chunk c; - if (mChunks.isEmpty()) { - c = obtainChunk(length); - } else { - c = mChunks.getLast(); - if (c.mLength == c.mArray.length) { - c = obtainChunk(length); + // Must be called with lock held on sPool. + private void processPoolLocked() { + while (true) { + SoftReference<Chunk> entry = (SoftReference<Chunk>) sQueue.poll(); + if (entry == null) { + break; } + sPool.remove(entry); } - return c; } private Chunk obtainChunk(int length) { - Chunk c; - if (mPool.isEmpty()) { - c = new Chunk(length); - } else { - c = mPool.removeFirst(); + // Correct a small length. + if (length < DEFAULT_CAPACITY) { + length = DEFAULT_CAPACITY; + } + synchronized (sPool) { + // Process any queued references and remove them from the pool. + processPoolLocked(); + if (!sPool.isEmpty()) { + Chunk c = sPool.removeFirst().get(); + // The first item may have been queued after processPoolLocked + // so check for null. + if (c != null) { + return c; + } + } + return new Chunk(length); } - mChunks.addLast(c); - return c; } public static class Chunk { @@ -153,5 +136,19 @@ class ByteArrayBuilder { mArray = new byte[length]; mLength = 0; } + + /** + * Release the chunk and make it available for reuse. + */ + public void release() { + mLength = 0; + synchronized (sPool) { + // Add the chunk back to the pool as a SoftReference so it can + // be gc'd if needed. + sPool.offer(new SoftReference<Chunk>(this, sQueue)); + sPool.notifyAll(); + } + } + } } diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index 75028de..910d7b2 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -283,16 +283,24 @@ public final class CacheManager { // only called from WebCore thread public static CacheResult getCacheFile(String url, Map<String, String> headers) { + return getCacheFile(url, 0, headers); + } + + // only called from WebCore thread + static CacheResult getCacheFile(String url, long postIdentifier, + Map<String, String> headers) { if (mDisabled) { return null; } - CacheResult result = mDataBase.getCache(url); + String databaseKey = getDatabaseKey(url, postIdentifier); + + CacheResult result = mDataBase.getCache(databaseKey); if (result != null) { if (result.contentLength == 0) { if (!checkCacheRedirect(result.httpStatusCode)) { // this should not happen. If it does, remove it. - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } } else { @@ -304,7 +312,7 @@ public final class CacheManager { } catch (FileNotFoundException e) { // the files in the cache directory can be removed by the // system. If it is gone, clean up the database - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } } @@ -352,14 +360,24 @@ public final class CacheManager { // only called from WebCore thread public static CacheResult createCacheFile(String url, int statusCode, Headers headers, String mimeType, boolean forceCache) { + return createCacheFile(url, statusCode, headers, mimeType, 0, + forceCache); + } + + // only called from WebCore thread + static CacheResult createCacheFile(String url, int statusCode, + Headers headers, String mimeType, long postIdentifier, + boolean forceCache) { if (!forceCache && mDisabled) { return null; } + String databaseKey = getDatabaseKey(url, postIdentifier); + // according to the rfc 2616, the 303 response MUST NOT be cached. if (statusCode == 303) { // remove the saved cache if there is any - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } @@ -367,7 +385,7 @@ public final class CacheManager { // header. if (checkCacheRedirect(statusCode) && !headers.getSetCookie().isEmpty()) { // remove the saved cache if there is any - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } @@ -375,9 +393,9 @@ public final class CacheManager { if (ret == null) { // this should only happen if the headers has "no-store" in the // cache-control. remove the saved cache if there is any - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); } else { - setupFiles(url, ret); + setupFiles(databaseKey, ret); try { ret.outStream = new FileOutputStream(ret.outFile); } catch (FileNotFoundException e) { @@ -408,6 +426,12 @@ public final class CacheManager { */ // only called from WebCore thread public static void saveCacheFile(String url, CacheResult cacheRet) { + saveCacheFile(url, 0, cacheRet); + } + + // only called from WebCore thread + static void saveCacheFile(String url, long postIdentifier, + CacheResult cacheRet) { try { cacheRet.outStream.close(); } catch (IOException e) { @@ -434,7 +458,7 @@ public final class CacheManager { return; } - mDataBase.addCache(url, cacheRet); + mDataBase.addCache(getDatabaseKey(url, postIdentifier), cacheRet); if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "saveCacheFile for url " + url); @@ -513,6 +537,11 @@ public final class CacheManager { } } + private static String getDatabaseKey(String url, long postIdentifier) { + if (postIdentifier == 0) return url; + return postIdentifier + url; + } + @SuppressWarnings("deprecation") private static void setupFiles(String url, CacheResult cacheRet) { if (true) { diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index f760b61..4a8e758 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -107,6 +107,7 @@ class CallbackProxy extends Handler { private static final int GEOLOCATION_PERMISSIONS_HIDE_PROMPT = 131; private static final int RECEIVED_TOUCH_ICON_URL = 132; private static final int GET_VISITED_HISTORY = 133; + private static final int OPEN_FILE_CHOOSER = 134; // Message triggered by the client to resume execution private static final int NOTIFY = 200; @@ -149,6 +150,16 @@ class CallbackProxy extends Handler { } /** + * Get the WebViewClient. + * @return the current WebViewClient instance. + * + *@hide pending API council approval. + */ + public WebViewClient getWebViewClient() { + return mWebViewClient; + } + + /** * Set the WebChromeClient. * @param client An implementation of WebChromeClient. */ @@ -238,8 +249,10 @@ class CallbackProxy extends Handler { break; case PAGE_FINISHED: + String finishedUrl = (String) msg.obj; + mWebView.onPageFinished(finishedUrl); if (mWebViewClient != null) { - mWebViewClient.onPageFinished(mWebView, (String) msg.obj); + mWebViewClient.onPageFinished(mWebView, finishedUrl); } break; @@ -660,6 +673,12 @@ class CallbackProxy extends Handler { mWebChromeClient.getVisitedHistory((ValueCallback<String[]>)msg.obj); } break; + + case OPEN_FILE_CHOOSER: + if (mWebChromeClient != null) { + mWebChromeClient.openFileChooser((UploadFile) msg.obj); + } + break; } } @@ -758,11 +777,6 @@ class CallbackProxy extends Handler { } public void onPageFinished(String url) { - // Do an unsynchronized quick check to avoid posting if no callback has - // been set. - if (mWebViewClient == null) { - return; - } // Performance probe if (PERF_PROBE) { // un-comment this if PERF_PROBE is true @@ -1000,10 +1014,10 @@ class CallbackProxy extends Handler { public void onProgressChanged(int newProgress) { // Synchronize so that mLatestProgress is up-to-date. synchronized (this) { - mLatestProgress = newProgress; - if (mWebChromeClient == null) { + if (mWebChromeClient == null || mLatestProgress == newProgress) { return; } + mLatestProgress = newProgress; if (!mProgressUpdatePending) { sendEmptyMessage(PROGRESS); mProgressUpdatePending = true; @@ -1335,4 +1349,40 @@ class CallbackProxy extends Handler { msg.obj = callback; sendMessage(msg); } + + private class UploadFile implements ValueCallback<Uri> { + private Uri mValue; + public void onReceiveValue(Uri value) { + mValue = value; + synchronized (CallbackProxy.this) { + CallbackProxy.this.notify(); + } + } + public Uri getResult() { + return mValue; + } + } + + /** + * Called by WebViewCore to open a file chooser. + */ + /* package */ Uri openFileChooser() { + if (mWebChromeClient == null) { + return null; + } + Message myMessage = obtainMessage(OPEN_FILE_CHOOSER); + UploadFile uploadFile = new UploadFile(); + myMessage.obj = uploadFile; + synchronized (this) { + sendMessage(myMessage); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, + "Caught exception while waiting for openFileChooser"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + return uploadFile.getResult(); + } } diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index c1eeb3b..c3dac6e 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -242,7 +242,7 @@ class FrameLoader { // to load POST content in a history navigation. case WebSettings.LOAD_CACHE_ONLY: { CacheResult result = CacheManager.getCacheFile(mListener.url(), - null); + mListener.postIdentifier(), null); if (result != null) { startCacheLoad(result); } else { @@ -270,7 +270,7 @@ class FrameLoader { // Get the cache file name for the current URL, passing null for // the validation headers causes no validation to occur CacheResult result = CacheManager.getCacheFile(mListener.url(), - null); + mListener.postIdentifier(), null); if (result != null) { startCacheLoad(result); return true; diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index b7a9065..3e0be1c 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -199,6 +199,8 @@ class HTML5VideoViewProxy extends Handler public void playbackEnded() { Message msg = Message.obtain(mWebCoreHandler, ENDED); mWebCoreHandler.sendMessage(msg); + // also send a message to ourselves to return to the WebView + sendMessage(obtainMessage(ENDED)); } // Handler for the messages from WebCore thread to the UI thread. @@ -224,6 +226,7 @@ class HTML5VideoViewProxy extends Handler VideoPlayer.pause(this); break; } + case ENDED: case ERROR: { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java index 2f46f2b..042953c 100644 --- a/core/java/android/webkit/HttpDateTime.java +++ b/core/java/android/webkit/HttpDateTime.java @@ -50,13 +50,15 @@ public final class HttpDateTime { * Wdy Mon DD HH:MM:SS YYYY GMT * * HH can be H if the first digit is zero. + * + * Mon can be the full name of the month. */ private static final String HTTP_DATE_RFC_REGEXP = - "([0-9]{1,2})[- ]([A-Za-z]{3,3})[- ]([0-9]{2,4})[ ]" + "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; private static final String HTTP_DATE_ANSIC_REGEXP = - "[ ]([A-Za-z]{3,3})[ ]+([0-9]{1,2})[ ]" + "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; /** diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index 4c17f99..938df95 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -78,7 +78,7 @@ class LoadListener extends Handler implements EventHandler { private static int sNativeLoaderCount; - private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(8192); + private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(); private String mUrl; private WebAddress mUri; @@ -104,6 +104,7 @@ class LoadListener extends Handler implements EventHandler { private SslError mSslError; private RequestHandle mRequestHandle; private RequestHandle mSslErrorRequestHandle; + private long mPostIdentifier; // Request data. It is only valid when we are doing a load from the // cache. It is needed if the cache returns a redirect @@ -123,13 +124,13 @@ class LoadListener extends Handler implements EventHandler { // Public functions // ========================================================================= - public static LoadListener getLoadListener( - Context context, BrowserFrame frame, String url, - int nativeLoader, boolean synchronous, boolean isMainPageLoader) { + public static LoadListener getLoadListener(Context context, + BrowserFrame frame, String url, int nativeLoader, + boolean synchronous, boolean isMainPageLoader, long postIdentifier) { sNativeLoaderCount += 1; - return new LoadListener( - context, frame, url, nativeLoader, synchronous, isMainPageLoader); + return new LoadListener(context, frame, url, nativeLoader, synchronous, + isMainPageLoader, postIdentifier); } public static int getNativeLoaderCount() { @@ -137,7 +138,8 @@ class LoadListener extends Handler implements EventHandler { } LoadListener(Context context, BrowserFrame frame, String url, - int nativeLoader, boolean synchronous, boolean isMainPageLoader) { + int nativeLoader, boolean synchronous, boolean isMainPageLoader, + long postIdentifier) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener constructor url=" + url); } @@ -150,6 +152,7 @@ class LoadListener extends Handler implements EventHandler { mMessageQueue = new Vector<Message>(); } mIsMainPageLoader = isMainPageLoader; + mPostIdentifier = postIdentifier; } /** @@ -408,9 +411,14 @@ class LoadListener extends Handler implements EventHandler { mStatusCode == HTTP_MOVED_PERMANENTLY || mStatusCode == HTTP_TEMPORARY_REDIRECT) && mNativeLoader != 0) { - if (!mFromCache && mRequestHandle != null) { + // for POST request, only cache the result if there is an identifier + // associated with it. postUrl() or form submission should set the + // identifier while XHR POST doesn't. + if (!mFromCache && mRequestHandle != null + && (!mRequestHandle.getMethod().equals("POST") + || mPostIdentifier != 0)) { mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, - headers, mMimeType, false); + headers, mMimeType, mPostIdentifier, false); } if (mCacheResult != null) { mCacheResult.encoding = mEncoding; @@ -522,17 +530,18 @@ class LoadListener extends Handler implements EventHandler { * IMPORTANT: as this is called from network thread, can't call native * directly * XXX: Unlike the other network thread methods, this method can do the - * work of decoding the data and appending it to the data builder because - * mDataBuilder is a thread-safe structure. + * work of decoding the data and appending it to the data builder. */ public void data(byte[] data, int length) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.data(): url: " + url()); } - // Synchronize on mData because commitLoad may write mData to WebCore - // and we don't want to replace mData or mDataLength at the same time - // as a write. + // The reason isEmpty() and append() need to synchronized together is + // because it is possible for getFirstChunk() to be called multiple + // times between isEmpty() and append(). This could cause commitLoad() + // to finish before processing the newly appended data and no message + // will be sent. boolean sendMessage = false; synchronized (mDataBuilder) { sendMessage = mDataBuilder.isEmpty(); @@ -636,7 +645,7 @@ class LoadListener extends Handler implements EventHandler { */ boolean checkCache(Map<String, String> headers) { // Get the cache file name for the current URL - CacheResult result = CacheManager.getCacheFile(url(), + CacheResult result = CacheManager.getCacheFile(url(), mPostIdentifier, headers); // Go ahead and set the cache loader to null in case the result is @@ -861,6 +870,10 @@ class LoadListener extends Handler implements EventHandler { } } + long postIdentifier() { + return mPostIdentifier; + } + void attachRequestHandle(RequestHandle requestHandle) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + @@ -907,8 +920,9 @@ class LoadListener extends Handler implements EventHandler { * be used. This is just for forward/back navigation to a POST * URL. */ - static boolean willLoadFromCache(String url) { - boolean inCache = CacheManager.getCacheFile(url, null) != null; + static boolean willLoadFromCache(String url, long identifier) { + boolean inCache = + CacheManager.getCacheFile(url, identifier, null) != null; if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + inCache); @@ -1009,28 +1023,34 @@ class LoadListener extends Handler implements EventHandler { if (mIsMainPageLoader) { String type = sCertificateTypeMap.get(mMimeType); if (type != null) { - // In the case of downloading certificate, we will save it to - // the KeyStore and stop the current loading so that it will not - // generate a new history page - byte[] cert = new byte[mDataBuilder.getByteSize()]; - int offset = 0; - while (true) { - ByteArrayBuilder.Chunk c = mDataBuilder.getFirstChunk(); - if (c == null) break; - - if (c.mLength != 0) { - System.arraycopy(c.mArray, 0, cert, offset, c.mLength); - offset += c.mLength; + // This must be synchronized so that no more data can be added + // after getByteSize returns. + synchronized (mDataBuilder) { + // In the case of downloading certificate, we will save it + // to the KeyStore and stop the current loading so that it + // will not generate a new history page + byte[] cert = new byte[mDataBuilder.getByteSize()]; + int offset = 0; + while (true) { + ByteArrayBuilder.Chunk c = mDataBuilder.getFirstChunk(); + if (c == null) break; + + if (c.mLength != 0) { + System.arraycopy(c.mArray, 0, cert, offset, c.mLength); + offset += c.mLength; + } + c.release(); } - mDataBuilder.releaseChunk(c); + CertTool.addCertificate(mContext, type, cert); + mBrowserFrame.stopLoading(); + return; } - CertTool.addCertificate(mContext, type, cert); - mBrowserFrame.stopLoading(); - return; } } - // Give the data to WebKit now + // Give the data to WebKit now. We don't have to synchronize on + // mDataBuilder here because pulling each chunk removes it from the + // internal list so it cannot be modified. PerfChecker checker = new PerfChecker(); ByteArrayBuilder.Chunk c; while (true) { @@ -1047,7 +1067,7 @@ class LoadListener extends Handler implements EventHandler { } nativeAddData(c.mArray, c.mLength); } - mDataBuilder.releaseChunk(c); + c.release(); checker.responseAlert("res nativeAddData"); } } @@ -1059,7 +1079,7 @@ class LoadListener extends Handler implements EventHandler { void tearDown() { if (mCacheResult != null) { if (getErrorID() == OK) { - CacheManager.saveCacheFile(mUrl, mCacheResult); + CacheManager.saveCacheFile(mUrl, mPostIdentifier, mCacheResult); } // we need to reset mCacheResult to be null @@ -1187,7 +1207,8 @@ class LoadListener extends Handler implements EventHandler { // Cache the redirect response if (mCacheResult != null) { if (getErrorID() == OK) { - CacheManager.saveCacheFile(mUrl, mCacheResult); + CacheManager.saveCacheFile(mUrl, mPostIdentifier, + mCacheResult); } mCacheResult = null; } @@ -1210,8 +1231,17 @@ class LoadListener extends Handler implements EventHandler { // mRequestHandle can be null when the request was satisfied // by the cache, and the cache returned a redirect if (mRequestHandle != null) { - mRequestHandle.setupRedirect(mUrl, mStatusCode, - mRequestHeaders); + try { + mRequestHandle.setupRedirect(mUrl, mStatusCode, + mRequestHeaders); + } catch(RuntimeException e) { + Log.e(LOGTAG, e.getMessage()); + // Signal a bad url error if we could not load the + // redirection. + handleError(EventHandler.ERROR_BAD_URL, + mContext.getString(R.string.httpErrorBadUrl)); + return; + } } else { // If the original request came from the cache, there is no // RequestHandle, we have to create a new one through diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index fffba1b..84a8a3c 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -431,6 +431,8 @@ public class MimeTypeMap { sMimeTypeMap.loadEntry("text/calendar", "icz"); sMimeTypeMap.loadEntry("text/comma-separated-values", "csv"); sMimeTypeMap.loadEntry("text/css", "css"); + sMimeTypeMap.loadEntry("text/html", "htm"); + sMimeTypeMap.loadEntry("text/html", "html"); sMimeTypeMap.loadEntry("text/h323", "323"); sMimeTypeMap.loadEntry("text/iuls", "uls"); sMimeTypeMap.loadEntry("text/mathml", "mml"); @@ -481,6 +483,7 @@ public class MimeTypeMap { sMimeTypeMap.loadEntry("video/dv", "dif"); sMimeTypeMap.loadEntry("video/dv", "dv"); sMimeTypeMap.loadEntry("video/fli", "fli"); + sMimeTypeMap.loadEntry("video/m4v", "m4v"); sMimeTypeMap.loadEntry("video/mpeg", "mpeg"); sMimeTypeMap.loadEntry("video/mpeg", "mpg"); sMimeTypeMap.loadEntry("video/mpeg", "mpe"); diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index af0cb1e..b53e404 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -180,20 +180,24 @@ class Network { } RequestQueue q = mRequestQueue; + RequestHandle handle = null; if (loader.isSynchronous()) { - q = new RequestQueue(loader.getContext(), 1); - } - - RequestHandle handle = q.queueRequest( - url, loader.getWebAddress(), method, headers, loader, - bodyProvider, bodyLength); - loader.attachRequestHandle(handle); - - if (loader.isSynchronous()) { - handle.waitUntilComplete(); + handle = q.queueSynchronousRequest(url, loader.getWebAddress(), + method, headers, loader, bodyProvider, bodyLength); + loader.attachRequestHandle(handle); + handle.processRequest(); loader.loadSynchronousMessages(); - q.shutdown(); + } else { + handle = q.queueRequest(url, loader.getWebAddress(), method, + headers, loader, bodyProvider, bodyLength); + // FIXME: Although this is probably a rare condition, normal network + // requests are processed in a separate thread. This means that it + // is possible to process part of the request before setting the + // request handle on the loader. We should probably refactor this to + // ensure the handle is attached before processing begins. + loader.attachRequestHandle(handle); } + return true; } diff --git a/core/java/android/webkit/PluginUtil.java b/core/java/android/webkit/PluginUtil.java index 8fdbd67..33ccf9d 100644 --- a/core/java/android/webkit/PluginUtil.java +++ b/core/java/android/webkit/PluginUtil.java @@ -19,9 +19,6 @@ import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.util.Log; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - class PluginUtil { private static final String LOGTAG = "PluginUtil"; @@ -35,12 +32,7 @@ class PluginUtil { static PluginStub getPluginStub(Context context, String packageName, String className) { try { - Context pluginContext = context.createPackageContext(packageName, - Context.CONTEXT_INCLUDE_CODE | - Context.CONTEXT_IGNORE_SECURITY); - ClassLoader pluginCL = pluginContext.getClassLoader(); - - Class<?> stubClass = pluginCL.loadClass(className); + Class<?> stubClass = getPluginClass(context, packageName, className); Object stubObject = stubClass.newInstance(); if (stubObject instanceof PluginStub) { @@ -56,4 +48,14 @@ class PluginUtil { } return null; } + + /* package */ + static Class<?> getPluginClass(Context context, String packageName, + String className) throws NameNotFoundException, ClassNotFoundException { + Context pluginContext = context.createPackageContext(packageName, + Context.CONTEXT_INCLUDE_CODE | + Context.CONTEXT_IGNORE_SECURITY); + ClassLoader pluginCL = pluginContext.getClassLoader(); + return pluginCL.loadClass(className); + } } diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index 232ed36..211e5e4 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -367,19 +367,23 @@ public final class URLUtil { /** Regex used to parse content-disposition headers */ private static final Pattern CONTENT_DISPOSITION_PATTERN = - Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$", + Pattern.CASE_INSENSITIVE); /* * Parse the Content-Disposition HTTP Header. The format of the header * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html * This header provides a filename for content that is going to be * downloaded to the file system. We only support the attachment type. + * Note that RFC 2616 specifies the filename value must be double-quoted. + * Unfortunately some servers do not quote the value so to maintain + * consistent behaviour with other browsers, we allow unquoted values too. */ static String parseContentDisposition(String contentDisposition) { try { Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); if (m.find()) { - return m.group(1); + return m.group(2); } } catch (IllegalStateException ex) { // This function is defined as returning null when it can't parse the header diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 92676aa..6adac0b 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -17,6 +17,7 @@ package android.webkit; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Message; import android.view.View; @@ -289,4 +290,13 @@ public class WebChromeClient { public void getVisitedHistory(ValueCallback<String[]> callback) { } + /** + * Tell the client to open a file chooser. + * @param uploadFile A ValueCallback to set the URI of the file to upload. + * onReceiveValue must be called to wake up the thread. + * @hide + */ + public void openFileChooser(ValueCallback<Uri> uploadFile) { + uploadFile.onReceiveValue(null); + } } diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 6f3262a..8e40b23 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1002,7 +1002,8 @@ public class WebSettings { * should never be null. */ public synchronized void setGeolocationDatabasePath(String databasePath) { - if (databasePath != null && !databasePath.equals(mDatabasePath)) { + if (databasePath != null + && !databasePath.equals(mGeolocationDatabasePath)) { mGeolocationDatabasePath = databasePath; postSync(); } diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index e0d41c2..71b1f9f 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -84,13 +84,24 @@ import java.util.ArrayList; // True if the most recent drag event has caused either the TextView to // scroll or the web page to scroll. Gets reset after a touch down. private boolean mScrolled; - // Gets set to true when the the IME jumps to the next textfield. When this - // happens, the next time the user hits a key it is okay for the focus - // pointer to not match the WebTextView's node pointer + // Gets set to true any time the WebTextView has focus, but the navigation + // cache does not yet know that the focus has been changed. This happens + // if the user presses "Next", if the user moves the cursor to a textfield + // and starts typing or clicks the trackball/center key, and when the user + // touches a textfield. boolean mOkayForFocusNotToMatch; // Whether or not a selection change was generated from webkit. If it was, // we do not need to pass the selection back to webkit. private boolean mFromWebKit; + // Whether or not a selection change was generated from the WebTextView + // gaining focus. If it is, we do not want to pass it to webkit. This + // selection comes from the MovementMethod, but we behave differently. If + // WebTextView gained focus from a touch, webkit will determine the + // selection. + private boolean mFromFocusChange; + // Whether or not a selection change was generated from setInputType. We + // do not want to pass this change to webkit. + private boolean mFromSetInputType; private boolean mGotTouchDown; private boolean mInSetTextAndKeepSelection; // Array to store the final character added in onTextChanged, so that its @@ -136,20 +147,23 @@ import java.util.ArrayList; isArrowKey = true; break; } - if (!isArrowKey && !mOkayForFocusNotToMatch - && mWebView.nativeFocusNodePointer() != mNodePointer) { - mWebView.nativeClearCursor(); - // Do not call remove() here, which hides the soft keyboard. If - // the soft keyboard is being displayed, the user will still want - // it there. - mWebView.removeView(this); - mWebView.requestFocus(); - return mWebView.dispatchKeyEvent(event); - } - // After a jump to next textfield and the first key press, the cursor - // and focus will once again match, so reset this value. - mOkayForFocusNotToMatch = false; + if (down) { + if (mOkayForFocusNotToMatch) { + if (mWebView.nativeFocusNodePointer() == mNodePointer) { + mOkayForFocusNotToMatch = false; + } + } else if (mWebView.nativeFocusNodePointer() != mNodePointer + && !isArrowKey) { + mWebView.nativeClearCursor(); + // Do not call remove() here, which hides the soft keyboard. If + // the soft keyboard is being displayed, the user will still want + // it there. + mWebView.removeView(this); + mWebView.requestFocus(); + return mWebView.dispatchKeyEvent(event); + } + } Spannable text = (Spannable) getText(); int oldLength = text.length(); // Normally the delete key's dom events are sent via onTextChanged. @@ -185,7 +199,7 @@ import java.util.ArrayList; } // Center key should be passed to a potential onClick if (!down) { - mWebView.shortPressOnTextField(); + mWebView.centerKeyPressOnTextField(); } // Pass to super to handle longpress. return super.dispatchKeyEvent(event); @@ -303,15 +317,19 @@ import java.util.ArrayList; public void onEditorAction(int actionCode) { switch (actionCode) { case EditorInfo.IME_ACTION_NEXT: + // Since the cursor will no longer be in the same place as the + // focus, set the focus controller back to inactive + mWebView.setFocusControllerInactive(); mWebView.nativeMoveCursorToNextTextInput(); + mOkayForFocusNotToMatch = true; + // Pass the click to set the focus to the textfield which will now + // have the cursor. + mWebView.centerKeyPressOnTextField(); // Preemptively rebuild the WebTextView, so that the action will // be set properly. mWebView.rebuildWebTextView(); - // Since the cursor will no longer be in the same place as the - // focus, set the focus controller back to inactive - mWebView.setFocusControllerInactive(); + setDefaultSelection(); mWebView.invalidate(); - mOkayForFocusNotToMatch = true; break; case EditorInfo.IME_ACTION_DONE: super.onEditorAction(actionCode); @@ -331,6 +349,14 @@ import java.util.ArrayList; } @Override + protected void onFocusChanged(boolean focused, int direction, + Rect previouslyFocusedRect) { + mFromFocusChange = true; + super.onFocusChanged(focused, direction, previouslyFocusedRect); + mFromFocusChange = false; + } + + @Override protected void onSelectionChanged(int selStart, int selEnd) { // This code is copied from TextView.onDraw(). That code does not get // executed, however, because the WebTextView does not draw, allowing @@ -342,7 +368,8 @@ import java.util.ArrayList; int candEnd = EditableInputConnection.getComposingSpanEnd(sp); imm.updateSelection(this, selStart, selEnd, candStart, candEnd); } - if (!mFromWebKit && mWebView != null) { + if (!mFromWebKit && !mFromFocusChange && !mFromSetInputType + && mWebView != null) { if (DebugFlags.WEB_TEXT_VIEW) { Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart + " selEnd=" + selEnd); @@ -591,6 +618,17 @@ import java.util.ArrayList; } /** + * Sets the selection when the user clicks on a textfield or textarea with + * the trackball or center key, or starts typing into it without clicking on + * it. + */ + /* package */ void setDefaultSelection() { + Spannable text = (Spannable) getText(); + int selection = mSingle ? text.length() : 0; + Selection. setSelection(text, selection, selection); + } + + /** * Determine whether to use the system-wide password disguising method, * or to use none. * @param inPassword True if the textfield is a password field. @@ -660,6 +698,13 @@ import java.util.ArrayList; setTextColor(Color.BLACK); } + @Override + public void setInputType(int type) { + mFromSetInputType = true; + super.setInputType(type); + mFromSetInputType = false; + } + /* package */ void setMaxLength(int maxLength) { mMaxLength = maxLength; if (-1 == maxLength) { @@ -761,32 +806,6 @@ import java.util.ArrayList; } /** - * Set the text for this WebTextView, and set the selection to (start, end) - * @param text Text to go into this WebTextView. - * @param start Beginning of the selection. - * @param end End of the selection. - */ - /* package */ void setText(CharSequence text, int start, int end) { - mPreChange = text.toString(); - setText(text); - Spannable span = (Spannable) getText(); - int length = span.length(); - if (end > length) { - end = length; - } - if (start < 0) { - start = 0; - } else if (start > length) { - start = length; - } - if (DebugFlags.WEB_TEXT_VIEW) { - Log.v(LOGTAG, "setText start=" + start - + " end=" + end); - } - Selection.setSelection(span, start, end); - } - - /** * Set the text to the new string, but use the old selection, making sure * to keep it within the new string. * @param text The new text to place in the textfield. diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 142dffb..9999573 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -64,7 +64,9 @@ import android.widget.AbsoluteLayout; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.Scroller; import android.widget.Toast; @@ -82,6 +84,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import junit.framework.Assert; + /** * <p>A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. @@ -390,6 +394,8 @@ public class WebView extends AbsoluteLayout private static final int LONG_PRESS_TIMEOUT = 1000; // needed to avoid flinging after a pause of no movement private static final int MIN_FLING_TIME = 250; + // draw unfiltered after drag is held without movement + private static final int MOTIONLESS_TIME = 100; // The time that the Zoom Controls are visible before fading away private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout(); @@ -427,6 +433,10 @@ public class WebView extends AbsoluteLayout private Scroller mScroller; private boolean mWrapContent; + private static final int MOTIONLESS_FALSE = 0; + private static final int MOTIONLESS_PENDING = 1; + private static final int MOTIONLESS_TRUE = 2; + private int mHeldMotionless; /** * Private message ids @@ -438,6 +448,8 @@ public class WebView extends AbsoluteLayout private static final int RELEASE_SINGLE_TAP = 5; private static final int REQUEST_FORM_DATA = 6; private static final int RESUME_WEBCORE_UPDATE = 7; + private static final int DRAG_HELD_MOTIONLESS = 8; + private static final int AWAKEN_SCROLL_BARS = 9; //! arg1=x, arg2=y static final int SCROLL_TO_MSG_ID = 10; @@ -450,6 +462,7 @@ public class WebView extends AbsoluteLayout static final int UPDATE_TEXT_ENTRY_MSG_ID = 15; static final int WEBCORE_INITIALIZED_MSG_ID = 16; static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17; + static final int FIND_AGAIN = 18; static final int MOVE_OUT_OF_PLUGIN = 19; static final int CLEAR_TEXT_ENTRY = 20; static final int UPDATE_TEXT_SELECTION_MSG_ID = 21; @@ -468,9 +481,9 @@ public class WebView extends AbsoluteLayout "SWITCH_TO_LONGPRESS", // = 4; "RELEASE_SINGLE_TAP", // = 5; "REQUEST_FORM_DATA", // = 6; - "SWITCH_TO_CLICK", // = 7; - "RESUME_WEBCORE_UPDATE", // = 8; - "9", + "RESUME_WEBCORE_UPDATE", // = 7; + "DRAG_HELD_MOTIONLESS", // = 8; + "AWAKEN_SCROLL_BARS", // = 9; "SCROLL_TO_MSG_ID", // = 10; "SCROLL_BY_MSG_ID", // = 11; "SPAWN_SCROLL_TO_MSG_ID", // = 12; @@ -479,7 +492,7 @@ public class WebView extends AbsoluteLayout "UPDATE_TEXT_ENTRY_MSG_ID", // = 15; "WEBCORE_INITIALIZED_MSG_ID", // = 16; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 17; - "18", // = 18; + "FIND_AGAIN", // = 18; "MOVE_OUT_OF_PLUGIN", // = 19; "CLEAR_TEXT_ENTRY", // = 20; "UPDATE_TEXT_SELECTION_MSG_ID", // = 21; @@ -535,11 +548,10 @@ public class WebView extends AbsoluteLayout private boolean mUserScroll = false; private int mSnapScrollMode = SNAP_NONE; - private static final int SNAP_NONE = 1; - private static final int SNAP_X = 2; - private static final int SNAP_Y = 3; - private static final int SNAP_X_LOCK = 4; - private static final int SNAP_Y_LOCK = 5; + private static final int SNAP_NONE = 0; + private static final int SNAP_LOCK = 1; // not a separate state + private static final int SNAP_X = 2; // may be combined with SNAP_LOCK + private static final int SNAP_Y = 4; // may be combined with SNAP_LOCK private boolean mSnapPositive; // Used to match key downs and key ups @@ -2350,6 +2362,7 @@ public class WebView extends AbsoluteLayout } int result = nativeFindAll(find.toLowerCase(), find.toUpperCase()); invalidate(); + mLastFind = find; return result; } @@ -2357,6 +2370,9 @@ public class WebView extends AbsoluteLayout // or not we draw the highlights for matches. private boolean mFindIsUp; private int mFindHeight; + // Keep track of the last string sent, so we can search again after an + // orientation change or the dismissal of the soft keyboard. + private String mLastFind; /** * Return the first substring consisting of the address of a physical @@ -2531,6 +2547,41 @@ public class WebView extends AbsoluteLayout } } + /** + * Called by CallbackProxy when the page finishes loading. + * @param url The URL of the page which has finished loading. + */ + /* package */ void onPageFinished(String url) { + if (mPageThatNeedsToSlideTitleBarOffScreen != null) { + // If the user is now on a different page, or has scrolled the page + // past the point where the title bar is offscreen, ignore the + // scroll request. + if (mPageThatNeedsToSlideTitleBarOffScreen.equals(url) + && mScrollX == 0 && mScrollY == 0) { + pinScrollTo(0, mYDistanceToSlideTitleOffScreen, true, + SLIDE_TITLE_DURATION); + } + mPageThatNeedsToSlideTitleBarOffScreen = null; + } + } + + /** + * The URL of a page that sent a message to scroll the title bar off screen. + * + * Many mobile sites tell the page to scroll to (0,1) in order to scroll the + * title bar off the screen. Sometimes, the scroll position is set before + * the page finishes loading. Rather than scrolling while the page is still + * loading, keep track of the URL and new scroll position so we can perform + * the scroll once the page finishes loading. + */ + private String mPageThatNeedsToSlideTitleBarOffScreen; + + /** + * The destination Y scroll position to be used when the page finishes + * loading. See mPageThatNeedsToSlideTitleBarOffScreen. + */ + private int mYDistanceToSlideTitleOffScreen; + // scale from content to view coordinates, and pin // return true if pin caused the final x/y different than the request cx/cy, // and a future scroll may reach the request cx/cy after our size has @@ -2565,8 +2616,18 @@ public class WebView extends AbsoluteLayout // page, assume this is an attempt to scroll off the title bar, and // animate the title bar off screen slowly enough that the user can see // it. - if (cx == 0 && cy == 1 && mScrollX == 0 && mScrollY == 0) { - pinScrollTo(vx, vy, true, SLIDE_TITLE_DURATION); + if (cx == 0 && cy == 1 && mScrollX == 0 && mScrollY == 0 + && mTitleBar != null) { + // FIXME: 100 should be defined somewhere as our max progress. + if (getProgress() < 100) { + // Wait to scroll the title bar off screen until the page has + // finished loading. Keep track of the URL and the destination + // Y position + mPageThatNeedsToSlideTitleBarOffScreen = getUrl(); + mYDistanceToSlideTitleOffScreen = vy; + } else { + pinScrollTo(vx, vy, true, SLIDE_TITLE_DURATION); + } // Since we are animating, we have not yet reached the desired // scroll position. Do not return true to request another attempt return false; @@ -2607,12 +2668,12 @@ public class WebView extends AbsoluteLayout if (mHeightCanMeasure) { if (getMeasuredHeight() != contentToViewDimension(mContentHeight) - && updateLayout) { + || updateLayout) { requestLayout(); } } else if (mWidthCanMeasure) { if (getMeasuredWidth() != contentToViewDimension(mContentWidth) - && updateLayout) { + || updateLayout) { requestLayout(); } } else { @@ -2632,6 +2693,16 @@ public class WebView extends AbsoluteLayout } /** + * Gets the WebViewClient + * @return the current WebViewClient instance. + * + *@hide pending API council approval. + */ + public WebViewClient getWebViewClient() { + return mCallbackProxy.getWebViewClient(); + } + + /** * Register the interface to be used when content can not be handled by * the rendering engine, and should be downloaded instead. This will replace * the current handler. @@ -2812,10 +2883,7 @@ public class WebView extends AbsoluteLayout public boolean performLongClick() { if (mNativeClass != 0 && nativeCursorIsTextInput()) { // Send the click so that the textfield is in focus - // FIXME: When we start respecting changes to the native textfield's - // selection, need to make sure that this does not change it. - mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), - nativeCursorNodePointer()); + centerKeyPressOnTextField(); rebuildWebTextView(); } if (inEditingMode()) { @@ -2838,6 +2906,21 @@ public class WebView extends AbsoluteLayout */ private boolean mNeedToAdjustWebTextView; + private boolean didUpdateTextViewBounds(boolean allowIntersect) { + Rect contentBounds = nativeFocusCandidateNodeBounds(); + Rect vBox = contentToViewRect(contentBounds); + Rect visibleRect = new Rect(); + calcOurVisibleRect(visibleRect); + if (allowIntersect ? Rect.intersects(visibleRect, vBox) : + visibleRect.contains(vBox)) { + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), + vBox.height()); + return true; + } else { + return false; + } + } + private void drawCoreAndCursorRing(Canvas canvas, int color, boolean drawCursorRing) { if (mDrawHistory) { @@ -2847,8 +2930,22 @@ public class WebView extends AbsoluteLayout } boolean animateZoom = mZoomScale != 0; - boolean animateScroll = !mScroller.isFinished() - || mVelocityTracker != null; + boolean animateScroll = (!mScroller.isFinished() + || mVelocityTracker != null) + && (mTouchMode != TOUCH_DRAG_MODE || + mHeldMotionless != MOTIONLESS_TRUE); + if (mTouchMode == TOUCH_DRAG_MODE) { + if (mHeldMotionless == MOTIONLESS_PENDING) { + mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); + mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); + mHeldMotionless = MOTIONLESS_FALSE; + } + if (mHeldMotionless == MOTIONLESS_FALSE) { + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(DRAG_HELD_MOTIONLESS), MOTIONLESS_TIME); + mHeldMotionless = MOTIONLESS_PENDING; + } + } if (animateZoom) { float zoomScale; int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); @@ -2865,19 +2962,13 @@ public class WebView extends AbsoluteLayout invalidate(); if (mNeedToAdjustWebTextView) { mNeedToAdjustWebTextView = false; - Rect contentBounds = nativeFocusCandidateNodeBounds(); - Rect vBox = contentToViewRect(contentBounds); - Rect visibleRect = new Rect(); - calcOurVisibleRect(visibleRect); - if (visibleRect.contains(vBox)) { - // As a result of the zoom, the textfield is now on - // screen. Place the WebTextView in its new place, - // accounting for our new scroll/zoom values. + // As a result of the zoom, the textfield is now on + // screen. Place the WebTextView in its new place, + // accounting for our new scroll/zoom values. + if (didUpdateTextViewBounds(false)) { mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, contentToViewDimension( nativeFocusCandidateTextSize())); - mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), - vBox.height()); // If it is a password field, start drawing the // WebTextView once again. if (nativeFocusCandidateIsPassword()) { @@ -2929,11 +3020,12 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) return; if (mShiftIsPressed && !animateZoom) { - if (mTouchSelection) { + if (mTouchSelection || mExtendSelection) { nativeDrawSelectionRegion(canvas); - } else { - nativeDrawSelection(canvas, mInvActualScale, getTitleHeight(), - mSelectX, mSelectY, mExtendSelection); + } + if (!mTouchSelection) { + nativeDrawSelectionPointer(canvas, mInvActualScale, mSelectX, + mSelectY - getTitleHeight(), mExtendSelection); } } else if (drawCursorRing) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { @@ -2953,6 +3045,10 @@ public class WebView extends AbsoluteLayout if (mFindIsUp && !animateScroll) { nativeDrawMatches(canvas); } + if (mFocusSizeChanged) { + mFocusSizeChanged = false; + didUpdateTextViewBounds(true); + } } // draw history @@ -3048,6 +3144,16 @@ public class WebView extends AbsoluteLayout imm.hideSoftInputFromWindow(this.getWindowToken(), 0); } + /** + * Only for calling from JNI. Allows a click on an unfocused textfield to + * put the textfield in focus. + */ + private void setOkayNotToMatch() { + if (inEditingMode()) { + mWebTextView.mOkayForFocusNotToMatch = true; + } + } + /* * This method checks the current focus and cursor and potentially rebuilds * mWebTextView to have the appropriate properties, such as password, @@ -3103,6 +3209,7 @@ public class WebView extends AbsoluteLayout && nativeTextGeneration() == mTextGeneration) { mWebTextView.setTextAndKeepSelection(text); } else { + // FIXME: Determine whether this is necessary. Selection.setSelection(spannable, start, end); } } else { @@ -3135,34 +3242,12 @@ public class WebView extends AbsoluteLayout mWebTextView.setSingleLine(isTextField); mWebTextView.setInPassword(nativeFocusCandidateIsPassword()); if (null == text) { - mWebTextView.setText("", 0, 0); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "rebuildWebTextView null == text"); } - } else { - // Change to true to enable the old style behavior, where - // entering a textfield/textarea always set the selection to the - // whole field. This was desirable for the case where the user - // intends to scroll past the field using the trackball. - // However, it causes a problem when replying to emails - the - // user expects the cursor to be at the beginning of the - // textarea. Testing out a new behavior, where textfields set - // selection at the end, and textareas at the beginning. - if (false) { - mWebTextView.setText(text, 0, text.length()); - } else if (isTextField) { - int length = text.length(); - mWebTextView.setText(text, length, length); - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "rebuildWebTextView length=" + length); - } - } else { - mWebTextView.setText(text, 0, 0); - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "rebuildWebTextView !isTextField"); - } - } + text = ""; } + mWebTextView.setTextAndKeepSelection(text); mWebTextView.requestFocus(); } } @@ -3229,23 +3314,22 @@ public class WebView extends AbsoluteLayout if (mShiftIsPressed == false && nativeCursorWantsKeyEvents() == false && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { - mExtendSelection = false; - mShiftIsPressed = true; - if (nativeHasCursorNode()) { - Rect rect = nativeCursorNodeBounds(); - mSelectX = contentToViewX(rect.left); - mSelectY = contentToViewY(rect.top); - } else { - mSelectX = mScrollX + (int) mLastTouchX; - mSelectY = mScrollY + (int) mLastTouchY; - } - nativeHideCursor(); - } + setUpSelectXY(); + } if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { // always handle the navigation keys in the UI thread switchOutDrawHistory(); + if (mShiftIsPressed) { + int xRate = keyCode == KeyEvent.KEYCODE_DPAD_LEFT + ? -1 : keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ? 1 : 0; + int yRate = keyCode == KeyEvent.KEYCODE_DPAD_UP ? + -1 : keyCode == KeyEvent.KEYCODE_DPAD_DOWN ? 1 : 0; + int multiplier = event.getRepeatCount() + 1; + moveSelection(xRate * multiplier, yRate * multiplier); + return true; + } if (navHandledKey(keyCode, 1, false, event.getEventTime(), false)) { playSoundEffect(keyCodeToSoundsEffect(keyCode)); return true; @@ -3257,6 +3341,9 @@ public class WebView extends AbsoluteLayout if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { + if (mShiftIsPressed) { + return true; // discard press if copy in progress + } mGotCenterDown = true; mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(LONG_PRESS_CENTER), LONG_PRESS_TIMEOUT); @@ -3306,10 +3393,7 @@ public class WebView extends AbsoluteLayout } } - if (nativeCursorIsPlugin()) { - nativeUpdatePluginReceivesEvents(); - invalidate(); - } else if (nativeCursorIsTextInput()) { + if (nativeCursorIsTextInput()) { // This message will put the node in focus, for the DOM's notion // of focus, and make the focuscontroller active mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), @@ -3318,13 +3402,17 @@ public class WebView extends AbsoluteLayout // our view system's notion of focus rebuildWebTextView(); // Now we need to pass the event to it - return mWebTextView.onKeyDown(keyCode, event); + if (inEditingMode()) { + mWebTextView.setDefaultSelection(); + mWebTextView.mOkayForFocusNotToMatch = true; + return mWebTextView.dispatchKeyEvent(event); + } } else if (nativeHasFocusNode()) { // In this case, the cursor is not on a text input, but the focus // might be. Check it, and if so, hand over to the WebTextView. rebuildWebTextView(); if (inEditingMode()) { - return mWebTextView.onKeyDown(keyCode, event); + return mWebTextView.dispatchKeyEvent(event); } } @@ -3389,7 +3477,13 @@ public class WebView extends AbsoluteLayout mGotCenterDown = false; if (mShiftIsPressed) { - return false; + if (mExtendSelection) { + commitCopy(); + } else { + mExtendSelection = true; + invalidate(); // draw the i-beam instead of the arrow + } + return true; // discard press if copy in progress } // perform the single click @@ -3399,21 +3493,23 @@ public class WebView extends AbsoluteLayout if (!nativeCursorIntersects(visibleRect)) { return false; } - nativeSetFollowedLink(true); - nativeUpdatePluginReceivesEvents(); WebViewCore.CursorData data = cursorData(); mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, data); playSoundEffect(SoundEffectConstants.CLICK); - boolean isTextInput = nativeCursorIsTextInput(); - if (isTextInput || !mCallbackProxy.uiOverrideUrlLoading( - nativeCursorText())) { + if (nativeCursorIsTextInput()) { + rebuildWebTextView(); + centerKeyPressOnTextField(); + if (inEditingMode()) { + mWebTextView.setDefaultSelection(); + mWebTextView.mOkayForFocusNotToMatch = true; + } + return true; + } + nativeSetFollowedLink(true); + if (!mCallbackProxy.uiOverrideUrlLoading(nativeCursorText())) { mWebViewCore.sendMessage(EventHub.CLICK, data.mFrame, nativeCursorNodePointer()); } - if (isTextInput) { - rebuildWebTextView(); - displaySoftKeyboard(true); - } return true; } @@ -3429,14 +3525,29 @@ public class WebView extends AbsoluteLayout return false; } + private void setUpSelectXY() { + mExtendSelection = false; + mShiftIsPressed = true; + if (nativeHasCursorNode()) { + Rect rect = nativeCursorNodeBounds(); + mSelectX = contentToViewX(rect.left); + mSelectY = contentToViewY(rect.top); + } else if (mLastTouchY > getVisibleTitleHeight()) { + mSelectX = mScrollX + (int) mLastTouchX; + mSelectY = mScrollY + (int) mLastTouchY; + } else { + mSelectX = mScrollX + getViewWidth() / 2; + mSelectY = mScrollY + getViewHeightWithTitle() / 2; + } + nativeHideCursor(); + } + /** * @hide */ public void emulateShiftHeld() { if (0 == mNativeClass) return; // client isn't initialized - mExtendSelection = false; - mShiftIsPressed = true; - nativeHideCursor(); + setUpSelectXY(); } private boolean commitCopy() { @@ -3454,6 +3565,7 @@ public class WebView extends AbsoluteLayout mExtendSelection = false; } mShiftIsPressed = false; + invalidate(); // remove selection region and pointer if (mTouchMode == TOUCH_SELECT_MODE) { mTouchMode = TOUCH_INIT_MODE; } @@ -3597,6 +3709,24 @@ public class WebView extends AbsoluteLayout super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * @hide + */ + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + boolean changed = super.setFrame(left, top, right, bottom); + if (!changed && mHeightCanMeasure) { + // When mHeightCanMeasure is true, we will set mLastHeightSent to 0 + // in WebViewCore after we get the first layout. We do call + // requestLayout() when we get contentSizeChanged(). But the View + // system won't call onSizeChanged if the dimension is not changed. + // In this case, we need to call sendViewSizeZoom() explicitly to + // notify the WebKit about the new dimensions. + sendViewSizeZoom(); + } + return changed; + } + @Override protected void onSizeChanged(int w, int h, int ow, int oh) { super.onSizeChanged(w, h, ow, oh); @@ -3705,8 +3835,10 @@ public class WebView extends AbsoluteLayout mLastSentTouchTime = eventTime; } - int deltaX = (int) (mLastTouchX - x); - int deltaY = (int) (mLastTouchY - y); + float fDeltaX = mLastTouchX - x; + float fDeltaY = mLastTouchY - y; + int deltaX = (int) fDeltaX; + int deltaY = (int) fDeltaY; switch (action) { case MotionEvent.ACTION_DOWN: { @@ -3728,6 +3860,7 @@ public class WebView extends AbsoluteLayout nativeMoveSelection(viewToContentX(mSelectX), viewToContentY(mSelectY), false); mTouchSelection = mExtendSelection = true; + invalidate(); // draw the i-beam instead of the arrow } else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) { @@ -3832,12 +3965,21 @@ public class WebView extends AbsoluteLayout // do pan int newScrollX = pinLocX(mScrollX + deltaX); - deltaX = newScrollX - mScrollX; + int newDeltaX = newScrollX - mScrollX; + if (deltaX != newDeltaX) { + deltaX = newDeltaX; + fDeltaX = (float) newDeltaX; + } int newScrollY = pinLocY(mScrollY + deltaY); - deltaY = newScrollY - mScrollY; + int newDeltaY = newScrollY - mScrollY; + if (deltaY != newDeltaY) { + deltaY = newDeltaY; + fDeltaY = (float) newDeltaY; + } boolean done = false; - if (deltaX == 0 && deltaY == 0) { - done = true; + boolean keepScrollBarsVisible = false; + if (Math.abs(fDeltaX) < 1.0f && Math.abs(fDeltaY) < 1.0f) { + keepScrollBarsVisible = done = true; } else { if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_Y) { int ax = Math.abs(deltaX); @@ -3849,56 +3991,47 @@ public class WebView extends AbsoluteLayout mSnapScrollMode = SNAP_NONE; } // reverse direction means lock in the snap mode - if ((ax > MAX_SLOPE_FOR_DIAG * ay) && - ((mSnapPositive && - deltaX < -mMinLockSnapReverseDistance) - || (!mSnapPositive && - deltaX > mMinLockSnapReverseDistance))) { - mSnapScrollMode = SNAP_X_LOCK; + if (ax > MAX_SLOPE_FOR_DIAG * ay && + (mSnapPositive + ? deltaX < -mMinLockSnapReverseDistance + : deltaX > mMinLockSnapReverseDistance)) { + mSnapScrollMode |= SNAP_LOCK; } } else { // radical change means getting out of snap mode - if ((ax > MAX_SLOPE_FOR_DIAG * ay) + if (ax > MAX_SLOPE_FOR_DIAG * ay && ax > MIN_BREAK_SNAP_CROSS_DISTANCE) { mSnapScrollMode = SNAP_NONE; } // reverse direction means lock in the snap mode - if ((ay > MAX_SLOPE_FOR_DIAG * ax) && - ((mSnapPositive && - deltaY < -mMinLockSnapReverseDistance) - || (!mSnapPositive && - deltaY > mMinLockSnapReverseDistance))) { - mSnapScrollMode = SNAP_Y_LOCK; + if (ay > MAX_SLOPE_FOR_DIAG * ax && + (mSnapPositive + ? deltaY < -mMinLockSnapReverseDistance + : deltaY > mMinLockSnapReverseDistance)) { + mSnapScrollMode |= SNAP_LOCK; } } } - - if (mSnapScrollMode == SNAP_X - || mSnapScrollMode == SNAP_X_LOCK) { - if (deltaX == 0) { - // keep the scrollbar on the screen even there is no - // scroll - awakenScrollBars(ViewConfiguration - .getScrollDefaultDelay(), false); + if (mSnapScrollMode != SNAP_NONE) { + if ((mSnapScrollMode & SNAP_X) == SNAP_X) { + deltaY = 0; } else { - scrollBy(deltaX, 0); + deltaX = 0; } - mLastTouchX = x; - } else if (mSnapScrollMode == SNAP_Y - || mSnapScrollMode == SNAP_Y_LOCK) { - if (deltaY == 0) { - // keep the scrollbar on the screen even there is no - // scroll - awakenScrollBars(ViewConfiguration - .getScrollDefaultDelay(), false); - } else { - scrollBy(0, deltaY); + } + if ((deltaX | deltaY) != 0) { + scrollBy(deltaX, deltaY); + if (deltaX != 0) { + mLastTouchX = x; } - mLastTouchY = y; + if (deltaY != 0) { + mLastTouchY = y; + } + mHeldMotionless = MOTIONLESS_FALSE; } else { - scrollBy(deltaX, deltaY); - mLastTouchX = x; - mLastTouchY = y; + // keep the scrollbar on the screen even there is no + // scroll + keepScrollBarsVisible = true; } mLastTouchTime = eventTime; mUserScroll = true; @@ -3917,13 +4050,17 @@ public class WebView extends AbsoluteLayout } } - if (done) { + if (keepScrollBarsVisible) { + if (mHeldMotionless != MOTIONLESS_TRUE) { + mHeldMotionless = MOTIONLESS_TRUE; + invalidate(); + } // keep the scrollbar on the screen even there is no scroll awakenScrollBars(ViewConfiguration.getScrollDefaultDelay(), false); // return false to indicate that we can't pan out of the // view space - return false; + return !done; } break; } @@ -3976,6 +4113,9 @@ public class WebView extends AbsoluteLayout break; } case TOUCH_DRAG_MODE: + mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); + mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); + mHeldMotionless = MOTIONLESS_TRUE; // redraw in high-quality, as we're done dragging invalidate(); // if the user waits a while w/o moving before the @@ -4015,6 +4155,9 @@ public class WebView extends AbsoluteLayout } mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); + mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); + mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); + mHeldMotionless = MOTIONLESS_TRUE; mTouchMode = TOUCH_DONE_MODE; nativeHideCursor(); break; @@ -4041,6 +4184,7 @@ public class WebView extends AbsoluteLayout private static final int SELECT_CURSOR_OFFSET = 16; private int mSelectX = 0; private int mSelectY = 0; + private boolean mFocusSizeChanged = false; private boolean mShiftIsPressed = false; private boolean mTrackballDown = false; private long mTrackballUpTime = 0; @@ -4099,6 +4243,7 @@ public class WebView extends AbsoluteLayout commitCopy(); } else { mExtendSelection = true; + invalidate(); // draw the i-beam instead of the arrow } return true; // discard press if copy in progress } @@ -4146,8 +4291,8 @@ public class WebView extends AbsoluteLayout return; int width = getViewWidth(); int height = getViewHeight(); - mSelectX += scaleTrackballX(xRate, width); - mSelectY += scaleTrackballY(yRate, height); + mSelectX += xRate; + mSelectY += yRate; int maxX = width + mScrollX; int maxY = height + mScrollY; mSelectX = Math.min(maxX, Math.max(mScrollX - SELECT_CURSOR_OFFSET @@ -4229,8 +4374,11 @@ public class WebView extends AbsoluteLayout } float xRate = mTrackballRemainsX * 1000 / elapsed; float yRate = mTrackballRemainsY * 1000 / elapsed; + int viewWidth = getViewWidth(); + int viewHeight = getViewHeight(); if (mShiftIsPressed) { - moveSelection(xRate, yRate); + moveSelection(scaleTrackballX(xRate, viewWidth), + scaleTrackballY(yRate, viewHeight)); mTrackballRemainsX = mTrackballRemainsY = 0; return; } @@ -4244,8 +4392,8 @@ public class WebView extends AbsoluteLayout + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } - int width = mContentWidth - getViewWidth(); - int height = mContentHeight - getViewHeight(); + int width = mContentWidth - viewWidth; + int height = mContentHeight - viewHeight; if (width < 0) width = 0; if (height < 0) height = 0; ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER); @@ -4320,7 +4468,7 @@ public class WebView extends AbsoluteLayout int vy = (int) mVelocityTracker.getYVelocity(); if (mSnapScrollMode != SNAP_NONE) { - if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_X_LOCK) { + if ((mSnapScrollMode & SNAP_X) == SNAP_X) { vy = 0; } else { vx = 0; @@ -4600,21 +4748,21 @@ public class WebView extends AbsoluteLayout } int x = viewToContentX((int) event.getX() + mWebTextView.getLeft()); int y = viewToContentY((int) event.getY() + mWebTextView.getTop()); - // In case the soft keyboard has been dismissed, bring it back up. - InputMethodManager.getInstance(getContext()).showSoftInput(mWebTextView, - 0); if (nativeFocusNodePointer() != nativeCursorNodePointer()) { nativeMotionUp(x, y, mNavSlop); } nativeTextInputMotionUp(x, y); } - /*package*/ void shortPressOnTextField() { - if (inEditingMode()) { - View v = mWebTextView; - int x = viewToContentX((v.getLeft() + v.getRight()) >> 1); - int y = viewToContentY((v.getTop() + v.getBottom()) >> 1); - nativeTextInputMotionUp(x, y); + /** + * Called when pressing the center key or trackball on a textfield. + */ + /*package*/ void centerKeyPressOnTextField() { + mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), + nativeCursorNodePointer()); + // Need to show the soft keyboard if it's not readonly. + if (!nativeCursorIsReadOnly()) { + displaySoftKeyboard(true); } } @@ -4684,15 +4832,6 @@ public class WebView extends AbsoluteLayout mCallbackProxy.uiOverrideUrlLoading(url); } - // called by JNI - private void sendPluginState(int state) { - WebViewCore.PluginStateData psd = new WebViewCore.PluginStateData(); - psd.mFrame = nativeCursorFramePointer(); - psd.mNode = nativeCursorNodePointer(); - psd.mState = state; - mWebViewCore.sendMessage(EventHub.PLUGIN_STATE, psd); - } - @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { boolean result = false; @@ -4841,14 +4980,6 @@ public class WebView extends AbsoluteLayout } /* package */ void passToJavaScript(String currentText, KeyEvent event) { - if (nativeCursorWantsKeyEvents() && !nativeCursorMatchesFocus()) { - mWebViewCore.sendMessage(EventHub.CLICK); - if (mWebTextView.mOkayForFocusNotToMatch) { - int select = nativeFocusCandidateIsTextField() ? - nativeFocusCandidateMaxLength() : 0; - setSelection(select, select); - } - } WebViewCore.JSKeyData arg = new WebViewCore.JSKeyData(); arg.mEvent = event; arg.mCurrentText = currentText; @@ -4879,9 +5010,10 @@ public class WebView extends AbsoluteLayout class PrivateHandler extends Handler { @Override public void handleMessage(Message msg) { - if (DebugFlags.WEB_VIEW) { + // exclude INVAL_RECT_MSG_ID since it is frequently output + if (DebugFlags.WEB_VIEW && msg.what != INVAL_RECT_MSG_ID) { Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what - > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) + > REQUEST_KEYBOARD ? Integer.toString(msg.what) : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); } if (mWebViewCore == null) { @@ -5043,6 +5175,9 @@ public class WebView extends AbsoluteLayout / mZoomOverviewWidth, false); } } + if (draw.mFocusSizeChanged && inEditingMode()) { + mFocusSizeChanged = true; + } break; } case WEBCORE_INITIALIZED_MSG_ID: @@ -5083,9 +5218,7 @@ public class WebView extends AbsoluteLayout } break; case MOVE_OUT_OF_PLUGIN: - if (nativePluginEatsNavKey()) { - navHandledKey(msg.arg1, 1, false, 0, true); - } + navHandledKey(msg.arg1, 1, false, 0, true); break; case UPDATE_TEXT_ENTRY_MSG_ID: // this is sent after finishing resize in WebViewCore. Make @@ -5169,9 +5302,36 @@ public class WebView extends AbsoluteLayout hideSoftKeyboard(); } else { displaySoftKeyboard(false); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "REQUEST_KEYBOARD" + + " focusCandidateIsPlugin=" + + nativeFocusCandidateIsPlugin()); + } } break; + case FIND_AGAIN: + // Ignore if find has been dismissed. + if (mFindIsUp) { + findAll(mLastFind); + } + break; + + case DRAG_HELD_MOTIONLESS: + mHeldMotionless = MOTIONLESS_TRUE; + invalidate(); + // fall through to keep scrollbars awake + + case AWAKEN_SCROLL_BARS: + if (mTouchMode == TOUCH_DRAG_MODE + && mHeldMotionless == MOTIONLESS_TRUE) { + awakenScrollBars(ViewConfiguration + .getScrollDefaultDelay(), false); + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(AWAKEN_SCROLL_BARS), + ViewConfiguration.getScrollDefaultDelay()); + } + break; default: super.handleMessage(msg); break; @@ -5195,8 +5355,16 @@ public class WebView extends AbsoluteLayout // Need these to provide stable ids to my ArrayAdapter, // which normally does not have stable ids. (Bug 1250098) private class Container extends Object { + /** + * Possible values for mEnabled. Keep in sync with OptionStatus in + * WebViewCore.cpp + */ + final static int OPTGROUP = -1; + final static int OPTION_DISABLED = 0; + final static int OPTION_ENABLED = 1; + String mString; - boolean mEnabled; + int mEnabled; int mId; public String toString() { @@ -5217,6 +5385,54 @@ public class WebView extends AbsoluteLayout } @Override + public View getView(int position, View convertView, + ViewGroup parent) { + // Always pass in null so that we will get a new CheckedTextView + // Otherwise, an item which was previously used as an <optgroup> + // element (i.e. has no check), could get used as an <option> + // element, which needs a checkbox/radio, but it would not have + // one. + convertView = super.getView(position, null, parent); + Container c = item(position); + if (c != null && Container.OPTION_ENABLED != c.mEnabled) { + // ListView does not draw dividers between disabled and + // enabled elements. Use a LinearLayout to provide dividers + LinearLayout layout = new LinearLayout(mContext); + layout.setOrientation(LinearLayout.VERTICAL); + if (position > 0) { + View dividerTop = new View(mContext); + dividerTop.setBackgroundResource( + android.R.drawable.divider_horizontal_bright); + layout.addView(dividerTop); + } + + if (Container.OPTGROUP == c.mEnabled) { + // Currently select_dialog_multichoice and + // select_dialog_singlechoice are CheckedTextViews. If + // that changes, the class cast will no longer be valid. + Assert.assertTrue( + convertView instanceof CheckedTextView); + ((CheckedTextView) convertView).setCheckMarkDrawable( + null); + } else { + // c.mEnabled == Container.OPTION_DISABLED + // Draw the disabled element in a disabled state. + convertView.setEnabled(false); + } + + layout.addView(convertView); + if (position < getCount() - 1) { + View dividerBottom = new View(mContext); + dividerBottom.setBackgroundResource( + android.R.drawable.divider_horizontal_bright); + layout.addView(dividerBottom); + } + return layout; + } + return convertView; + } + + @Override public boolean hasStableIds() { // AdapterView's onChanged method uses this to determine whether // to restore the old state. Return false so that the old (out @@ -5251,12 +5467,11 @@ public class WebView extends AbsoluteLayout if (item == null) { return false; } - return item.mEnabled; + return Container.OPTION_ENABLED == item.mEnabled; } } - private InvokeListBox(String[] array, - boolean[] enabled, int[] selected) { + private InvokeListBox(String[] array, int[] enabled, int[] selected) { mMultiple = true; mSelectedArray = selected; @@ -5270,8 +5485,7 @@ public class WebView extends AbsoluteLayout } } - private InvokeListBox(String[] array, boolean[] enabled, int - selection) { + private InvokeListBox(String[] array, int[] enabled, int selection) { mSelection = selection; mMultiple = false; @@ -5402,10 +5616,11 @@ public class WebView extends AbsoluteLayout * Request a dropdown menu for a listbox with multiple selection. * * @param array Labels for the listbox. - * @param enabledArray Which positions are enabled. + * @param enabledArray State for each element in the list. See static + * integers in Container class. * @param selectedArray Which positions are initally selected. */ - void requestListBox(String[] array, boolean[]enabledArray, int[] + void requestListBox(String[] array, int[] enabledArray, int[] selectedArray) { mPrivateHandler.post( new InvokeListBox(array, enabledArray, selectedArray)); @@ -5416,10 +5631,11 @@ public class WebView extends AbsoluteLayout * <select> element. * * @param array Labels for the listbox. - * @param enabledArray Which positions are enabled. + * @param enabledArray State for each element in the list. See static + * integers in Container class. * @param selection Which position is initally selected. */ - void requestListBox(String[] array, boolean[]enabledArray, int selection) { + void requestListBox(String[] array, int[] enabledArray, int selection) { mPrivateHandler.post( new InvokeListBox(array, enabledArray, selection)); } @@ -5503,7 +5719,7 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) { return false; } - if (ignorePlugin == false && nativePluginEatsNavKey()) { + if (ignorePlugin == false && nativeFocusIsPlugin()) { KeyEvent event = new KeyEvent(time, time, KeyEvent.ACTION_DOWN , keyCode, count, (mShiftIsPressed ? KeyEvent.META_SHIFT_ON : 0) | (false ? KeyEvent.META_ALT_ON : 0) // FIXME @@ -5578,6 +5794,17 @@ public class WebView extends AbsoluteLayout } /** + * Draw the HTML page into the specified canvas. This call ignores any + * view-specific zoom, scroll offset, or other changes. It does not draw + * any view-specific chrome, such as progress or URL bars. + * + * @hide only needs to be accessible to Browser and testing + */ + public void drawPage(Canvas canvas) { + mWebViewCore.drawContentPicture(canvas, 0, false, false); + } + + /** * Update our cache with updatedText. * @param updatedText The new text to put in our cache. */ @@ -5595,7 +5822,7 @@ public class WebView extends AbsoluteLayout /* package */ native boolean nativeCursorMatchesFocus(); private native boolean nativeCursorIntersects(Rect visibleRect); private native boolean nativeCursorIsAnchor(); - private native boolean nativeCursorIsPlugin(); + private native boolean nativeCursorIsReadOnly(); private native boolean nativeCursorIsTextInput(); private native Point nativeCursorPosition(); private native String nativeCursorText(); @@ -5608,13 +5835,15 @@ public class WebView extends AbsoluteLayout private native void nativeDestroy(); private native void nativeDrawCursorRing(Canvas content); private native void nativeDrawMatches(Canvas canvas); - private native void nativeDrawSelection(Canvas content, float scale, - int offset, int x, int y, boolean extendSelection); + private native void nativeDrawSelectionPointer(Canvas content, + float scale, int x, int y, boolean extendSelection); private native void nativeDrawSelectionRegion(Canvas content); private native void nativeDumpDisplayTree(String urlOrNull); private native int nativeFindAll(String findLower, String findUpper); private native void nativeFindNext(boolean forward); + private native int nativeFocusCandidateFramePointer(); private native boolean nativeFocusCandidateIsPassword(); + private native boolean nativeFocusCandidateIsPlugin(); private native boolean nativeFocusCandidateIsRtlText(); private native boolean nativeFocusCandidateIsTextField(); private native boolean nativeFocusCandidateIsTextInput(); @@ -5624,6 +5853,7 @@ public class WebView extends AbsoluteLayout /* package */ native int nativeFocusCandidatePointer(); private native String nativeFocusCandidateText(); private native int nativeFocusCandidateTextSize(); + private native boolean nativeFocusIsPlugin(); /* package */ native int nativeFocusNodePointer(); private native Rect nativeGetCursorRingBounds(); private native Region nativeGetSelection(); @@ -5641,7 +5871,6 @@ public class WebView extends AbsoluteLayout private native int nativeMoveGeneration(); private native void nativeMoveSelection(int x, int y, boolean extendSelection); - private native boolean nativePluginEatsNavKey(); // Like many other of our native methods, you must make sure that // mNativeClass is not null before calling this method. private native void nativeRecordButtons(boolean focused, @@ -5663,7 +5892,6 @@ public class WebView extends AbsoluteLayout // we always want to pass in our generation number. private native void nativeUpdateCachedTextfield(String updatedText, int generation); - private native void nativeUpdatePluginReceivesEvents(); // return NO_LEFTEDGE means failure. private static final int NO_LEFTEDGE = -1; private native int nativeGetBlockLeftEdge(int x, int y, float scale); diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index a5a4852..5460a47 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -18,6 +18,8 @@ package android.webkit; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.Cursor; import android.graphics.Canvas; import android.graphics.DrawFilter; import android.graphics.Paint; @@ -26,11 +28,13 @@ import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.provider.Browser; +import android.provider.OpenableColumns; import android.util.Log; import android.util.SparseBooleanArray; import android.view.KeyEvent; @@ -273,6 +277,39 @@ final class WebViewCore { mCallbackProxy.onJsAlert(url, message); } + + /** + * Called by JNI. Open a file chooser to upload a file. + * @return String version of the URI plus the name of the file. + * FIXME: Just return the URI here, and in FileSystem::pathGetFileName, call + * into Java to get the filename. + */ + private String openFileChooser() { + Uri uri = mCallbackProxy.openFileChooser(); + if (uri == null) return ""; + // Find out the name, and append it to the URI. + // Webkit will treat the name as the filename, and + // the URI as the path. The URI will be used + // in BrowserFrame to get the actual data. + Cursor cursor = mContext.getContentResolver().query( + uri, + new String[] { OpenableColumns.DISPLAY_NAME }, + null, + null, + null); + String name = ""; + if (cursor != null) { + try { + if (cursor.moveToNext()) { + name = cursor.getString(0); + } + } finally { + cursor.close(); + } + } + return uri.toString() + "/" + name; + } + /** * Notify the browser that the origin has exceeded it's database quota. * @param url The URL that caused the overflow. @@ -422,6 +459,8 @@ final class WebViewCore { */ private native boolean nativeRecordContent(Region invalRegion, Point wh); + private native boolean nativeFocusBoundsChanged(); + /** * Splits slow parts of the picture set. Called from the webkit * thread after nativeDrawContent returns true. @@ -522,8 +561,6 @@ final class WebViewCore { */ private native void nativeSetNewStorageLimit(long limit); - private native void nativeUpdatePluginState(int framePtr, int nodePtr, int state); - /** * Provide WebCore with a Geolocation permission state for the specified * origin. @@ -678,12 +715,6 @@ final class WebViewCore { int mY; } - static class PluginStateData { - int mFrame; - int mNode; - int mState; - } - static class GeolocationPermissionsData { String mOrigin; boolean mAllow; @@ -720,7 +751,7 @@ final class WebViewCore { "SINGLE_LISTBOX_CHOICE", // = 124; "MESSAGE_RELAY", // = 125; "SET_BACKGROUND_COLOR", // = 126; - "PLUGIN_STATE", // = 127; + "127", // = 127 "SAVE_DOCUMENT_STATE", // = 128; "GET_SELECTION", // = 129; "WEBKIT_DRAW", // = 130; @@ -771,7 +802,6 @@ final class WebViewCore { static final int SINGLE_LISTBOX_CHOICE = 124; static final int MESSAGE_RELAY = 125; static final int SET_BACKGROUND_COLOR = 126; - static final int PLUGIN_STATE = 127; // plugin notifications static final int SAVE_DOCUMENT_STATE = 128; static final int GET_SELECTION = 129; static final int WEBKIT_DRAW = 130; @@ -1031,11 +1061,6 @@ final class WebViewCore { nativeFreeMemory(); break; - case PLUGIN_STATE: - PluginStateData psd = (PluginStateData) msg.obj; - nativeUpdatePluginState(psd.mFrame, psd.mNode, psd.mState); - break; - case SET_NETWORK_STATE: if (BrowserFrame.sJavaBridge == null) { throw new IllegalStateException("No WebView " + @@ -1593,6 +1618,7 @@ final class WebViewCore { int mMinPrefWidth; RestoreState mRestoreState; // only non-null if it is for the first // picture set after the first layout + boolean mFocusSizeChanged; } private void webkitDraw() { @@ -1607,6 +1633,7 @@ final class WebViewCore { if (mWebView != null) { // Send the native view size that was used during the most recent // layout. + draw.mFocusSizeChanged = nativeFocusBoundsChanged(); draw.mViewPoint = new Point(mCurrentViewWidth, mCurrentViewHeight); if (mSettings.getUseWideViewPort()) { draw.mMinPrefWidth = Math.max( @@ -1643,6 +1670,9 @@ final class WebViewCore { final DrawFilter mZoomFilter = new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); + final DrawFilter mScrollFilter = null; + // If we need to trade more speed for less quality on slower devices + // use this: new PaintFlagsDrawFilter(SCROLL_BITS, 0); /* package */ void drawContentPicture(Canvas canvas, int color, boolean animatingZoom, @@ -1651,7 +1681,7 @@ final class WebViewCore { if (animatingZoom) { df = mZoomFilter; } else if (animatingScroll) { - df = null; + df = mScrollFilter; } canvas.setDrawFilter(df); boolean tookTooLong = nativeDrawContent(canvas, color); @@ -2013,9 +2043,16 @@ final class WebViewCore { // know the exact scale. If mRestoredScale is non-zero, use it; // otherwise just use mTextWrapScale as the initial scale. data.mScale = mRestoreState.mViewScale == 0 - ? (mRestoredScale > 0 ? mRestoredScale + ? (mRestoredScale > 0 ? mRestoredScale / 100.0f : mRestoreState.mTextWrapScale) : mRestoreState.mViewScale; + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, "setupViewport" + + " mRestoredScale=" + mRestoredScale + + " mViewScale=" + mRestoreState.mViewScale + + " mTextWrapScale=" + mRestoreState.mTextWrapScale + ); + } data.mWidth = Math.round(webViewWidth / data.mScale); data.mHeight = mCurrentViewHeight * data.mWidth / viewportWidth; data.mTextWrapWidth = Math.round(webViewWidth @@ -2086,6 +2123,13 @@ final class WebViewCore { WebView.CLEAR_TEXT_ENTRY).sendToTarget(); } + // called by JNI + private void sendFindAgain() { + if (mWebView == null) return; + Message.obtain(mWebView.mPrivateHandler, + WebView.FIND_AGAIN).sendToTarget(); + } + private native void nativeUpdateFrameCacheIfLoading(); /** @@ -2099,7 +2143,7 @@ final class WebViewCore { private native void nativeSetGlobalBounds(int x, int y, int w, int h); // called by JNI - private void requestListBox(String[] array, boolean[] enabledArray, + private void requestListBox(String[] array, int[] enabledArray, int[] selectedArray) { if (mWebView != null) { mWebView.requestListBox(array, enabledArray, selectedArray); @@ -2107,7 +2151,7 @@ final class WebViewCore { } // called by JNI - private void requestListBox(String[] array, boolean[] enabledArray, + private void requestListBox(String[] array, int[] enabledArray, int selection) { if (mWebView != null) { mWebView.requestListBox(array, enabledArray, selection); @@ -2124,6 +2168,32 @@ final class WebViewCore { } } + // called by JNI + private Class<?> getPluginClass(String libName, String clsName) { + + if (mWebView == null) { + return null; + } + + String pkgName = PluginManager.getInstance(null).getPluginsAPKName(libName); + if (pkgName == null) { + Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); + return null; + } + + Class<?> pluginClass = null; + try { + pluginClass = PluginUtil.getPluginClass(mWebView.getContext(), pkgName, clsName); + } catch (NameNotFoundException e) { + Log.e(LOGTAG, "Unable to find plugin classloader for the apk (" + pkgName + ")"); + } catch (ClassNotFoundException e) { + Log.e(LOGTAG, "Unable to find plugin class (" + clsName + + ") in the apk (" + pkgName + ")"); + } + + return pluginClass; + } + // called by JNI. PluginWidget function to launch an activity and overlays // the activity with the View provided by the plugin class. private void startFullScreenPluginActivity(String libName, String clsName, int npp) { @@ -2173,6 +2243,11 @@ final class WebViewCore { return view; } + private void updateSurface(ViewManager.ChildView childView, int x, int y, + int width, int height) { + childView.attachView(x, y, width, height); + } + private void destroySurface(ViewManager.ChildView childView) { childView.removeView(); } diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index 6e10811..110e4f8 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -27,6 +27,7 @@ import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; import android.util.Log; import android.webkit.CookieManager.Cookie; @@ -174,7 +175,16 @@ public class WebViewDatabase { public static synchronized WebViewDatabase getInstance(Context context) { if (mInstance == null) { mInstance = new WebViewDatabase(); - mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null); + try { + mDatabase = context + .openOrCreateDatabase(DATABASE_FILE, 0, null); + } catch (SQLiteException e) { + // try again by deleting the old db and create a new one + if (context.deleteDatabase(DATABASE_FILE)) { + mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, + null); + } + } // mDatabase should not be null, // the only case is RequestAPI test has problem to create db @@ -194,8 +204,16 @@ public class WebViewDatabase { mDatabase.setLockingEnabled(false); } - mCacheDatabase = context.openOrCreateDatabase(CACHE_DATABASE_FILE, - 0, null); + try { + mCacheDatabase = context.openOrCreateDatabase( + CACHE_DATABASE_FILE, 0, null); + } catch (SQLiteException e) { + // try again by deleting the old db and create a new one + if (context.deleteDatabase(CACHE_DATABASE_FILE)) { + mCacheDatabase = context.openOrCreateDatabase( + CACHE_DATABASE_FILE, 0, null); + } + } // mCacheDatabase should not be null, // the only case is RequestAPI test has problem to create db diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 271989a..e353501 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -3563,6 +3563,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // into the scrap heap int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { + removeDetachedView(scrap, false); return; } diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index fd590ed..aa9062b 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -117,11 +117,11 @@ public class CheckedTextView extends TextView implements Checkable { * @param d The Drawable to use for the checkmark. */ public void setCheckMarkDrawable(Drawable d) { + if (mCheckMarkDrawable != null) { + mCheckMarkDrawable.setCallback(null); + unscheduleDrawable(mCheckMarkDrawable); + } if (d != null) { - if (mCheckMarkDrawable != null) { - mCheckMarkDrawable.setCallback(null); - unscheduleDrawable(mCheckMarkDrawable); - } d.setCallback(this); d.setVisible(getVisibility() == VISIBLE, false); d.setState(CHECKED_STATE_SET); @@ -130,10 +130,10 @@ public class CheckedTextView extends TextView implements Checkable { mCheckMarkWidth = d.getIntrinsicWidth(); mPaddingRight = mCheckMarkWidth + mBasePaddingRight; d.setState(getDrawableState()); - mCheckMarkDrawable = d; } else { mPaddingRight = mBasePaddingRight; } + mCheckMarkDrawable = d; requestLayout(); } diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index 8019f14..07c3e4b 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -25,9 +25,9 @@ import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.QuickContact; import android.provider.ContactsContract.Intents; import android.provider.ContactsContract.PhoneLookup; +import android.provider.ContactsContract.QuickContact; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.CommonDataKinds.Email; import android.util.AttributeSet; @@ -55,21 +55,28 @@ public class QuickContactBadge extends ImageView implements OnClickListener { static final private int TOKEN_PHONE_LOOKUP = 1; static final private int TOKEN_EMAIL_LOOKUP_AND_TRIGGER = 2; static final private int TOKEN_PHONE_LOOKUP_AND_TRIGGER = 3; + static final private int TOKEN_CONTACT_LOOKUP_AND_TRIGGER = 4; static final String[] EMAIL_LOOKUP_PROJECTION = new String[] { RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY, }; - static int EMAIL_ID_COLUMN_INDEX = 0; - static int EMAIL_LOOKUP_STRING_COLUMN_INDEX = 1; + static final int EMAIL_ID_COLUMN_INDEX = 0; + static final int EMAIL_LOOKUP_STRING_COLUMN_INDEX = 1; static final String[] PHONE_LOOKUP_PROJECTION = new String[] { PhoneLookup._ID, PhoneLookup.LOOKUP_KEY, }; - static int PHONE_ID_COLUMN_INDEX = 0; - static int PHONE_LOOKUP_STRING_COLUMN_INDEX = 1; + static final int PHONE_ID_COLUMN_INDEX = 0; + static final int PHONE_LOOKUP_STRING_COLUMN_INDEX = 1; + static final String[] CONTACT_LOOKUP_PROJECTION = new String[] { + Contacts._ID, + Contacts.LOOKUP_KEY, + }; + static final int CONTACT_ID_COLUMN_INDEX = 0; + static final int CONTACT_LOOKUPKEY_COLUMN_INDEX = 1; public QuickContactBadge(Context context) { @@ -181,9 +188,9 @@ public class QuickContactBadge extends ImageView implements OnClickListener { public void onClick(View v) { if (mContactUri != null) { - final ContentResolver resolver = getContext().getContentResolver(); - final Uri lookupUri = Contacts.getLookupUri(resolver, mContactUri); - trigger(lookupUri); + mQueryHandler.startQuery(TOKEN_CONTACT_LOOKUP_AND_TRIGGER, null, + mContactUri, + CONTACT_LOOKUP_PROJECTION, null, null, null); } else if (mContactEmail != null) { mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail, Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)), @@ -249,6 +256,17 @@ public class QuickContactBadge extends ImageView implements OnClickListener { lookupUri = Contacts.getLookupUri(contactId, lookupKey); } } + + case TOKEN_CONTACT_LOOKUP_AND_TRIGGER: { + if (cursor != null && cursor.moveToFirst()) { + long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); + String lookupKey = cursor.getString(CONTACT_LOOKUPKEY_COLUMN_INDEX); + lookupUri = Contacts.getLookupUri(contactId, lookupKey); + trigger = true; + } + + break; + } } } finally { if (cursor != null) { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 6771711..3b1f7a0 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -457,6 +457,46 @@ public class RemoteViews implements Parcelable, Filter { } } + /** + * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the + * given {@link RemoteViews}, or calling {@link ViewGroup#removeAllViews()} + * when null. This allows users to build "nested" {@link RemoteViews}. + */ + private class ViewGroupAction extends Action { + public ViewGroupAction(int viewId, RemoteViews nestedViews) { + this.viewId = viewId; + this.nestedViews = nestedViews; + } + + public ViewGroupAction(Parcel parcel) { + viewId = parcel.readInt(); + nestedViews = parcel.readParcelable(null); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeParcelable(nestedViews, 0 /* no flags */); + } + + @Override + public void apply(View root) { + final Context context = root.getContext(); + final ViewGroup target = (ViewGroup) root.findViewById(viewId); + if (nestedViews != null) { + // Inflate nested views and add as children + target.addView(nestedViews.apply(context, target)); + } else if (target != null) { + // Clear all children when nested views omitted + target.removeAllViews(); + } + } + + int viewId; + RemoteViews nestedViews; + + public final static int TAG = 4; + } /** * Create a new RemoteViews object that will display the views contained @@ -493,6 +533,9 @@ public class RemoteViews implements Parcelable, Filter { case ReflectionAction.TAG: mActions.add(new ReflectionAction(parcel)); break; + case ViewGroupAction.TAG: + mActions.add(new ViewGroupAction(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -519,7 +562,31 @@ public class RemoteViews implements Parcelable, Filter { } mActions.add(a); } - + + /** + * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the + * given {@link RemoteViews}. This allows users to build "nested" + * {@link RemoteViews}. In cases where consumers of {@link RemoteViews} may + * recycle layouts, use {@link #removeAllViews(int)} to clear any existing + * children. + * + * @param viewId The id of the parent {@link ViewGroup} to add child into. + * @param nestedView {@link RemoteViews} that describes the child. + */ + public void addView(int viewId, RemoteViews nestedView) { + addAction(new ViewGroupAction(viewId, nestedView)); + } + + /** + * Equivalent to calling {@link ViewGroup#removeAllViews()}. + * + * @param viewId The id of the parent {@link ViewGroup} to remove all + * children from. + */ + public void removeAllViews(int viewId) { + addAction(new ViewGroupAction(viewId, null)); + } + /** * Equivalent to calling View.setVisibility * diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 596fd98..f55ca3f 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -5197,7 +5197,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mDesiredHeightAtMeasure = desired; if (heightMode == MeasureSpec.AT_MOST) { - height = Math.min(desired, height); + height = Math.min(desired, heightSize); } } diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index 8a7946b..2dd79b2 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -16,8 +16,10 @@ package android.widget; - +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.TypedArray; import android.os.Handler; import android.os.Message; @@ -30,10 +32,19 @@ import android.widget.RemoteViews.RemoteView; * requested, can automatically flip between each child at a regular interval. * * @attr ref android.R.styleable#ViewFlipper_flipInterval + * @attr ref android.R.styleable#ViewFlipper_autoStart */ +@RemoteView public class ViewFlipper extends ViewAnimator { - private int mFlipInterval = 3000; - private boolean mKeepFlipping = false; + private static final int DEFAULT_INTERVAL = 3000; + + private int mFlipInterval = DEFAULT_INTERVAL; + private boolean mAutoStart = false; + + private boolean mRunning = false; + private boolean mStarted = false; + private boolean mVisible = false; + private boolean mUserPresent = true; public ViewFlipper(Context context) { super(context); @@ -44,14 +55,62 @@ public class ViewFlipper extends ViewAnimator { TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewFlipper); - mFlipInterval = a.getInt(com.android.internal.R.styleable.ViewFlipper_flipInterval, - 3000); + mFlipInterval = a.getInt( + com.android.internal.R.styleable.ViewFlipper_flipInterval, DEFAULT_INTERVAL); + mAutoStart = a.getBoolean( + com.android.internal.R.styleable.ViewFlipper_autoStart, false); a.recycle(); } + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + mUserPresent = false; + updateRunning(); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + mUserPresent = true; + updateRunning(); + } + } + }; + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Listen for broadcasts related to user-presence + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + getContext().registerReceiver(mReceiver, filter); + + if (mAutoStart) { + // Automatically start when requested + startFlipping(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mVisible = false; + + getContext().unregisterReceiver(mReceiver); + updateRunning(); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + mVisible = visibility == VISIBLE; + updateRunning(); + } + /** * How long to wait before flipping to the next view - * + * * @param milliseconds * time in milliseconds */ @@ -64,26 +123,57 @@ public class ViewFlipper extends ViewAnimator { * Start a timer to cycle through child views */ public void startFlipping() { - if (!mKeepFlipping) { - mKeepFlipping = true; - showOnly(mWhichChild); - Message msg = mHandler.obtainMessage(FLIP_MSG); - mHandler.sendMessageDelayed(msg, mFlipInterval); - } + mStarted = true; + updateRunning(); } /** * No more flips */ public void stopFlipping() { - mKeepFlipping = false; + mStarted = false; + updateRunning(); + } + + /** + * Internal method to start or stop dispatching flip {@link Message} based + * on {@link #mRunning} and {@link #mVisible} state. + */ + private void updateRunning() { + boolean running = mVisible && mStarted && mUserPresent; + if (running != mRunning) { + if (running) { + showOnly(mWhichChild); + Message msg = mHandler.obtainMessage(FLIP_MSG); + mHandler.sendMessageDelayed(msg, mFlipInterval); + } else { + mHandler.removeMessages(FLIP_MSG); + } + mRunning = running; + } } /** * Returns true if the child views are flipping. */ public boolean isFlipping() { - return mKeepFlipping; + return mStarted; + } + + /** + * Set if this view automatically calls {@link #startFlipping()} when it + * becomes attached to a window. + */ + public void setAutoStart(boolean autoStart) { + mAutoStart = autoStart; + } + + /** + * Returns true if this view automatically calls {@link #startFlipping()} + * when it becomes attached to a window. + */ + public boolean isAutoStart() { + return mAutoStart; } private final int FLIP_MSG = 1; @@ -92,7 +182,7 @@ public class ViewFlipper extends ViewAnimator { @Override public void handleMessage(Message msg) { if (msg.what == FLIP_MSG) { - if (mKeepFlipping) { + if (mRunning) { showNext(); msg = obtainMessage(FLIP_MSG); sendMessageDelayed(msg, mFlipInterval); diff --git a/core/java/com/android/internal/os/IDropBoxService.aidl b/core/java/com/android/internal/os/IDropBoxService.aidl new file mode 100644 index 0000000..f940041 --- /dev/null +++ b/core/java/com/android/internal/os/IDropBoxService.aidl @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.os; + +import android.os.DropBox; +import android.os.ParcelFileDescriptor; + +/** + * "Backend" interface used by {@link android.os.DropBox} to talk to the + * DropBoxService that actually implements the drop box functionality. + * + * @see DropBox + * @hide + */ +interface IDropBoxService { + /** + * @see DropBox#addText + * @see DropBox#addData + * @see DropBox#addFile + */ + void add(in DropBox.Entry entry); + + /** @see DropBox#getNextEntry */ + boolean isTagEnabled(String tag); + + /** @see DropBox#getNextEntry */ + DropBox.Entry getNextEntry(String tag, long millis); +} diff --git a/core/jni/android/graphics/Canvas.cpp b/core/jni/android/graphics/Canvas.cpp index dc72008..e1e9536 100644 --- a/core/jni/android/graphics/Canvas.cpp +++ b/core/jni/android/graphics/Canvas.cpp @@ -27,6 +27,9 @@ #include "SkShader.h" #include "SkTemplates.h" +#include "SkBoundaryPatch.h" +#include "SkMeshUtils.h" + #define TIME_DRAWx static uint32_t get_thread_msec() { @@ -861,8 +864,6 @@ public: *matrix = canvas->getTotalMatrix(); } }; - -/////////////////////////////////////////////////////////////////////////////// static JNINativeMethod gCanvasMethods[] = { {"finalizer", "(I)V", (void*) SkCanvasGlue::finalizer}, @@ -965,6 +966,42 @@ static JNINativeMethod gCanvasMethods[] = { {"freeCaches", "()V", (void*) SkCanvasGlue::freeCaches} }; +/////////////////////////////////////////////////////////////////////////////// + +static void BoundaryPatch_computeCubic(JNIEnv* env, jobject, jfloatArray jpts, + int texW, int texH, int rows, int cols, + jfloatArray jverts, jshortArray jidx) { + AutoJavaFloatArray ptsArray(env, jpts, 24, kRO_JNIAccess); + + int vertCount = rows * cols; + AutoJavaFloatArray vertsArray(env, jverts, vertCount * 4, kRW_JNIAccess); + SkPoint* verts = (SkPoint*)vertsArray.ptr(); + SkPoint* texs = verts + vertCount; + + int idxCount = (rows - 1) * (cols - 1) * 6; + AutoJavaShortArray idxArray(env, jidx, idxCount, kRW_JNIAccess); + uint16_t* idx = (uint16_t*)idxArray.ptr(); // cast from int16_t* + + SkCubicBoundary cubic; + memcpy(cubic.fPts, ptsArray.ptr(), 12 * sizeof(SkPoint)); + + SkBoundaryPatch patch; + patch.setBoundary(&cubic); + // generate our verts + patch.evalPatch(verts, rows, cols); + + SkMeshIndices mesh; + // generate our texs and idx + mesh.init(texs, idx, texW, texH, rows, cols); +} + +static JNINativeMethod gBoundaryPatchMethods[] = { + {"nativeComputeCubicPatch", "([FIIII[F[S)V", + (void*)BoundaryPatch_computeCubic }, +}; + +/////////////////////////////////////////////////////////////////////////////// + #include <android_runtime/AndroidRuntime.h> #define REG(env, name, array) \ @@ -976,6 +1013,7 @@ int register_android_graphics_Canvas(JNIEnv* env) { int result; REG(env, "android/graphics/Canvas", gCanvasMethods); + REG(env, "android/graphics/utils/BoundaryPatch", gBoundaryPatchMethods); return result; } diff --git a/core/jni/android/graphics/Graphics.cpp b/core/jni/android/graphics/Graphics.cpp index 2e0caed..01aad93 100644 --- a/core/jni/android/graphics/Graphics.cpp +++ b/core/jni/android/graphics/Graphics.cpp @@ -56,7 +56,7 @@ bool GraphicsJNI::hasException(JNIEnv *env) { /////////////////////////////////////////////////////////////////////////////// AutoJavaFloatArray::AutoJavaFloatArray(JNIEnv* env, jfloatArray array, - int minLength) + int minLength, JNIAccess access) : fEnv(env), fArray(array), fPtr(NULL), fLen(0) { SkASSERT(env); if (array) { @@ -66,11 +66,12 @@ AutoJavaFloatArray::AutoJavaFloatArray(JNIEnv* env, jfloatArray array, } fPtr = env->GetFloatArrayElements(array, NULL); } + fReleaseMode = (access == kRO_JNIAccess) ? JNI_ABORT : 0; } AutoJavaFloatArray::~AutoJavaFloatArray() { if (fPtr) { - fEnv->ReleaseFloatArrayElements(fArray, fPtr, 0); + fEnv->ReleaseFloatArrayElements(fArray, fPtr, fReleaseMode); } } @@ -94,7 +95,7 @@ AutoJavaIntArray::~AutoJavaIntArray() { } AutoJavaShortArray::AutoJavaShortArray(JNIEnv* env, jshortArray array, - int minLength) + int minLength, JNIAccess access) : fEnv(env), fArray(array), fPtr(NULL), fLen(0) { SkASSERT(env); if (array) { @@ -104,11 +105,12 @@ AutoJavaShortArray::AutoJavaShortArray(JNIEnv* env, jshortArray array, } fPtr = env->GetShortArrayElements(array, NULL); } + fReleaseMode = (access == kRO_JNIAccess) ? JNI_ABORT : 0; } AutoJavaShortArray::~AutoJavaShortArray() { if (fPtr) { - fEnv->ReleaseShortArrayElements(fArray, fPtr, 0); + fEnv->ReleaseShortArrayElements(fArray, fPtr, fReleaseMode); } } diff --git a/core/jni/android/graphics/GraphicsJNI.h b/core/jni/android/graphics/GraphicsJNI.h index 7adadbc..fe24b05 100644 --- a/core/jni/android/graphics/GraphicsJNI.h +++ b/core/jni/android/graphics/GraphicsJNI.h @@ -80,9 +80,15 @@ private: bool fReportSizeToVM; }; +enum JNIAccess { + kRO_JNIAccess, + kRW_JNIAccess +}; + class AutoJavaFloatArray { public: - AutoJavaFloatArray(JNIEnv* env, jfloatArray array, int minLength = 0); + AutoJavaFloatArray(JNIEnv* env, jfloatArray array, + int minLength = 0, JNIAccess = kRW_JNIAccess); ~AutoJavaFloatArray(); float* ptr() const { return fPtr; } @@ -93,6 +99,7 @@ private: jfloatArray fArray; float* fPtr; int fLen; + int fReleaseMode; }; class AutoJavaIntArray { @@ -112,7 +119,8 @@ private: class AutoJavaShortArray { public: - AutoJavaShortArray(JNIEnv* env, jshortArray array, int minLength = 0); + AutoJavaShortArray(JNIEnv* env, jshortArray array, + int minLength = 0, JNIAccess = kRW_JNIAccess); ~AutoJavaShortArray(); jshort* ptr() const { return fPtr; } @@ -123,6 +131,7 @@ private: jshortArray fArray; jshort* fPtr; int fLen; + int fReleaseMode; }; class AutoJavaByteArray { diff --git a/core/jni/android_net_wifi_Wifi.cpp b/core/jni/android_net_wifi_Wifi.cpp index 38f3fda..46000c9 100644 --- a/core/jni/android_net_wifi_Wifi.cpp +++ b/core/jni/android_net_wifi_Wifi.cpp @@ -20,6 +20,7 @@ #include <utils/misc.h> #include <android_runtime/AndroidRuntime.h> #include <utils/Log.h> +#include <utils/String16.h> #include "wifi.h" @@ -92,7 +93,8 @@ static jstring doStringCommand(JNIEnv *env, const char *cmd) if (doCommand(cmd, reply, sizeof(reply)) != 0) { return env->NewStringUTF(NULL); } else { - return env->NewStringUTF(reply); + String16 str((char *)reply); + return env->NewString((const jchar *)str.string(), str.size()); } } diff --git a/core/res/assets/images/combobox-disabled.png b/core/res/assets/images/combobox-disabled.png Binary files differindex 42fc0c5..fe220e4 100644 --- a/core/res/assets/images/combobox-disabled.png +++ b/core/res/assets/images/combobox-disabled.png diff --git a/core/res/assets/images/combobox-noHighlight.png b/core/res/assets/images/combobox-noHighlight.png Binary files differindex 838dc65..abcdf72 100644 --- a/core/res/assets/images/combobox-noHighlight.png +++ b/core/res/assets/images/combobox-noHighlight.png diff --git a/core/res/assets/webkit/youtube.html b/core/res/assets/webkit/youtube.html index 2aaaa15..45d9c5e 100644 --- a/core/res/assets/webkit/youtube.html +++ b/core/res/assets/webkit/youtube.html @@ -30,14 +30,8 @@ </head> <body> <div id="bg"> - <table height="100%" width="100%" border="0" cellpadding="0" - cellspacing="0"> - <tr> - <td valign="middle"> - <img src="http://img.youtube.com/vi/VIDEO_ID/0.jpg" width="100%"/> - </td> - </tr> - </table> + <img src="http://img.youtube.com/vi/VIDEO_ID/0.jpg" + style="width:100%; height:100%"/> </div> <div id="main"> <table height="100%" width="100%"> diff --git a/core/res/res/layout/alert_dialog.xml b/core/res/res/layout/alert_dialog.xml index cf2de05..40e3f42 100644 --- a/core/res/res/layout/alert_dialog.xml +++ b/core/res/res/layout/alert_dialog.xml @@ -50,11 +50,11 @@ android:paddingTop="6dip" android:paddingRight="10dip" android:src="@drawable/ic_dialog_info" /> - <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle" + <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle" style="?android:attr/textAppearanceLarge" android:singleLine="true" android:ellipsize="end" - android:layout_width="fill_parent" + android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> <ImageView android:id="@+id/titleDivider" @@ -63,9 +63,7 @@ android:visibility="gone" android:scaleType="fitXY" android:gravity="fill_horizontal" - android:src="@android:drawable/dialog_divider_horizontal_light" - android:layout_marginLeft="10dip" - android:layout_marginRight="10dip"/> + android:src="@android:drawable/divider_horizontal_dark" /> <!-- If the client uses a customTitle, it will be added here. --> </LinearLayout> @@ -88,7 +86,7 @@ android:padding="5dip" /> </ScrollView> </LinearLayout> - + <FrameLayout android:id="@+id/customPanel" android:layout_width="fill_parent" android:layout_height="wrap_content" @@ -99,13 +97,13 @@ android:paddingTop="5dip" android:paddingBottom="5dip" /> </FrameLayout> - + <LinearLayout android:id="@+id/buttonPanel" android:layout_width="fill_parent" android:layout_height="wrap_content" android:minHeight="54dip" - android:orientation="vertical" > - <LinearLayout + android:orientation="vertical" > + <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" diff --git a/core/res/res/layout/grant_credentials_permission.xml b/core/res/res/layout/grant_credentials_permission.xml index fe1c22e..84b6623 100644 --- a/core/res/res/layout/grant_credentials_permission.xml +++ b/core/res/res/layout/grant_credentials_permission.xml @@ -1,41 +1,163 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -/* -** Copyright 2007, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ +/** + * Copyright (c) 2008, Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_height="fill_parent"> + + <!-- The header --> <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@+id/message" /> - <Button android:id="@+id/allow" + android:id="@+id/header_text" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:text="@string/allow" /> + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="@color/white" + android:textStyle="bold" + android:text="@string/grant_permissions_header_text" + android:shadowColor="@color/shadow" + android:shadowRadius="2" + android:singleLine="true" + android:background="@drawable/title_bar_medium" + android:gravity="left|center_vertical" + android:paddingLeft="19dip" + android:ellipsize="marquee" /> - <Button android:id="@+id/deny" + <!-- The list of packages that correspond to the requesting UID + and the account/authtokenType that is being requested --> + <ScrollView android:layout_width="fill_parent" android:layout_height="wrap_content" - android:text="@string/deny" /> + android:fillViewport="true" + android:layout_weight="1" + android:gravity="top|center_horizontal" + android:foreground="@drawable/title_bar_shadow"> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:paddingTop="14dip" + android:orientation="vertical"> + + <TextView + android:id="@+id/grant_credentials_permission_message_header" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/grant_credentials_permission_message_header" + android:textAppearance="?android:attr/textAppearanceMedium" + android:paddingLeft="19dip" + android:paddingBottom="12dip" /> + + <LinearLayout + android:id="@+id/packages_list" + android:orientation="vertical" + android:paddingLeft="16dip" + android:paddingRight="12dip" + android:layout_width="fill_parent" + android:layout_height="wrap_content" /> + + <RelativeLayout + android:paddingLeft="16dip" + android:paddingRight="12dip" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/permission_icon" + android:layout_width="30dip" + android:layout_height="30dip" + android:src="@drawable/ic_bullet_key_permission" + android:layout_alignParentLeft="true" + android:scaleType="fitCenter" /> + + <TextView + android:id="@+id/account_type" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="@color/perms_dangerous_perm_color" + android:textStyle="bold" + android:paddingLeft="6dip" + android:layout_toRightOf="@id/permission_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> - <ListView android:id="@+id/packages_list" - android:layout_width="fill_parent" android:layout_height="fill_parent"/> + <TextView + android:id="@+id/account_name" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@color/perms_dangerous_perm_color" + android:layout_marginTop="-4dip" + android:paddingBottom="8dip" + android:paddingLeft="6dip" + android:layout_below="@id/account_type" + android:layout_toRightOf="@id/permission_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/authtoken_type" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="@color/perms_dangerous_perm_color" + android:textStyle="bold" + android:layout_marginTop="-4dip" + android:paddingBottom="8dip" + android:paddingLeft="6dip" + android:layout_below="@id/account_name" + android:layout_toRightOf="@id/permission_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + </RelativeLayout> + + <TextView + android:id="@+id/grant_credentials_permission_message_footer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/grant_credentials_permission_message_footer" + android:textAppearance="?android:attr/textAppearanceMedium" + android:paddingLeft="19dip" + android:paddingBottom="12dip" /> + </LinearLayout> + </ScrollView> + + <!-- The buttons to allow or deny --> + <LinearLayout + android:id="@+id/buttons" + android:layout_width="fill_parent" + android:layout_height="52dip" + android:background="@drawable/bottom_bar" + android:paddingTop="4dip" + android:paddingLeft="2dip" + android:paddingRight="2dip"> + + <Button + android:id="@+id/allow_button" + android:text="@string/allow" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="2" /> + + <Button + android:id="@+id/deny_button" + android:text="@string/deny" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="2" /> + + </LinearLayout> </LinearLayout> + diff --git a/core/res/res/layout/permissions_account_and_authtokentype.xml b/core/res/res/layout/permissions_account_and_authtokentype.xml new file mode 100644 index 0000000..4494a2c --- /dev/null +++ b/core/res/res/layout/permissions_account_and_authtokentype.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- + Defines the layout of an account and authtoken type permission item. + Contains an icon, the account type and name and the authtoken type. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/permission_icon" + android:layout_width="30dip" + android:layout_height="30dip" + android:drawable="@drawable/ic_bullet_key_permission" + android:layout_alignParentLeft="true" + android:scaleType="fitCenter" /> + + + <TextView + android:id="@+id/account_type" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + android:paddingLeft="6dip" + android:layout_toRightOf="@id/permission_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/account_name" + android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_marginTop="-4dip" + android:paddingBottom="8dip" + android:paddingLeft="6dip" + android:layout_below="@id/account_type" + android:layout_toRightOf="@id/permission_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/authtoken_type" + android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_marginTop="-4dip" + android:paddingBottom="8dip" + android:paddingLeft="6dip" + android:layout_below="@id/account_name" + android:layout_toRightOf="@id/permission_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + +</RelativeLayout> diff --git a/core/res/res/layout/permissions_package_list_item.xml b/core/res/res/layout/permissions_package_list_item.xml new file mode 100644 index 0000000..1bffe51 --- /dev/null +++ b/core/res/res/layout/permissions_package_list_item.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- + Defines the layout of a single package item. + Contains a bullet point icon and the name of the package. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/package_icon" + android:layout_width="30dip" + android:layout_height="30dip" + android:layout_alignParentLeft="true" + android:src="@drawable/ic_text_dot" + android:scaleType="fitCenter" /> + + + <TextView + android:id="@+id/package_label" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + android:paddingLeft="6dip" + android:layout_toRightOf="@id/package_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + +</RelativeLayout> diff --git a/core/res/res/layout/screen_title_icons.xml b/core/res/res/layout/screen_title_icons.xml index 4d7a6c8..5415909 100644 --- a/core/res/res/layout/screen_title_icons.xml +++ b/core/res/res/layout/screen_title_icons.xml @@ -66,22 +66,25 @@ This is the basic layout for a screen, with all of its features enabled. android:layout_toLeftOf="@id/progress_circular" android:layout_toRightOf="@android:id/left_icon" > - <!-- 2dip between the icon and the title text, if icon is present. --> - <ImageView android:id="@android:id/right_icon" - android:visibility="gone" - android:layout_width="16dip" - android:layout_height="16dip" - android:layout_gravity="center_vertical" - android:scaleType="fitCenter" - android:layout_marginRight="2dip" /> <TextView android:id="@android:id/title" style="?android:attr/windowTitleStyle" - android:layout_width="fill_parent" + android:layout_width="0dip" android:layout_height="fill_parent" + android:layout_weight="1" android:background="@null" android:fadingEdge="horizontal" android:scrollHorizontally="true" android:gravity="center_vertical" + android:layout_marginRight="2dip" + /> + <!-- 2dip between the icon and the title text, if icon is present. --> + <ImageView android:id="@android:id/right_icon" + android:visibility="gone" + android:layout_width="16dip" + android:layout_height="16dip" + android:layout_weight="0" + android:layout_gravity="center_vertical" + android:scaleType="fitCenter" /> </LinearLayout> </RelativeLayout> diff --git a/core/res/res/values-cs/strings.xml b/core/res/res/values-cs/strings.xml index f93fb01..6b5b168 100644 --- a/core/res/res/values-cs/strings.xml +++ b/core/res/res/values-cs/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Vytvořit kontakt"\n"pro <xliff:g id="NUMBER">%s</xliff:g>."</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"Zaškrtnuto"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"Nezaškrtnuto"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Uvedené aplikace od <xliff:g id="APPLICATION">%2$s</xliff:g> požadují oprávnění přistupovat k přihlašovacím údajům účtu <xliff:g id="ACCOUNT">%1$s</xliff:g>. Chcete toto oprávnění udělit? Pokud je udělíte, vaše odpověď se uloží a tato výzva se již nebude zobrazovat."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Uvedené aplikace požadují od <xliff:g id="APPLICATION">%3$s</xliff:g> oprávnění přistupovat k přihlašovacím údajům (typ: <xliff:g id="TYPE">%1$s</xliff:g>) účtu <xliff:g id="ACCOUNT">%2$s</xliff:g>. Chcete toto oprávnění udělit? Pokud je udělíte, vaše odpověď se uloží a tato výzva se již nebude zobrazovat."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Povolit"</string> <string name="deny" msgid="2081879885755434506">"Odepřít"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Požadováno oprávnění"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protokol L2TP (Layer 2 Tunneling Protocol)"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"Síť VPN L2TP/IPSec s předsdíleným klíčem"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Síť VPN L2TP/IPSec s certifikátem"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml index fa89fea..33c21e3 100644 --- a/core/res/res/values-da/strings.xml +++ b/core/res/res/values-da/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Opret kontakt"\n"ved hjælp af <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"kontrolleret"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"ikke kontrolleret"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"De nævnte programmer beder om tilladelse til at få adgang til loginoplysningerne til kontoen <xliff:g id="ACCOUNT">%1$s</xliff:g> fra <xliff:g id="APPLICATION">%2$s</xliff:g>. Ønsker du at give denne tilladelse? Hvis ja, så huskes dit svar, og du vil ikke blive spurgt om det igen."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"De nævnte programmer beder om tilladelse til at få adgang til <xliff:g id="TYPE">%1$s</xliff:g>-loginoplysningerne til kontoen <xliff:g id="ACCOUNT">%2$s</xliff:g> fra <xliff:g id="APPLICATION">%3$s</xliff:g>. Vil du give denne tilladelse? Hvis ja, så huskes dit svar, og du vil ikke blive spurgt om det igen."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Tillad"</string> <string name="deny" msgid="2081879885755434506">"Afvis"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Der er anmodet om tilladelse"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Layer 2 Tunneling Protocol"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"L2TP/IPSec VPN baseret på forhåndsdelt nøglekodning"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Certifikatbaseret L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml index ebf3b66..167faa0 100644 --- a/core/res/res/values-de/strings.xml +++ b/core/res/res/values-de/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Neuer Kontakt"\n"mit <xliff:g id="NUMBER">%s</xliff:g> erstellen"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"aktiviert"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"nicht aktiviert"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Die aufgelisteten Anwendungen fordern eine Berechtigung zum Zugriff auf die Anmeldeinformationen für das Konto <xliff:g id="ACCOUNT">%1$s</xliff:g> von <xliff:g id="APPLICATION">%2$s</xliff:g> an. Möchten Sie diese Berechtigung erteilen? Wenn ja, wird Ihre Antwort gespeichert und Sie erhalten keine erneute Aufforderung."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Die aufgelisteten Anwendungen fordern eine Berechtigung zum Zugriff auf die <xliff:g id="TYPE">%1$s</xliff:g>-Anmeldeinformationen für das Konto <xliff:g id="APPLICATION">%3$s</xliff:g> von <xliff:g id="ACCOUNT">%2$s</xliff:g> an. Möchten Sie diese Berechtigung erteilen? Wenn ja, wird Ihre Antwort gespeichert und Sie erhalten keine erneute Aufforderung."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Zulassen"</string> <string name="deny" msgid="2081879885755434506">"Ablehnen"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Berechtigung angefordert"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Layer-2-Tunneling-Protokoll"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"L2TP/IPSec-VPN mit vorinstalliertem Schlüssel"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Zertifikat mit vorinstalliertem Schlüssel"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml index 062658d..5b02a65 100644 --- a/core/res/res/values-el/strings.xml +++ b/core/res/res/values-el/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Δημιουργία επαφής"\n"με τη χρήση του <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"επιλεγμένο"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"δεν ελέγχθηκε"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Οι εφαρμογές που παραθέτονται στη λίστα ζητούν άδεια για να αποκτήσουν πρόσβαση στα διαπιστευτήρια σύνδεσης για τον λογαριασμό <xliff:g id="ACCOUNT">%1$s</xliff:g> από <xliff:g id="APPLICATION">%2$s</xliff:g>. Θα αποδεχτείτε το αίτημα; Εάν το αποδεχτείτε, η απάντησή σας θα αποθηκευτεί και δεν θα ερωτηθείτε ξανά."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Οι εφαρμογές που παραθέτονται στη λίστα ζητούν άδεια για να αποκτήσουν πρόσβαση στα διαπιστευτήρια σύνδεσης <xliff:g id="TYPE">%1$s</xliff:g> για τον λογαριασμό <xliff:g id="ACCOUNT">%2$s</xliff:g> από <xliff:g id="APPLICATION">%3$s</xliff:g>. Θα αποδεχτείτε το αίτημα; Εάν το αποδεχτείτε, η απάντησή σας θα αποθηκευτεί και δεν θα ερωτηθείτε ξανά."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Να επιτρέπεται"</string> <string name="deny" msgid="2081879885755434506">"Άρνηση"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Απαιτείται άδεια"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Πρωτόκολλο Layer 2 Tunneling Protocol (L2TP)"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"Κλειδί pre-shared βάσει L2TP/IPSec VPN"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Πιστοποιητικό βάσει L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml index 88f16a9..e0408b3 100644 --- a/core/res/res/values-es-rUS/strings.xml +++ b/core/res/res/values-es-rUS/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Crear contacto "\n"con <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"verificado"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"no verificado"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Las aplicaciones enumeradas requieren permiso para acceder a las credenciales de inicio de sesión para la cuenta <xliff:g id="ACCOUNT">%1$s</xliff:g> desde <xliff:g id="APPLICATION">%2$s</xliff:g> ¿Deseas otorgar este permiso? Si es así, el sistema recordará tu respuesta y no volverá a solicitarla."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Las aplicaciones enumeradas requieren permiso para acceder a las <xliff:g id="TYPE">%1$s</xliff:g> credenciales de inicio de sesión para la cuenta <xliff:g id="ACCOUNT">%2$s</xliff:g> desde <xliff:g id="APPLICATION">%3$s</xliff:g>.¿Deseas otorgar este permiso? Si es así, el sistema recordará tu respuesta y no volverá a solicitarla."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Permitir"</string> <string name="deny" msgid="2081879885755434506">"Denegar"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Permiso solicitado"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protocolo de túnel de nivel 2"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"Clave previamente compartida según L2TP/IPSec VPN"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Certificado según L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml index 2757978..fa6160a 100644 --- a/core/res/res/values-es/strings.xml +++ b/core/res/res/values-es/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Crear un contacto"\n"a partir de <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"seleccionado"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"no seleccionado"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Las aplicaciones de la lista están solicitando permiso para acceder a las credenciales de acceso de la cuenta <xliff:g id="ACCOUNT">%1$s</xliff:g> desde <xliff:g id="APPLICATION">%2$s</xliff:g>. ¿Deseas conceder este permiso? En tal caso, se recordará tu respuesta y no se te volverá a preguntar."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Las aplicaciones de la lista están solicitando permiso para acceder a las credenciales de acceso (<xliff:g id="TYPE">%1$s</xliff:g>) de la cuenta <xliff:g id="ACCOUNT">%2$s</xliff:g> desde <xliff:g id="APPLICATION">%3$s</xliff:g>. ¿Deseas conceder este permiso? En tal caso, se recordará tu respuesta y no se te volverá a preguntar."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Permitir"</string> <string name="deny" msgid="2081879885755434506">"Denegar"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Permiso solicitado"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protocolo de túnel de nivel 2"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"Red privada virtual L2TP/IPSec basada en clave compartida previamente"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Red privada virtual L2TP/IPSec basada en certificado"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml index 7a07a7c..75f7406 100644 --- a/core/res/res/values-fr/strings.xml +++ b/core/res/res/values-fr/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Ajouter un contact"\n"en utilisant <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"sélectionné"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"non sélectionné"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Les applications répertoriées demandent l\'autorisation d\'accéder aux informations d\'identification du compte <xliff:g id="ACCOUNT">%1$s</xliff:g> depuis <xliff:g id="APPLICATION">%2$s</xliff:g>. Souhaitez-vous accorder cette autorisation ? Si vous acceptez, votre choix sera enregistré et cette question ne vous sera plus posée."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Les applications répertoriées demandent l\'autorisation d\'accéder aux informations d\'identification des <xliff:g id="TYPE">%1$s</xliff:g> associés au compte <xliff:g id="ACCOUNT">%2$s</xliff:g> depuis <xliff:g id="APPLICATION">%3$s</xliff:g>. Souhaitez-vous accorder cette autorisation ? Si vous acceptez, votre choix sera enregistré et cette question ne vous sera plus posée."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Autoriser"</string> <string name="deny" msgid="2081879885755434506">"Refuser"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Autorisation demandée"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protocole de tunnelisation de niveau 2"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"Clé pré-partagée basée sur L2TP/IPSec VPN"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Certificat basé sur L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml index 8d2aefa..f89b9ad 100644 --- a/core/res/res/values-it/strings.xml +++ b/core/res/res/values-it/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Crea contatto"\n"utilizzando <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"selezionato"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"non selezionato"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Le applicazioni elencate richiedono l\'autorizzazione per accedere alle credenziali di accesso per l\'account <xliff:g id="ACCOUNT">%1$s</xliff:g> da <xliff:g id="APPLICATION">%2$s</xliff:g>. Concedere questa autorizzazione? In tal caso la tua risposta verrà memorizzata e questa domanda non ti verrà più posta."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Le applicazioni elencate richiedono l\'autorizzazione per accedere alle credenziali di accesso <xliff:g id="TYPE">%1$s</xliff:g> per l\'account <xliff:g id="ACCOUNT">%2$s</xliff:g> da <xliff:g id="APPLICATION">%3$s</xliff:g>. Concedere questa autorizzazione? In tal caso la tua risposta verrà memorizzata e questa domanda non ti verrà più posta."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Consenti"</string> <string name="deny" msgid="2081879885755434506">"Nega"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Autorizzazione richiesta"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protocollo di tunneling livello 2"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"VPN L2TP/IPSec basata su chiave precondivisa"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"VPN L2TP/IPSec basata su certificato"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml index 8c3836c..e26ece2 100644 --- a/core/res/res/values-ja/strings.xml +++ b/core/res/res/values-ja/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"<xliff:g id="NUMBER">%s</xliff:g>を使って"\n"連絡先を新規登録"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"オン"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"オフ"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"リストされているアプリケーションが、アカウント<xliff:g id="ACCOUNT">%1$s</xliff:g>のログイン認証情報に<xliff:g id="APPLICATION">%2$s</xliff:g>からアクセスする権限をリクエストしています。この権限を許可しますか?許可すると、入力が記録され、次回以降はこのメッセージが表示されなくなります。"</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"リストされているアプリケーションが、アカウント<xliff:g id="ACCOUNT">%2$s</xliff:g>の<xliff:g id="TYPE">%1$s</xliff:g>ログイン認証情報に<xliff:g id="APPLICATION">%3$s</xliff:g>からアクセスする権限をリクエストしています。この権限を許可しますか?許可すると、入力は記録され、次回以降はこのメッセージが表示されなくなります。"</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"許可"</string> <string name="deny" msgid="2081879885755434506">"拒否"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"リクエスト済み権限"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"レイヤー2トンネリングプロトコル"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"L2TP/IPSec VPNベースの事前共有鍵"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"L2TP/IPSec VPNベースの証明書"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml index 840fa99..fc156b1 100644 --- a/core/res/res/values-ko/strings.xml +++ b/core/res/res/values-ko/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"전화번호부에"\n"<xliff:g id="NUMBER">%s</xliff:g> 추가"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"선택함"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"선택 안함"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"나열된 응용프로그램이 <xliff:g id="APPLICATION">%2$s</xliff:g>에서 <xliff:g id="ACCOUNT">%1$s</xliff:g> 계정의 로그인 자격증명에 액세스할 수 있는 권한을 요청 중입니다. 권한을 부여하시겠습니까? 권한을 부여하면 응답 내용이 저장되며 메시지가 다시 표시되지 않습니다."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"나열된 응용프로그램이 <xliff:g id="APPLICATION">%3$s</xliff:g>에서 <xliff:g id="ACCOUNT">%2$s</xliff:g> 계정의 <xliff:g id="TYPE">%1$s</xliff:g> 로그인 자격증명에 액세스할 수 있는 권한을 요청 중입니다. 권한을 부여하시겠습니까? 권한을 부여하면 응답 내용이 저장되며 메시지가 다시 표시되지 않습니다."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"허용"</string> <string name="deny" msgid="2081879885755434506">"거부"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"권한 요청"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"L2TP(Layer 2 Tunneling Protocol)"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"사전 공유 키 기반 L2TP/IPSec VPN"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"인증서 기반 L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml index ced4f16..c0aae98 100644 --- a/core/res/res/values-nb/strings.xml +++ b/core/res/res/values-nb/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Lag kontakt"\n"med nummeret <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"valgt"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"ikke valgt"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Det nevnte programmet ber om tilgangstillatelse til påloggingsopplysningene for konto <xliff:g id="ACCOUNT">%1$s</xliff:g> fra <xliff:g id="APPLICATION">%2$s</xliff:g>. Vil du gi denne tillatelsen? I så fall vil svaret ditt bli lagret, og du vil ikke bli spurt flere ganger."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Det nevnte programmet ber om tilgangstillatelse til <xliff:g id="TYPE">%1$s</xliff:g>-påloggingsopplysningene for konto <xliff:g id="ACCOUNT">%2$s</xliff:g> fra <xliff:g id="APPLICATION">%3$s</xliff:g>. Vil du gi denne tillatelsen? I så fall vil svaret ditt bli lagret, og du vil ikke bli spurt flere ganger."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Tillat"</string> <string name="deny" msgid="2081879885755434506">"Avslå"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Tillatelse forespurt"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Lag 2-tunneleringsprotokoll"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"Passordbasert L2TP/IPSec-VPN"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Sertifikatbasert L2TP/IPSec-VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml index c5f5a4b..9b95c09 100644 --- a/core/res/res/values-nl/strings.xml +++ b/core/res/res/values-nl/strings.xml @@ -782,8 +782,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Contact maken"\n"met <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"aangevinkt"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"niet aangevinkt"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"De weergegeven toepassingen vragen toestemming voor toegang tot de aanmeldingsgegevens voor account \'<xliff:g id="ACCOUNT">%1$s</xliff:g>\' van <xliff:g id="APPLICATION">%2$s</xliff:g>. Wilt u deze toestemming verlenen? Als u dit wilt doen, wordt uw antwoord onthouden en wordt dit niet opnieuw gevraagd."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"De weergegeven toepassingen vragen toestemming voor toegang tot de aanmeldingsgegevens voor <xliff:g id="TYPE">%1$s</xliff:g> van het account \'<xliff:g id="ACCOUNT">%2$s</xliff:g>\' van <xliff:g id="APPLICATION">%3$s</xliff:g>. Wilt u deze toestemming verlenen? Als u dit wilt doen, wordt uw antwoord onthouden en wordt dit niet opnieuw gevraagd."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Toestaan"</string> <string name="deny" msgid="2081879885755434506">"Weigeren"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Toestemming gevraagd"</string> @@ -801,4 +805,6 @@ <skip /> <!-- no translation found for l2tp_ipsec_crt_vpn_description (5382714073103653577) --> <skip /> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml index 388a9e7..6ee52ed 100644 --- a/core/res/res/values-pl/strings.xml +++ b/core/res/res/values-pl/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Utwórz kontakt"\n"dla numeru <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"zaznaczone"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"niezaznaczone"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Wymienione aplikacje żądają pozwolenia na dostęp do danych logowania dla konta <xliff:g id="ACCOUNT">%1$s</xliff:g> powiązanego z aplikacją <xliff:g id="APPLICATION">%2$s</xliff:g>. Czy chcesz udzielić takiego pozwolenia? Jeśli tak, odpowiedź zostanie zapamiętana i to pytanie nie będzie już wyświetlane."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Wymienione aplikacje żądają pozwolenia na dostęp do danych logowania dotyczących funkcji <xliff:g id="TYPE">%1$s</xliff:g> dla konta <xliff:g id="ACCOUNT">%2$s</xliff:g> powiązanego z aplikacją <xliff:g id="APPLICATION">%3$s</xliff:g>. Czy chcesz udzielić takiego pozwolenia? Jeśli tak, odpowiedź zostanie zapamiętana i to pytanie nie będzie już wyświetlane."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Zezwól"</string> <string name="deny" msgid="2081879885755434506">"Odmów"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Żądane pozwolenie"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protokół L2TP"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"Sieć VPN L2TP/IPSec z kluczem PSK"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Sieć VPN L2TP/IPSec z certyfikatem"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml index b2edaae..c86fbdb 100644 --- a/core/res/res/values-pt-rPT/strings.xml +++ b/core/res/res/values-pt-rPT/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Criar contacto"\n"utilizando <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"verificado"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"não verificado"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"As aplicações listadas estão a pedir autorização para aceder às credenciais de início de sessão da conta <xliff:g id="ACCOUNT">%1$s</xliff:g> a partir de <xliff:g id="APPLICATION">%2$s</xliff:g>. Pretende conceder esta autorização? Em caso afirmativo, a sua resposta será memorizada e não terá de responder a esta questão novamente."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"As aplicações listadas estão a pedir autorização para aceder às credenciais de início de sessão <xliff:g id="TYPE">%1$s</xliff:g> para a conta <xliff:g id="ACCOUNT">%2$s</xliff:g> a partir de <xliff:g id="APPLICATION">%3$s</xliff:g>. Pretende conceder esta autorização? Em caso afirmativo, a sua resposta será memorizada e não terá de responder a esta questão novamente."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Permitir"</string> <string name="deny" msgid="2081879885755434506">"Recusar"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Autorização Solicitada"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protocolo de túnel de camada 2 (L2TP)"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"VPN L2TP/IPSec baseada em chave pré- partilhada"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"VPN L2TP/IPSec baseada em certificado"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml index 725053c..70f82ee 100644 --- a/core/res/res/values-pt/strings.xml +++ b/core/res/res/values-pt/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Criar contato "\n"usando <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"selecionado"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"não selecionado"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Os aplicativos listados estão solicitando autorização para acessar as credenciais de login para a conta <xliff:g id="ACCOUNT">%1$s</xliff:g> do <xliff:g id="APPLICATION">%2$s</xliff:g>. Deseja conceder essa autorização? Em caso afirmativo, sua resposta será lembrada e essa pergunta não será feita novamente."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Os aplicativos listados estão solicitando autorização para acessar as credenciais de login <xliff:g id="TYPE">%1$s</xliff:g> para a conta <xliff:g id="ACCOUNT">%2$s</xliff:g> do <xliff:g id="APPLICATION">%3$s</xliff:g>. Deseja conceder essa autorização? Em caso afirmativo, sua resposta será lembrada e essa pergunta não será feita novamente."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Permitir"</string> <string name="deny" msgid="2081879885755434506">"Negar"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Autorização solicitada"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Protocolo de encapsulamento de camada 2"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"VPN L2TP/IPSec com base em chave pré-compartilhada"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"VPN L2TP/IPSec com base em certificado"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml index 1e1500b..56b75fc 100644 --- a/core/res/res/values-ru/strings.xml +++ b/core/res/res/values-ru/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Создать контакт"\n"с номером <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"отмечено"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"не проверено"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Перечисленные приложения запрашивают разрешение на доступ к регистрационном данным аккаунта <xliff:g id="ACCOUNT">%1$s</xliff:g> из <xliff:g id="APPLICATION">%2$s</xliff:g>. Разрешить доступ? Если да, ответ будет сохранен и это сообщение больше не будет выводиться."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Перечисленные приложения запрашивают разрешение на доступ к регистрационном данным <xliff:g id="TYPE">%1$s</xliff:g> аккаунта <xliff:g id="ACCOUNT">%2$s</xliff:g> из <xliff:g id="APPLICATION">%3$s</xliff:g>. Разрешить доступ? Если да, ответ будет сохранен и это сообщение больше не будет выводиться."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Разрешить"</string> <string name="deny" msgid="2081879885755434506">"Отклонить"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Разрешение запрошено"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Протокол L2TP"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"L2TP/IPSec VPN (на основе предв. общ. ключа)"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"L2TP/IPSec VPN (на основе сертификата)"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml index 823ba22..3e32b15 100644 --- a/core/res/res/values-sv/strings.xml +++ b/core/res/res/values-sv/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"Skapa kontakt"\n"med <xliff:g id="NUMBER">%s</xliff:g>"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"markerad"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"inte markerad"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Programmen begär åtkomst till inloggningsuppgifterna för kontot <xliff:g id="ACCOUNT">%1$s</xliff:g> från <xliff:g id="APPLICATION">%2$s</xliff:g>. Vill du bevilja behörighet? Om du gör det kommer vi ihåg det och du blir inte tillfrågad igen."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Programmen begär åtkomst till inloggningsuppgifterna <xliff:g id="TYPE">%1$s</xliff:g> för kontot <xliff:g id="ACCOUNT">%2$s</xliff:g> från <xliff:g id="APPLICATION">%3$s</xliff:g>. Vill du bevilja behörighet? Om du gör det kommer vi ihåg det och du blir inte tillfrågad igen."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"Tillåt"</string> <string name="deny" msgid="2081879885755434506">"Neka"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"Begärd behörighet"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Layer 2 Tunneling Protocol"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"I förväg delad L2TP/IPSec VPN-nyckel"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"Certifikatsbaserad L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml index f95fc0a..cae0c03 100644 --- a/core/res/res/values-tr/strings.xml +++ b/core/res/res/values-tr/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"<xliff:g id="NUMBER">%s</xliff:g>"\n" ile kişi oluştur"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"seçildi"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"seçilmedi"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"Listelenen uygulamalar, <xliff:g id="APPLICATION">%2$s</xliff:g> uygulamasının <xliff:g id="ACCOUNT">%1$s</xliff:g> hesabı için giriş bilgilerine erişim izni istiyor. Bu izni vermek istiyor musunuz? İstiyorsanız yanıtınız kaydedilecek ve tekrar sorulmayacaktır."</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"Listelenen uygulamalar, <xliff:g id="APPLICATION">%3$s</xliff:g> uygulamasının <xliff:g id="ACCOUNT">%2$s</xliff:g> hesabı için <xliff:g id="TYPE">%1$s</xliff:g> girişi bilgilerine erişim izni istiyor. Bu izni vermek istiyor musunuz? İstiyorsanız yanıtınız kaydedilecek ve tekrar sorulmayacaktır."</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"İzin Ver"</string> <string name="deny" msgid="2081879885755434506">"Reddet"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"İzin İstendi"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"Katman 2 Tünel Protokolü"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"L2TP/IPSec VPN temelli önceden paylaşılmış anahtar"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"L2TP/IPSec VPN temelli sertifika"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml index ab5554b..6620a60 100644 --- a/core/res/res/values-zh-rCN/strings.xml +++ b/core/res/res/values-zh-rCN/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"创建电话号码为"\n"<xliff:g id="NUMBER">%s</xliff:g> 的联系人"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"已选中"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"未选中"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"所列应用程序正在请求相应的权限,以便从<xliff:g id="APPLICATION">%2$s</xliff:g>访问 <xliff:g id="ACCOUNT">%1$s</xliff:g> 帐户的登录凭据。是否要授予这种权限?如果授予,系统会记住您所做的选择,且不再提示。"</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"所列应用程序正在请求相应的权限,以便从<xliff:g id="APPLICATION">%3$s</xliff:g>访问 <xliff:g id="ACCOUNT">%2$s</xliff:g> 帐户的<xliff:g id="TYPE">%1$s</xliff:g>登录凭据。是否要授予这种权限?如果授予,系统会记住您所做的选择,且不再提示。"</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"允许"</string> <string name="deny" msgid="2081879885755434506">"拒绝"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"已请求权限"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"第 2 层隧道协议"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"基于预共享密钥的 L2TP/IPSec VPN"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"基于证书的 L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml index da02fe7..b9f344e 100644 --- a/core/res/res/values-zh-rTW/strings.xml +++ b/core/res/res/values-zh-rTW/strings.xml @@ -777,8 +777,12 @@ <string name="create_contact_using" msgid="4947405226788104538">"建立手機號碼為 <xliff:g id="NUMBER">%s</xliff:g>"\n"的聯絡人"</string> <string name="accessibility_compound_button_selected" msgid="5612776946036285686">"已勾選"</string> <string name="accessibility_compound_button_unselected" msgid="8864512895673924091">"未勾選"</string> - <string name="grant_credentials_permission_message_desc" msgid="6883276587034335667">"這些應用程式要求存取「<xliff:g id="APPLICATION">%2$s</xliff:g>」的 <xliff:g id="ACCOUNT">%1$s</xliff:g> 帳戶登入認證的權限,您要授予此權限嗎?如果確定授予,系統會記住您的選擇,不會再次詢問您。"</string> - <string name="grant_credentials_permission_message_with_authtokenlabel_desc" msgid="3159007601893584687">"這些應用程式要求存取「<xliff:g id="APPLICATION">%3$s</xliff:g>」的 <xliff:g id="ACCOUNT">%2$s</xliff:g> 帳戶「<xliff:g id="TYPE">%1$s</xliff:g>」登入認證的權限,您要授予此權限嗎?如果確定授予,系統會記住您的選擇,不會再次詢問您。"</string> + <!-- no translation found for grant_credentials_permission_message_header (6824538733852821001) --> + <skip /> + <!-- no translation found for grant_credentials_permission_message_footer (3125211343379376561) --> + <skip /> + <!-- no translation found for grant_permissions_header_text (2722567482180797717) --> + <skip /> <string name="allow" msgid="7225948811296386551">"允許"</string> <string name="deny" msgid="2081879885755434506">"拒絕"</string> <string name="permission_request_notification_title" msgid="5390555465778213840">"已要求權限"</string> @@ -792,4 +796,6 @@ <string name="l2tp_vpn_description" msgid="3750692169378923304">"第二層通道通訊協定"</string> <string name="l2tp_ipsec_psk_vpn_description" msgid="3945043564008303239">"採用預先共用金鑰的 L2TP/IPSec VPN"</string> <string name="l2tp_ipsec_crt_vpn_description" msgid="5382714073103653577">"採用憑證的 L2TP/IPSec VPN"</string> + <!-- no translation found for upload_file (2897957172366730416) --> + <skip /> </resources> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 81da739..df13403 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2138,6 +2138,8 @@ </declare-styleable> <declare-styleable name="ViewFlipper"> <attr name="flipInterval" format="integer" min="0" /> + <!-- When true, automatically start animating --> + <attr name="autoStart" format="boolean" /> </declare-styleable> <declare-styleable name="ViewSwitcher"> </declare-styleable> diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 5d3069b..a506c9a 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -78,6 +78,7 @@ <!-- For security permissions --> <color name="perms_dangerous_grp_color">#dd6826</color> <color name="perms_dangerous_perm_color">#dd6826</color> + <color name="shadow">#cc222222</color> <!-- For search-related UIs --> <color name="search_url_text_normal">#7fa87f</color> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 5eb1c8e..4ede620 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1201,8 +1201,8 @@ <public type="attr" name="quickContactBadgeStyleSmallWindowSmall" /> <public type="attr" name="quickContactBadgeStyleSmallWindowMedium" /> <public type="attr" name="quickContactBadgeStyleSmallWindowLarge" /> - <public type="attr" name="wallpaperAuthor" /> <public type="attr" name="wallpaperDescription" /> + <public type="attr" name="autoStart" /> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index de30fe7..83a6dc5 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2067,16 +2067,9 @@ <!-- Title for the unselected state of a CompoundButton. --> <string name="accessibility_compound_button_unselected">not checked</string> - <string name="grant_credentials_permission_message_desc">The - listed applications are requesting permission to access the login credentials for account <xliff:g id="account" example="foo@gmail.com">%1$s</xliff:g> from - <xliff:g id="application" example="Google Apps">%2$s</xliff:g>. Do you wish to grant this permission? If so, your answer will be remembered and you will not be prompted - again.</string> - - <string name="grant_credentials_permission_message_with_authtokenlabel_desc">The - listed applications are requesting permission to access the <xliff:g id="type" example="Contacts">%1$s</xliff:g> login credentials for account <xliff:g id="account" example="foo@gmail.com">%2$s</xliff:g> from - <xliff:g id="application" example="Google Apps">%3$s</xliff:g>. Do you wish to grant this permission? If so, your answer will be remembered and you will not be prompted - again.</string> - + <string name="grant_credentials_permission_message_header">The following one or more applications request permission to access your account, now and in the future.</string> + <string name="grant_credentials_permission_message_footer">Do you want to allow this request?</string> + <string name="grant_permissions_header_text">Access Request</string> <string name="allow">Allow</string> <string name="deny">Deny</string> <string name="permission_request_notification_title">Permission Requested</string> @@ -2100,4 +2093,6 @@ <string name="l2tp_vpn_description">Layer 2 Tunneling Protocol</string> <string name="l2tp_ipsec_psk_vpn_description">Pre-shared key based L2TP/IPSec VPN</string> <string name="l2tp_ipsec_crt_vpn_description">Certificate based L2TP/IPSec VPN</string> + <!-- Label for button in a WebView that will open a chooser to choose a file to upload --> + <string name="upload_file">Choose file</string> </resources> diff --git a/graphics/java/android/graphics/utils/BoundaryPatch.java b/graphics/java/android/graphics/utils/BoundaryPatch.java new file mode 100644 index 0000000..1cd5e13 --- /dev/null +++ b/graphics/java/android/graphics/utils/BoundaryPatch.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Shader; +import android.graphics.Xfermode; + +/** + * @hide + */ +public class BoundaryPatch { + private Paint mPaint; + private Bitmap mTexture; + private int mRows; + private int mCols; + private float[] mCubicPoints; + private boolean mDirty; + // these are the computed output of the native code + private float[] mVerts; + private short[] mIndices; + + public BoundaryPatch() { + mRows = mCols = 2; // default minimum + mCubicPoints = new float[24]; + mPaint = new Paint(); + mPaint.setDither(true); + mPaint.setFilterBitmap(true); + mDirty = true; + } + + /** + * Set the boundary to be 4 cubics. This takes a single array of floats, + * and picks up the 12 pairs starting at offset, and treats them as + * the x,y coordinates of the cubic control points. The points wrap around + * a patch, as follows. For documentation purposes, pts[i] will mean the + * x,y pair of floats, as if pts[] were an array of "points". + * + * Top: pts[0..3] + * Right: pts[3..6] + * Bottom: pts[6..9] + * Right: pts[9..11], pts[0] + * + * The coordinates are copied from the input array, so subsequent changes + * to pts[] will not be reflected in the boundary. + * + * @param pts The src array of x,y pairs for the boundary cubics + * @param offset The index into pts of the first pair + * @param rows The number of points across to approximate the boundary. + * Must be >= 2, though very large values may slow down drawing + * @param cols The number of points down to approximate the boundary. + * Must be >= 2, though very large values may slow down drawing + */ + public void setCubicBoundary(float[] pts, int offset, int rows, int cols) { + if (rows < 2 || cols < 2) { + throw new RuntimeException("rows and cols must be >= 2"); + } + System.arraycopy(pts, offset, mCubicPoints, 0, 24); + if (mRows != rows || mCols != cols) { + mRows = rows; + mCols = cols; + } + mDirty = true; + } + + /** + * Reference a bitmap texture to be mapped onto the patch. + */ + public void setTexture(Bitmap texture) { + if (mTexture != texture) { + if (mTexture == null || + mTexture.getWidth() != texture.getWidth() || + mTexture.getHeight() != texture.getHeight()) { + // need to recompute texture coordinates + mDirty = true; + } + mTexture = texture; + mPaint.setShader(new BitmapShader(texture, + Shader.TileMode.CLAMP, + Shader.TileMode.CLAMP)); + } + } + + /** + * Return the paint flags for the patch + */ + public int getPaintFlags() { + return mPaint.getFlags(); + } + + /** + * Set the paint flags for the patch + */ + public void setPaintFlags(int flags) { + mPaint.setFlags(flags); + } + + /** + * Set the xfermode for the patch + */ + public void setXfermode(Xfermode mode) { + mPaint.setXfermode(mode); + } + + /** + * Set the alpha for the patch + */ + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + /** + * Draw the patch onto the canvas. + * + * setCubicBoundary() and setTexture() must be called before drawing. + */ + public void draw(Canvas canvas) { + if (mDirty) { + buildCache(); + mDirty = false; + } + + // cut the count in half, since mVerts.length is really the length of + // the verts[] and tex[] arrays combined + // (tex[] are stored after verts[]) + int vertCount = mVerts.length >> 1; + canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertCount, + mVerts, 0, mVerts, vertCount, null, 0, + mIndices, 0, mIndices.length, + mPaint); + } + + private void buildCache() { + // we need mRows * mCols points, for verts and another set for textures + // so *2 for going from points -> floats, and *2 for verts and textures + int vertCount = mRows * mCols * 4; + if (mVerts == null || mVerts.length != vertCount) { + mVerts = new float[vertCount]; + } + + int indexCount = (mRows - 1) * (mCols - 1) * 6; + if (mIndices == null || mIndices.length != indexCount) { + mIndices = new short[indexCount]; + } + + nativeComputeCubicPatch(mCubicPoints, + mTexture.getWidth(), mTexture.getHeight(), + mRows, mCols, mVerts, mIndices); + } + + private static native + void nativeComputeCubicPatch(float[] cubicPoints, + int texW, int texH, int rows, int cols, + float[] verts, short[] indices); +} + diff --git a/include/media/stagefright/CachingDataSource.h b/include/media/stagefright/CachingDataSource.h index e35e19e..b0fc4b2 100644 --- a/include/media/stagefright/CachingDataSource.h +++ b/include/media/stagefright/CachingDataSource.h @@ -29,9 +29,9 @@ public: CachingDataSource( const sp<DataSource> &source, size_t pageSize, int numPages); - status_t InitCheck() const; + virtual status_t initCheck() const; - virtual ssize_t read_at(off_t offset, void *data, size_t size); + virtual ssize_t readAt(off_t offset, void *data, size_t size); protected: virtual ~CachingDataSource(); diff --git a/include/media/stagefright/CameraSource.h b/include/media/stagefright/CameraSource.h index 7042e1a..ff3ea05 100644 --- a/include/media/stagefright/CameraSource.h +++ b/include/media/stagefright/CameraSource.h @@ -26,12 +26,11 @@ namespace android { -class ICamera; -class ICameraClient; class IMemory; +class ISurface; +class Camera; -class CameraSource : public MediaSource, - public MediaBufferObserver { +class CameraSource : public MediaSource { public: static CameraSource *Create(); @@ -45,24 +44,25 @@ public: virtual status_t read( MediaBuffer **buffer, const ReadOptions *options = NULL); - virtual void notifyCallback(int32_t msgType, int32_t ext1, int32_t ext2); - virtual void dataCallback(int32_t msgType, const sp<IMemory>& data); - - virtual void signalBufferReturned(MediaBuffer *buffer); - private: - CameraSource(const sp<ICamera> &camera, const sp<ICameraClient> &client); + friend class CameraSourceListener; - sp<ICamera> mCamera; - sp<ICameraClient> mCameraClient; + sp<Camera> mCamera; Mutex mLock; Condition mFrameAvailableCondition; List<sp<IMemory> > mFrames; + List<int64_t> mFrameTimes; - int mNumFrames; + int mWidth, mHeight; + int64_t mFirstFrameTimeUs; + int32_t mNumFrames; bool mStarted; + CameraSource(const sp<Camera> &camera); + + void dataCallback(int32_t msgType, const sp<IMemory> &data); + CameraSource(const CameraSource &); CameraSource &operator=(const CameraSource &); }; diff --git a/include/media/stagefright/ColorConverter.h b/include/media/stagefright/ColorConverter.h new file mode 100644 index 0000000..1e341b9 --- /dev/null +++ b/include/media/stagefright/ColorConverter.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COLOR_CONVERTER_H_ + +#define COLOR_CONVERTER_H_ + +#include <sys/types.h> + +#include <stdint.h> + +#include <OMX_Video.h> + +namespace android { + +struct ColorConverter { + ColorConverter(OMX_COLOR_FORMATTYPE from, OMX_COLOR_FORMATTYPE to); + ~ColorConverter(); + + bool isValid() const; + + void convert( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip); + +private: + OMX_COLOR_FORMATTYPE mSrcFormat, mDstFormat; + uint8_t *mClip; + + uint8_t *initClip(); + + void convertCbYCrY( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip); + + void convertYUV420Planar( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip); + + void convertQCOMYUV420SemiPlanar( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip); + + ColorConverter(const ColorConverter &); + ColorConverter &operator=(const ColorConverter &); +}; + +} // namespace android + +#endif // COLOR_CONVERTER_H_ diff --git a/include/media/stagefright/DataSource.h b/include/media/stagefright/DataSource.h index f46f0af..b843cd9 100644 --- a/include/media/stagefright/DataSource.h +++ b/include/media/stagefright/DataSource.h @@ -33,7 +33,9 @@ class DataSource : public RefBase { public: DataSource() {} - virtual ssize_t read_at(off_t offset, void *data, size_t size) = 0; + virtual status_t initCheck() const = 0; + + virtual ssize_t readAt(off_t offset, void *data, size_t size) = 0; // Convenience methods: bool getUInt16(off_t offset, uint16_t *x); diff --git a/include/media/stagefright/FileSource.h b/include/media/stagefright/FileSource.h index ccbe0ef..d7b42c3 100644 --- a/include/media/stagefright/FileSource.h +++ b/include/media/stagefright/FileSource.h @@ -29,11 +29,13 @@ namespace android { class FileSource : public DataSource { public: FileSource(const char *filename); - virtual ~FileSource(); - status_t InitCheck() const; + virtual status_t initCheck() const; + + virtual ssize_t readAt(off_t offset, void *data, size_t size); - virtual ssize_t read_at(off_t offset, void *data, size_t size); +protected: + virtual ~FileSource(); private: FILE *mFile; diff --git a/include/media/stagefright/HTTPDataSource.h b/include/media/stagefright/HTTPDataSource.h index 0587c7c..d5dc9e6 100644 --- a/include/media/stagefright/HTTPDataSource.h +++ b/include/media/stagefright/HTTPDataSource.h @@ -19,28 +19,29 @@ #define HTTP_DATASOURCE_H_ #include <media/stagefright/DataSource.h> -#include <media/stagefright/HTTPStream.h> namespace android { +class HTTPStream; + class HTTPDataSource : public DataSource { public: HTTPDataSource(const char *host, int port, const char *path); HTTPDataSource(const char *uri); - virtual ~HTTPDataSource(); + virtual status_t initCheck() const; - // XXXandih - status_t InitCheck() const { return OK; } + virtual ssize_t readAt(off_t offset, void *data, size_t size); - virtual ssize_t read_at(off_t offset, void *data, size_t size); +protected: + virtual ~HTTPDataSource(); private: enum { kBufferSize = 64 * 1024 }; - HTTPStream mHttp; + HTTPStream *mHttp; char *mHost; int mPort; char *mPath; @@ -49,6 +50,8 @@ private: size_t mBufferLength; off_t mBufferOffset; + status_t mInitCheck; + HTTPDataSource(const HTTPDataSource &); HTTPDataSource &operator=(const HTTPDataSource &); }; diff --git a/include/media/stagefright/MediaDefs.h b/include/media/stagefright/MediaDefs.h index feb66e3..1efeb92 100644 --- a/include/media/stagefright/MediaDefs.h +++ b/include/media/stagefright/MediaDefs.h @@ -34,6 +34,7 @@ extern const char *MEDIA_MIMETYPE_AUDIO_AAC; extern const char *MEDIA_MIMETYPE_AUDIO_RAW; extern const char *MEDIA_MIMETYPE_CONTAINER_MPEG4; +extern const char *MEDIA_MIMETYPE_CONTAINER_WAV; } // namespace android diff --git a/include/media/stagefright/MediaErrors.h b/include/media/stagefright/MediaErrors.h index 2bb0ed6..73d0f77 100644 --- a/include/media/stagefright/MediaErrors.h +++ b/include/media/stagefright/MediaErrors.h @@ -36,6 +36,9 @@ enum { ERROR_BUFFER_TOO_SMALL = MEDIA_ERROR_BASE - 9, ERROR_UNSUPPORTED = MEDIA_ERROR_BASE - 10, ERROR_END_OF_STREAM = MEDIA_ERROR_BASE - 11, + + // Not technically an error. + INFO_FORMAT_CHANGED = MEDIA_ERROR_BASE - 12, }; } // namespace android diff --git a/include/media/stagefright/MediaExtractor.h b/include/media/stagefright/MediaExtractor.h index 67e45bd..d56d4b3 100644 --- a/include/media/stagefright/MediaExtractor.h +++ b/include/media/stagefright/MediaExtractor.h @@ -31,9 +31,17 @@ public: static sp<MediaExtractor> Create( const sp<DataSource> &source, const char *mime = NULL); + static sp<MediaExtractor> CreateFromURI( + const char *uri, const char *mime = NULL); + virtual size_t countTracks() = 0; virtual sp<MediaSource> getTrack(size_t index) = 0; - virtual sp<MetaData> getTrackMetaData(size_t index) = 0; + + enum GetTrackMetaDataFlags { + kIncludeExtensiveMetaData = 1 + }; + virtual sp<MetaData> getTrackMetaData( + size_t index, uint32_t flags = 0) = 0; protected: MediaExtractor() {} diff --git a/include/media/stagefright/MediaSource.h b/include/media/stagefright/MediaSource.h index d1fa114..96d57e7 100644 --- a/include/media/stagefright/MediaSource.h +++ b/include/media/stagefright/MediaSource.h @@ -51,6 +51,9 @@ struct MediaSource : public RefBase { // buffer is available, an error is encountered of the end of the stream // is reached. // End of stream is signalled by a result of ERROR_END_OF_STREAM. + // A result of INFO_FORMAT_CHANGED indicates that the format of this + // MediaSource has changed mid-stream, the client can continue reading + // but should be prepared for buffers of the new configuration. virtual status_t read( MediaBuffer **buffer, const ReadOptions *options = NULL) = 0; diff --git a/include/media/stagefright/MetaData.h b/include/media/stagefright/MetaData.h index abb45a9..c2d8f98 100644 --- a/include/media/stagefright/MetaData.h +++ b/include/media/stagefright/MetaData.h @@ -27,25 +27,26 @@ namespace android { +// The following keys map to int32_t data unless indicated otherwise. enum { - kKeyMIMEType = 'mime', + kKeyMIMEType = 'mime', // cstring kKeyWidth = 'widt', kKeyHeight = 'heig', kKeyChannelCount = '#chn', kKeySampleRate = 'srte', kKeyBitRate = 'brte', - kKeyESDS = 'esds', - kKeyAVCC = 'avcc', - kKeyTimeUnits = '#tim', - kKeyTimeScale = 'scal', + kKeyESDS = 'esds', // raw data + kKeyAVCC = 'avcc', // raw data kKeyWantsNALFragments = 'NALf', kKeyIsSyncFrame = 'sync', - kKeyDuration = 'dura', + kKeyTime = 'time', // int64_t (usecs) + kKeyDuration = 'dura', // int64_t (usecs) kKeyColorFormat = 'colf', - kKeyPlatformPrivate = 'priv', - kKeyDecoderComponent = 'decC', + kKeyPlatformPrivate = 'priv', // pointer + kKeyDecoderComponent = 'decC', // cstring kKeyBufferID = 'bfID', kKeyMaxInputSize = 'inpS', + kKeyThumbnailTime = 'thbT', // int64_t (usecs) }; enum { @@ -62,6 +63,7 @@ public: TYPE_NONE = 'none', TYPE_C_STRING = 'cstr', TYPE_INT32 = 'in32', + TYPE_INT64 = 'in64', TYPE_FLOAT = 'floa', TYPE_POINTER = 'ptr ', }; @@ -71,11 +73,13 @@ public: bool setCString(uint32_t key, const char *value); bool setInt32(uint32_t key, int32_t value); + bool setInt64(uint32_t key, int64_t value); bool setFloat(uint32_t key, float value); bool setPointer(uint32_t key, void *value); bool findCString(uint32_t key, const char **value); bool findInt32(uint32_t key, int32_t *value); + bool findInt64(uint32_t key, int64_t *value); bool findFloat(uint32_t key, float *value); bool findPointer(uint32_t key, void **value); diff --git a/include/media/stagefright/MmapSource.h b/include/media/stagefright/MmapSource.h index a8bd57f..1b39d53 100644 --- a/include/media/stagefright/MmapSource.h +++ b/include/media/stagefright/MmapSource.h @@ -30,13 +30,14 @@ public: // Assumes ownership of "fd". MmapSource(int fd, int64_t offset, int64_t length); - virtual ~MmapSource(); - - status_t InitCheck() const; + virtual status_t initCheck() const; - virtual ssize_t read_at(off_t offset, void *data, size_t size); + virtual ssize_t readAt(off_t offset, void *data, size_t size); virtual status_t getSize(off_t *size); +protected: + virtual ~MmapSource(); + private: int mFd; void *mBase; diff --git a/include/media/stagefright/OMXCodec.h b/include/media/stagefright/OMXCodec.h index 3f3dcf9..7890883 100644 --- a/include/media/stagefright/OMXCodec.h +++ b/include/media/stagefright/OMXCodec.h @@ -30,11 +30,15 @@ struct OMXCodecObserver; struct OMXCodec : public MediaSource, public MediaBufferObserver { + enum CreationFlags { + kPreferSoftwareCodecs = 1, + }; static sp<OMXCodec> Create( const sp<IOMX> &omx, const sp<MetaData> &meta, bool createEncoder, const sp<MediaSource> &source, - const char *matchComponentName = NULL); + const char *matchComponentName = NULL, + uint32_t flags = 0); static void setComponentRole( const sp<IOMX> &omx, IOMX::node_id node, bool isEncoder, @@ -90,7 +94,6 @@ private: kRequiresFlushCompleteEmulation = 16, kRequiresAllocateBufferOnOutputPorts = 32, kRequiresFlushBeforeShutdown = 64, - kOutputDimensionsAre16Aligned = 128, }; struct BufferInfo { @@ -107,7 +110,6 @@ private: sp<IOMX> mOMX; IOMX::node_id mNode; - sp<OMXCodecObserver> mObserver; uint32_t mQuirks; bool mIsEncoder; char *mMIME; @@ -125,6 +127,7 @@ private: bool mInitialBufferSubmit; bool mSignalledEOS; bool mNoMoreOutputData; + bool mOutputPortSettingsHaveChanged; int64_t mSeekTimeUs; Mutex mLock; @@ -155,6 +158,8 @@ private: void setVideoInputFormat( const char *mime, OMX_U32 width, OMX_U32 height); + status_t setupMPEG4EncoderParameters(); + void setVideoOutputFormat( const char *mime, OMX_U32 width, OMX_U32 height); @@ -208,6 +213,14 @@ private: void dumpPortStatus(OMX_U32 portIndex); + static uint32_t getComponentQuirks(const char *componentName); + + static void findMatchingCodecs( + const char *mime, + bool createEncoder, const char *matchComponentName, + uint32_t flags, + Vector<String8> *matchingCodecs); + OMXCodec(const OMXCodec &); OMXCodec &operator=(const OMXCodec &); }; diff --git a/include/media/stagefright/ShoutcastSource.h b/include/media/stagefright/ShoutcastSource.h index 352857a..bc67156 100644 --- a/include/media/stagefright/ShoutcastSource.h +++ b/include/media/stagefright/ShoutcastSource.h @@ -31,7 +31,6 @@ class ShoutcastSource : public MediaSource { public: // Assumes ownership of "http". ShoutcastSource(HTTPStream *http); - virtual ~ShoutcastSource(); virtual status_t start(MetaData *params = NULL); virtual status_t stop(); @@ -41,6 +40,9 @@ public: virtual status_t read( MediaBuffer **buffer, const ReadOptions *options = NULL); +protected: + virtual ~ShoutcastSource(); + private: HTTPStream *mHttp; size_t mMetaDataOffset; diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java index e80d8aa..1713324 100644 --- a/media/java/android/media/Ringtone.java +++ b/media/java/android/media/Ringtone.java @@ -137,11 +137,17 @@ public class Ringtone { cursor = res.query(uri, MEDIA_COLUMNS, null, null, null); } - if (cursor != null && cursor.getCount() == 1) { - cursor.moveToFirst(); - return cursor.getString(2); - } else { - title = uri.getLastPathSegment(); + try { + if (cursor != null && cursor.getCount() == 1) { + cursor.moveToFirst(); + return cursor.getString(2); + } else { + title = uri.getLastPathSegment(); + } + } finally { + if (cursor != null) { + cursor.close(); + } } } } diff --git a/media/java/android/media/ThumbnailUtil.java b/media/java/android/media/ThumbnailUtil.java index f9d69fb..0cf4e76 100644 --- a/media/java/android/media/ThumbnailUtil.java +++ b/media/java/android/media/ThumbnailUtil.java @@ -33,10 +33,9 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.media.MediaMetadataRetriever; +import android.media.MediaFile.MediaFileType; -import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; @@ -295,7 +294,7 @@ public class ThumbnailUtil { * @param uri URI of original image * @param origId image id * @param kind either MINI_KIND or MICRO_KIND - * @param saveImage Whether to save MINI_KIND thumbnail obtained in this method. + * @param saveMini Whether to save MINI_KIND thumbnail obtained in this method. * @return Bitmap */ public static Bitmap createImageThumbnail(ContentResolver cr, String filePath, Uri uri, @@ -305,16 +304,12 @@ public class ThumbnailUtil { ThumbnailUtil.THUMBNAIL_TARGET_SIZE : ThumbnailUtil.MINI_THUMB_TARGET_SIZE; int maxPixels = wantMini ? ThumbnailUtil.THUMBNAIL_MAX_NUM_PIXELS : ThumbnailUtil.MINI_THUMB_MAX_NUM_PIXELS; - byte[] thumbData = createThumbnailFromEXIF(filePath, targetSize); + SizedThumbnailBitmap sizedThumbnailBitmap = new SizedThumbnailBitmap(); Bitmap bitmap = null; - - if (thumbData != null) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = computeSampleSize(options, targetSize, maxPixels); - options.inDither = false; - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - options.inJustDecodeBounds = false; - bitmap = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); + MediaFileType fileType = MediaFile.getFileType(filePath); + if (fileType != null && fileType.fileType == MediaFile.FILE_TYPE_JPEG) { + createThumbnailFromEXIF(filePath, targetSize, maxPixels, sizedThumbnailBitmap); + bitmap = sizedThumbnailBitmap.mBitmap; } if (bitmap == null) { @@ -326,9 +321,11 @@ public class ThumbnailUtil { } if (saveMini) { - if (thumbData != null) { - ThumbnailUtil.storeThumbnail(cr, origId, thumbData, bitmap.getWidth(), - bitmap.getHeight()); + if (sizedThumbnailBitmap.mThumbnailData != null) { + ThumbnailUtil.storeThumbnail(cr, origId, + sizedThumbnailBitmap.mThumbnailData, + sizedThumbnailBitmap.mThumbnailWidth, + sizedThumbnailBitmap.mThumbnailHeight); } else { ThumbnailUtil.storeThumbnail(cr, origId, bitmap); } @@ -454,6 +451,7 @@ public class ThumbnailUtil { Cursor c = cr.query(thumbUri, THUMB_PROJECTION, Thumbnails.IMAGE_ID + "=?", new String[]{String.valueOf(origId)}, null); + if (c == null) return null; try { if (c.moveToNext()) { return ContentUris.withAppendedId(thumbUri, c.getLong(0)); @@ -482,6 +480,7 @@ public class ThumbnailUtil { if (thumb == null) return false; try { Uri uri = getImageThumbnailUri(cr, origId, thumb.getWidth(), thumb.getHeight()); + if (uri == null) return false; OutputStream thumbOut = cr.openOutputStream(uri); thumb.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut); thumbOut.close(); @@ -514,31 +513,69 @@ public class ThumbnailUtil { } } - // Extract thumbnail in image that meets the targetSize criteria. - static byte[] createThumbnailFromEXIF(String filePath, int targetSize) { - if (filePath == null) return null; - - try { - ExifInterface exif = new ExifInterface(filePath); - if (exif == null) return null; - byte [] thumbData = exif.getThumbnail(); - if (thumbData == null) return null; - // Sniff the size of the EXIF thumbnail before decoding it. Photos - // from the device will pass, but images that are side loaded from - // other cameras may not. - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); + // SizedThumbnailBitmap contains the bitmap, which is downsampled either from + // the thumbnail in exif or the full image. + // mThumbnailData, mThumbnailWidth and mThumbnailHeight are set together only if mThumbnail is not null. + // The width/height of the sized bitmap may be different from mThumbnailWidth/mThumbnailHeight. + private static class SizedThumbnailBitmap { + public byte[] mThumbnailData; + public Bitmap mBitmap; + public int mThumbnailWidth; + public int mThumbnailHeight; + } - int width = options.outWidth; - int height = options.outHeight; + // Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image. + // The functions returns a SizedThumbnailBitmap, + // which contains a downsampled bitmap and the thumbnail data in EXIF if exists. + private static void createThumbnailFromEXIF(String filePath, int targetSize, + int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) { + if (filePath == null) return; - if (width >= targetSize && height >= targetSize) { - return thumbData; + ExifInterface exif = null; + byte [] thumbData = null; + try { + exif = new ExifInterface(filePath); + if (exif != null) { + thumbData = exif.getThumbnail(); } } catch (IOException ex) { Log.w(TAG, ex); } - return null; + + BitmapFactory.Options fullOptions = new BitmapFactory.Options(); + BitmapFactory.Options exifOptions = new BitmapFactory.Options(); + int exifThumbWidth = 0; + int fullThumbWidth = 0; + + // Compute exifThumbWidth. + if (thumbData != null) { + exifOptions.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions); + exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels); + exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize; + } + + // Compute fullThumbWidth. + fullOptions.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filePath, fullOptions); + fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels); + fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize; + + // Choose the larger thumbnail as the returning sizedThumbBitmap. + if (exifThumbWidth >= fullThumbWidth) { + int width = exifOptions.outWidth; + int height = exifOptions.outHeight; + exifOptions.inJustDecodeBounds = false; + sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0, + thumbData.length, exifOptions); + if (sizedThumbBitmap.mBitmap != null) { + sizedThumbBitmap.mThumbnailData = thumbData; + sizedThumbBitmap.mThumbnailWidth = width; + sizedThumbBitmap.mThumbnailHeight = height; + } + } else { + fullOptions.inJustDecodeBounds = false; + sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions); + } } } diff --git a/media/libmedia/IOMX.cpp b/media/libmedia/IOMX.cpp index 88a7064..76a9e7d 100644 --- a/media/libmedia/IOMX.cpp +++ b/media/libmedia/IOMX.cpp @@ -284,7 +284,7 @@ public: data.writeInterfaceToken(IOMX::getInterfaceDescriptor()); data.writeIntPtr((intptr_t)node); data.writeIntPtr((intptr_t)buffer); - remote()->transact(FILL_BUFFER, data, &reply, IBinder::FLAG_ONEWAY); + remote()->transact(FILL_BUFFER, data, &reply); return reply.readInt32(); } @@ -302,7 +302,7 @@ public: data.writeInt32(range_length); data.writeInt32(flags); data.writeInt64(timestamp); - remote()->transact(EMPTY_BUFFER, data, &reply, IBinder::FLAG_ONEWAY); + remote()->transact(EMPTY_BUFFER, data, &reply); return reply.readInt32(); } diff --git a/media/libmediaplayerservice/Android.mk b/media/libmediaplayerservice/Android.mk index fb569da..6fc9fa2 100644 --- a/media/libmediaplayerservice/Android.mk +++ b/media/libmediaplayerservice/Android.mk @@ -18,8 +18,9 @@ LOCAL_SRC_FILES:= \ ifeq ($(BUILD_WITH_FULL_STAGEFRIGHT),true) -LOCAL_SRC_FILES += \ - StagefrightPlayer.cpp +LOCAL_SRC_FILES += \ + StagefrightPlayer.cpp \ + StagefrightMetadataRetriever.cpp LOCAL_CFLAGS += -DBUILD_WITH_FULL_STAGEFRIGHT=1 diff --git a/media/libmediaplayerservice/MediaPlayerService.cpp b/media/libmediaplayerservice/MediaPlayerService.cpp index 0a6c365..b81684b 100644 --- a/media/libmediaplayerservice/MediaPlayerService.cpp +++ b/media/libmediaplayerservice/MediaPlayerService.cpp @@ -366,11 +366,44 @@ extern "C" void get_malloc_leak_info(uint8_t** info, size_t* overallSize, size_t* infoSize, size_t* totalMemory, size_t* backtraceSize); extern "C" void free_malloc_leak_info(uint8_t* info); +// Use the String-class below instead of String8 to allocate all memory +// beforehand and not reenter the heap while we are examining it... +struct MyString8 { + static const size_t MAX_SIZE = 256 * 1024; + + MyString8() + : mPtr((char *)malloc(MAX_SIZE)) { + *mPtr = '\0'; + } + + ~MyString8() { + free(mPtr); + } + + void append(const char *s) { + strcat(mPtr, s); + } + + const char *string() const { + return mPtr; + } + + size_t size() const { + return strlen(mPtr); + } + +private: + char *mPtr; + + MyString8(const MyString8 &); + MyString8 &operator=(const MyString8 &); +}; + void memStatus(int fd, const Vector<String16>& args) { const size_t SIZE = 256; char buffer[SIZE]; - String8 result; + MyString8 result; typedef struct { size_t size; diff --git a/media/libmediaplayerservice/MetadataRetrieverClient.cpp b/media/libmediaplayerservice/MetadataRetrieverClient.cpp index 2cdc351..866c7bd 100644 --- a/media/libmediaplayerservice/MetadataRetrieverClient.cpp +++ b/media/libmediaplayerservice/MetadataRetrieverClient.cpp @@ -38,6 +38,7 @@ #include "VorbisMetadataRetriever.h" #include "MidiMetadataRetriever.h" #include "MetadataRetrieverClient.h" +#include "StagefrightMetadataRetriever.h" /* desktop Linux needs a little help with gettid() */ #if defined(HAVE_GETTID) && !defined(HAVE_ANDROID_OS) @@ -118,9 +119,15 @@ static sp<MediaMetadataRetrieverBase> createRetriever(player_type playerType) LOGV("create midi metadata retriever"); p = new MidiMetadataRetriever(); break; +#if BUILD_WITH_FULL_STAGEFRIGHT + case STAGEFRIGHT_PLAYER: + LOGV("create StagefrightMetadataRetriever"); + p = new StagefrightMetadataRetriever; + break; +#endif default: // TODO: - // support for STAGEFRIGHT_PLAYER and TEST_PLAYER + // support for TEST_PLAYER LOGE("player type %d is not supported", playerType); break; } @@ -138,12 +145,6 @@ status_t MetadataRetrieverClient::setDataSource(const char *url) return UNKNOWN_ERROR; } player_type playerType = getPlayerType(url); -#if !defined(NO_OPENCORE) && defined(BUILD_WITH_FULL_STAGEFRIGHT) - if (playerType == STAGEFRIGHT_PLAYER) { - // Stagefright doesn't support metadata in this branch yet. - playerType = PV_PLAYER; - } -#endif LOGV("player type = %d", playerType); sp<MediaMetadataRetrieverBase> p = createRetriever(playerType); if (p == NULL) return NO_INIT; @@ -182,12 +183,6 @@ status_t MetadataRetrieverClient::setDataSource(int fd, int64_t offset, int64_t } player_type playerType = getPlayerType(fd, offset, length); -#if !defined(NO_OPENCORE) && defined(BUILD_WITH_FULL_STAGEFRIGHT) - if (playerType == STAGEFRIGHT_PLAYER) { - // Stagefright doesn't support metadata in this branch yet. - playerType = PV_PLAYER; - } -#endif LOGV("player type = %d", playerType); sp<MediaMetadataRetrieverBase> p = createRetriever(playerType); if (p == NULL) { diff --git a/media/libmediaplayerservice/StagefrightMetadataRetriever.cpp b/media/libmediaplayerservice/StagefrightMetadataRetriever.cpp new file mode 100644 index 0000000..7a3aee8 --- /dev/null +++ b/media/libmediaplayerservice/StagefrightMetadataRetriever.cpp @@ -0,0 +1,197 @@ +/* +** +** Copyright 2009, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "StagefrightMetadataRetriever" +#include <utils/Log.h> + +#include "StagefrightMetadataRetriever.h" + +#include <media/stagefright/CachingDataSource.h> +#include <media/stagefright/ColorConverter.h> +#include <media/stagefright/DataSource.h> +#include <media/stagefright/HTTPDataSource.h> +#include <media/stagefright/MediaDebug.h> +#include <media/stagefright/MediaExtractor.h> +#include <media/stagefright/MetaData.h> +#include <media/stagefright/MmapSource.h> +#include <media/stagefright/OMXCodec.h> + +namespace android { + +StagefrightMetadataRetriever::StagefrightMetadataRetriever() { + LOGV("StagefrightMetadataRetriever()"); + + DataSource::RegisterDefaultSniffers(); + CHECK_EQ(mClient.connect(), OK); +} + +StagefrightMetadataRetriever::~StagefrightMetadataRetriever() { + LOGV("~StagefrightMetadataRetriever()"); + mClient.disconnect(); +} + +status_t StagefrightMetadataRetriever::setDataSource(const char *uri) { + LOGV("setDataSource(%s)", uri); + + mExtractor = MediaExtractor::CreateFromURI(uri); + + return mExtractor.get() != NULL ? OK : UNKNOWN_ERROR; +} + +status_t StagefrightMetadataRetriever::setDataSource( + int fd, int64_t offset, int64_t length) { + LOGV("setDataSource(%d, %lld, %lld)", fd, offset, length); + + mExtractor = MediaExtractor::Create( + new MmapSource(fd, offset, length)); + + return OK; +} + +VideoFrame *StagefrightMetadataRetriever::captureFrame() { + LOGV("captureFrame"); + + if (mExtractor.get() == NULL) { + LOGV("no extractor."); + return NULL; + } + + size_t n = mExtractor->countTracks(); + size_t i; + for (i = 0; i < n; ++i) { + sp<MetaData> meta = mExtractor->getTrackMetaData(i); + + const char *mime; + CHECK(meta->findCString(kKeyMIMEType, &mime)); + + if (!strncasecmp(mime, "video/", 6)) { + break; + } + } + + if (i == n) { + LOGV("no video track found."); + return NULL; + } + + sp<MetaData> trackMeta = mExtractor->getTrackMetaData( + i, MediaExtractor::kIncludeExtensiveMetaData); + + sp<MediaSource> source = mExtractor->getTrack(i); + + if (source.get() == NULL) { + LOGV("unable to instantiate video track."); + return NULL; + } + + sp<MetaData> meta = source->getFormat(); + + sp<MediaSource> decoder = + OMXCodec::Create( + mClient.interface(), meta, false, source, + NULL, OMXCodec::kPreferSoftwareCodecs); + + if (decoder.get() == NULL) { + LOGV("unable to instantiate video decoder."); + + return NULL; + } + + decoder->start(); + + // Read one output buffer, ignore format change notifications + // and spurious empty buffers. + + MediaSource::ReadOptions options; + int64_t thumbNailTime; + if (trackMeta->findInt64(kKeyThumbnailTime, &thumbNailTime)) { + options.setSeekTo(thumbNailTime); + } + + MediaBuffer *buffer = NULL; + status_t err; + do { + if (buffer != NULL) { + buffer->release(); + buffer = NULL; + } + err = decoder->read(&buffer, &options); + options.clearSeekTo(); + } while (err == INFO_FORMAT_CHANGED + || (buffer != NULL && buffer->range_length() == 0)); + + if (err != OK) { + CHECK_EQ(buffer, NULL); + + LOGV("decoding frame failed."); + decoder->stop(); + + return NULL; + } + + LOGV("successfully decoded video frame."); + + meta = decoder->getFormat(); + + int32_t width, height; + CHECK(meta->findInt32(kKeyWidth, &width)); + CHECK(meta->findInt32(kKeyHeight, &height)); + + VideoFrame *frame = new VideoFrame; + frame->mWidth = width; + frame->mHeight = height; + frame->mDisplayWidth = width; + frame->mDisplayHeight = height; + frame->mSize = width * height * 2; + frame->mData = new uint8_t[frame->mSize]; + + int32_t srcFormat; + CHECK(meta->findInt32(kKeyColorFormat, &srcFormat)); + + ColorConverter converter( + (OMX_COLOR_FORMATTYPE)srcFormat, OMX_COLOR_Format16bitRGB565); + CHECK(converter.isValid()); + + converter.convert( + width, height, + (const uint8_t *)buffer->data() + buffer->range_offset(), + 0, + frame->mData, width * 2); + + buffer->release(); + buffer = NULL; + + decoder->stop(); + + return frame; +} + +MediaAlbumArt *StagefrightMetadataRetriever::extractAlbumArt() { + LOGV("extractAlbumArt (extractor: %s)", mExtractor.get() != NULL ? "YES" : "NO"); + + return NULL; +} + +const char *StagefrightMetadataRetriever::extractMetadata(int keyCode) { + LOGV("extractMetadata %d (extractor: %s)", + keyCode, mExtractor.get() != NULL ? "YES" : "NO"); + + return NULL; +} + +} // namespace android diff --git a/media/libmediaplayerservice/StagefrightMetadataRetriever.h b/media/libmediaplayerservice/StagefrightMetadataRetriever.h new file mode 100644 index 0000000..16127d7 --- /dev/null +++ b/media/libmediaplayerservice/StagefrightMetadataRetriever.h @@ -0,0 +1,53 @@ +/* +** +** Copyright 2009, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +#ifndef STAGEFRIGHT_METADATA_RETRIEVER_H_ + +#define STAGEFRIGHT_METADATA_RETRIEVER_H_ + +#include <media/MediaMetadataRetrieverInterface.h> + +#include <media/stagefright/OMXClient.h> + +namespace android { + +class MediaExtractor; + +struct StagefrightMetadataRetriever : public MediaMetadataRetrieverInterface { + StagefrightMetadataRetriever(); + virtual ~StagefrightMetadataRetriever(); + + virtual status_t setDataSource(const char *url); + virtual status_t setDataSource(int fd, int64_t offset, int64_t length); + + virtual VideoFrame *captureFrame(); + virtual MediaAlbumArt *extractAlbumArt(); + virtual const char *extractMetadata(int keyCode); + +private: + OMXClient mClient; + sp<MediaExtractor> mExtractor; + + StagefrightMetadataRetriever(const StagefrightMetadataRetriever &); + + StagefrightMetadataRetriever &operator=( + const StagefrightMetadataRetriever &); +}; + +} // namespace android + +#endif // STAGEFRIGHT_METADATA_RETRIEVER_H_ diff --git a/media/libmediaplayerservice/TestPlayerStub.cpp b/media/libmediaplayerservice/TestPlayerStub.cpp index 8627708..aa49429 100644 --- a/media/libmediaplayerservice/TestPlayerStub.cpp +++ b/media/libmediaplayerservice/TestPlayerStub.cpp @@ -176,7 +176,7 @@ status_t TestPlayerStub::resetInternal() mContentUrl = NULL; if (mPlayer) { - LOG_ASSERT(mDeletePlayer != NULL); + LOG_ASSERT(mDeletePlayer != NULL, "mDeletePlayer is null"); (*mDeletePlayer)(mPlayer); mPlayer = NULL; } diff --git a/media/libstagefright/AMRExtractor.cpp b/media/libstagefright/AMRExtractor.cpp index 8d85ce2..1e3c5a4 100644 --- a/media/libstagefright/AMRExtractor.cpp +++ b/media/libstagefright/AMRExtractor.cpp @@ -18,7 +18,8 @@ #define LOG_TAG "AMRExtractor" #include <utils/Log.h> -#include <media/stagefright/AMRExtractor.h> +#include "include/AMRExtractor.h" + #include <media/stagefright/DataSource.h> #include <media/stagefright/MediaBufferGroup.h> #include <media/stagefright/MediaDebug.h> @@ -86,7 +87,7 @@ sp<MediaSource> AMRExtractor::getTrack(size_t index) { return new AMRSource(mDataSource, mIsWide); } -sp<MetaData> AMRExtractor::getTrackMetaData(size_t index) { +sp<MetaData> AMRExtractor::getTrackMetaData(size_t index, uint32_t flags) { if (mInitCheck != OK || index != 0) { return NULL; } @@ -155,7 +156,7 @@ status_t AMRSource::read( *out = NULL; uint8_t header; - ssize_t n = mDataSource->read_at(mOffset, &header, 1); + ssize_t n = mDataSource->readAt(mOffset, &header, 1); if (n < 1) { return ERROR_IO; @@ -191,7 +192,7 @@ status_t AMRSource::read( // Round up bits to bytes and add 1 for the header byte. frameSize = (frameSize + 7) / 8 + 1; - n = mDataSource->read_at(mOffset, buffer->data(), frameSize); + n = mDataSource->readAt(mOffset, buffer->data(), frameSize); if (n != (ssize_t)frameSize) { buffer->release(); @@ -201,10 +202,7 @@ status_t AMRSource::read( } buffer->set_range(0, frameSize); - buffer->meta_data()->setInt32( - kKeyTimeUnits, (mCurrentTimeUs + 500) / 1000); - buffer->meta_data()->setInt32( - kKeyTimeScale, 1000); + buffer->meta_data()->setInt64(kKeyTime, mCurrentTimeUs); mOffset += frameSize; mCurrentTimeUs += 20000; // Each frame is 20ms @@ -220,7 +218,7 @@ bool SniffAMR( const sp<DataSource> &source, String8 *mimeType, float *confidence) { char header[9]; - if (source->read_at(0, header, sizeof(header)) != sizeof(header)) { + if (source->readAt(0, header, sizeof(header)) != sizeof(header)) { return false; } diff --git a/media/libstagefright/Android.mk b/media/libstagefright/Android.mk index 9f71dae..c36e769 100644 --- a/media/libstagefright/Android.mk +++ b/media/libstagefright/Android.mk @@ -16,24 +16,26 @@ ifeq ($(BUILD_WITH_FULL_STAGEFRIGHT),true) LOCAL_SRC_FILES += \ AMRExtractor.cpp \ + AudioPlayer.cpp \ CachingDataSource.cpp \ + CameraSource.cpp \ DataSource.cpp \ FileSource.cpp \ HTTPDataSource.cpp \ HTTPStream.cpp \ JPEGSource.cpp \ - MediaExtractor.cpp \ MP3Extractor.cpp \ MPEG4Extractor.cpp \ MPEG4Writer.cpp \ + MediaExtractor.cpp \ MediaPlayerImpl.cpp \ MmapSource.cpp \ SampleTable.cpp \ ShoutcastSource.cpp \ TimeSource.cpp \ TimedEventQueue.cpp \ - AudioPlayer.cpp \ - stagefright_string.cpp + WAVExtractor.cpp \ + string.cpp endif diff --git a/media/libstagefright/AudioPlayer.cpp b/media/libstagefright/AudioPlayer.cpp index 538facb..d7e3f66 100644 --- a/media/libstagefright/AudioPlayer.cpp +++ b/media/libstagefright/AudioPlayer.cpp @@ -210,15 +210,9 @@ void AudioPlayer::fillBuffer(void *data, size_t size) { break; } - int32_t units, scale; - bool success = - mInputBuffer->meta_data()->findInt32(kKeyTimeUnits, &units); - success = success && - mInputBuffer->meta_data()->findInt32(kKeyTimeScale, &scale); - CHECK(success); - Mutex::Autolock autoLock(mLock); - mPositionTimeMediaUs = (int64_t)units * 1000000 / scale; + CHECK(mInputBuffer->meta_data()->findInt64( + kKeyTime, &mPositionTimeMediaUs)); mPositionTimeRealUs = ((mNumFramesPlayed + size_done / mFrameSize) * 1000000) diff --git a/media/libstagefright/CachingDataSource.cpp b/media/libstagefright/CachingDataSource.cpp index fd00576..23f4897 100644 --- a/media/libstagefright/CachingDataSource.cpp +++ b/media/libstagefright/CachingDataSource.cpp @@ -61,11 +61,11 @@ CachingDataSource::~CachingDataSource() { mData = NULL; } -status_t CachingDataSource::InitCheck() const { - return OK; +status_t CachingDataSource::initCheck() const { + return mSource->initCheck(); } -ssize_t CachingDataSource::read_at(off_t offset, void *data, size_t size) { +ssize_t CachingDataSource::readAt(off_t offset, void *data, size_t size) { Mutex::Autolock autoLock(mLock); size_t total = 0; @@ -82,7 +82,7 @@ ssize_t CachingDataSource::read_at(off_t offset, void *data, size_t size) { if (page == NULL) { page = allocate_page(); page->mOffset = offset - offset % mPageSize; - ssize_t n = mSource->read_at(page->mOffset, page->mData, mPageSize); + ssize_t n = mSource->readAt(page->mOffset, page->mData, mPageSize); if (n < 0) { page->mLength = 0; } else { diff --git a/media/libstagefright/CameraSource.cpp b/media/libstagefright/CameraSource.cpp index 596ab67..e9d8557 100644 --- a/media/libstagefright/CameraSource.cpp +++ b/media/libstagefright/CameraSource.cpp @@ -21,120 +21,142 @@ #include <binder/IServiceManager.h> #include <media/stagefright/CameraSource.h> #include <media/stagefright/MediaDebug.h> +#include <media/stagefright/MediaDefs.h> #include <media/stagefright/MediaErrors.h> #include <media/stagefright/MetaData.h> -#include <ui/ICameraClient.h> -#include <ui/ICameraService.h> +#include <ui/Camera.h> +#include <ui/CameraParameters.h> +#include <ui/GraphicBuffer.h> +#include <ui/ISurface.h> #include <ui/Overlay.h> -#include <utils/String16.h> +#include <utils/String8.h> namespace android { -class CameraBuffer : public MediaBuffer { -public: - CameraBuffer(const sp<IMemory> &frame) - : MediaBuffer(frame->pointer(), frame->size()), - mFrame(frame) { - } +static int64_t getNowUs() { + struct timeval tv; + gettimeofday(&tv, NULL); - sp<IMemory> releaseFrame() { - sp<IMemory> frame = mFrame; - mFrame.clear(); - return frame; - } + return (int64_t)tv.tv_usec + tv.tv_sec * 1000000; +} -private: - sp<IMemory> mFrame; -}; +struct DummySurface : public BnSurface { + DummySurface() {} -class CameraSourceClient : public BnCameraClient { -public: - CameraSourceClient() - : mSource(NULL) { + virtual sp<GraphicBuffer> requestBuffer(int bufferIdx, int usage) { + return NULL; } - virtual void notifyCallback(int32_t msgType, int32_t ext1, int32_t ext2) { - CHECK(mSource != NULL); - mSource->notifyCallback(msgType, ext1, ext2); + virtual status_t registerBuffers(const BufferHeap &buffers) { + return OK; } - virtual void dataCallback(int32_t msgType, const sp<IMemory> &data) { - CHECK(mSource != NULL); - mSource->dataCallback(msgType, data); - } + virtual void postBuffer(ssize_t offset) {} + virtual void unregisterBuffers() {} - void setCameraSource(CameraSource *source) { - mSource = source; + virtual sp<OverlayRef> createOverlay( + uint32_t w, uint32_t h, int32_t format) { + return NULL; } +protected: + virtual ~DummySurface() {} + + DummySurface(const DummySurface &); + DummySurface &operator=(const DummySurface &); +}; + +struct CameraSourceListener : public CameraListener { + CameraSourceListener(const sp<CameraSource> &source); + + virtual void notify(int32_t msgType, int32_t ext1, int32_t ext2); + virtual void postData(int32_t msgType, const sp<IMemory> &dataPtr); + + virtual void postDataTimestamp( + nsecs_t timestamp, int32_t msgType, const sp<IMemory>& dataPtr); + +protected: + virtual ~CameraSourceListener(); + private: - CameraSource *mSource; + wp<CameraSource> mSource; + + CameraSourceListener(const CameraSourceListener &); + CameraSourceListener &operator=(const CameraSourceListener &); }; -class DummySurface : public BnSurface { -public: - DummySurface() {} +CameraSourceListener::CameraSourceListener(const sp<CameraSource> &source) + : mSource(source) { +} - virtual status_t registerBuffers(const BufferHeap &buffers) { - return OK; - } +CameraSourceListener::~CameraSourceListener() { +} - virtual void postBuffer(ssize_t offset) { - } +void CameraSourceListener::notify(int32_t msgType, int32_t ext1, int32_t ext2) { + LOGV("notify(%d, %d, %d)", msgType, ext1, ext2); +} - virtual void unregisterBuffers() { - } - - virtual sp<OverlayRef> createOverlay( - uint32_t w, uint32_t h, int32_t format) { - return NULL; +void CameraSourceListener::postData(int32_t msgType, const sp<IMemory> &dataPtr) { + LOGV("postData(%d, ptr:%p, size:%d)", + msgType, dataPtr->pointer(), dataPtr->size()); + + sp<CameraSource> source = mSource.promote(); + if (source.get() != NULL) { + source->dataCallback(msgType, dataPtr); } -}; +} + +void CameraSourceListener::postDataTimestamp( + nsecs_t timestamp, int32_t msgType, const sp<IMemory>& dataPtr) { + LOGV("postDataTimestamp(%lld, %d, ptr:%p, size:%d)", + timestamp, msgType, dataPtr->pointer(), dataPtr->size()); +} // static CameraSource *CameraSource::Create() { - sp<IServiceManager> sm = defaultServiceManager(); - - sp<ICameraService> service = - interface_cast<ICameraService>( - sm->getService(String16("media.camera"))); + sp<Camera> camera = Camera::connect(); - sp<CameraSourceClient> client = new CameraSourceClient; - sp<ICamera> camera = service->connect(client); - - CameraSource *source = new CameraSource(camera, client); - client->setCameraSource(source); + if (camera.get() == NULL) { + return NULL; + } - return source; + return new CameraSource(camera); } -CameraSource::CameraSource( - const sp<ICamera> &camera, const sp<ICameraClient> &client) +CameraSource::CameraSource(const sp<Camera> &camera) : mCamera(camera), - mCameraClient(client), + mWidth(0), + mHeight(0), + mFirstFrameTimeUs(0), mNumFrames(0), mStarted(false) { - printf("params: \"%s\"\n", mCamera->getParameters().string()); + String8 s = mCamera->getParameters(); + printf("params: \"%s\"\n", s.string()); + + CameraParameters params(s); + params.getPreviewSize(&mWidth, &mHeight); } CameraSource::~CameraSource() { if (mStarted) { stop(); } - - mCamera->disconnect(); } status_t CameraSource::start(MetaData *) { CHECK(!mStarted); - status_t err = mCamera->lock(); - CHECK_EQ(err, OK); + mCamera->setListener(new CameraSourceListener(this)); - err = mCamera->setPreviewDisplay(new DummySurface); + sp<ISurface> dummy = new DummySurface; + status_t err = mCamera->setPreviewDisplay(dummy); CHECK_EQ(err, OK); - mCamera->setPreviewCallbackFlag(1); - mCamera->startPreview(); + + mCamera->setPreviewCallbackFlags( + FRAME_CALLBACK_FLAG_ENABLE_MASK + | FRAME_CALLBACK_FLAG_COPY_OUT_MASK); + + err = mCamera->startPreview(); CHECK_EQ(err, OK); mStarted = true; @@ -146,7 +168,6 @@ status_t CameraSource::stop() { CHECK(mStarted); mCamera->stopPreview(); - mCamera->unlock(); mStarted = false; @@ -157,8 +178,8 @@ sp<MetaData> CameraSource::getFormat() { sp<MetaData> meta = new MetaData; meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_RAW); meta->setInt32(kKeyColorFormat, OMX_COLOR_FormatYUV420SemiPlanar); - meta->setInt32(kKeyWidth, 480); - meta->setInt32(kKeyHeight, 320); + meta->setInt32(kKeyWidth, mWidth); + meta->setInt32(kKeyHeight, mHeight); return meta; } @@ -175,6 +196,7 @@ status_t CameraSource::read( } sp<IMemory> frame; + int64_t frameTime; { Mutex::Autolock autoLock(mLock); @@ -184,41 +206,33 @@ status_t CameraSource::read( frame = *mFrames.begin(); mFrames.erase(mFrames.begin()); - } - int count = mNumFrames++; + frameTime = *mFrameTimes.begin(); + mFrameTimes.erase(mFrameTimes.begin()); + } - *buffer = new CameraBuffer(frame); + *buffer = new MediaBuffer(frame->size()); + memcpy((*buffer)->data(), frame->pointer(), frame->size()); + (*buffer)->set_range(0, frame->size()); (*buffer)->meta_data()->clear(); - (*buffer)->meta_data()->setInt32(kKeyTimeScale, 15); - (*buffer)->meta_data()->setInt32(kKeyTimeUnits, count); - - (*buffer)->add_ref(); - (*buffer)->setObserver(this); + (*buffer)->meta_data()->setInt64(kKeyTime, frameTime); return OK; } -void CameraSource::notifyCallback(int32_t msgType, int32_t ext1, int32_t ext2) { - printf("notifyCallback %d, %d, %d\n", msgType, ext1, ext2); -} - void CameraSource::dataCallback(int32_t msgType, const sp<IMemory> &data) { Mutex::Autolock autoLock(mLock); + int64_t nowUs = getNowUs(); + if (mNumFrames == 0) { + mFirstFrameTimeUs = nowUs; + } + ++mNumFrames; + mFrames.push_back(data); + mFrameTimes.push_back(nowUs - mFirstFrameTimeUs); mFrameAvailableCondition.signal(); } -void CameraSource::signalBufferReturned(MediaBuffer *_buffer) { - CameraBuffer *buffer = static_cast<CameraBuffer *>(_buffer); - - mCamera->releaseRecordingFrame(buffer->releaseFrame()); - - buffer->setObserver(NULL); - buffer->release(); - buffer = NULL; -} - } // namespace android diff --git a/media/libstagefright/DataSource.cpp b/media/libstagefright/DataSource.cpp index daac539..2a6dbc4 100644 --- a/media/libstagefright/DataSource.cpp +++ b/media/libstagefright/DataSource.cpp @@ -14,11 +14,13 @@ * limitations under the License. */ -#include <media/stagefright/AMRExtractor.h> +#include "include/AMRExtractor.h" +#include "include/MP3Extractor.h" +#include "include/MPEG4Extractor.h" +#include "include/WAVExtractor.h" + #include <media/stagefright/DataSource.h> #include <media/stagefright/MediaErrors.h> -#include <media/stagefright/MP3Extractor.h> -#include <media/stagefright/MPEG4Extractor.h> #include <utils/String8.h> namespace android { @@ -27,7 +29,7 @@ bool DataSource::getUInt16(off_t offset, uint16_t *x) { *x = 0; uint8_t byte[2]; - if (read_at(offset, byte, 2) != 2) { + if (readAt(offset, byte, 2) != 2) { return false; } @@ -86,6 +88,7 @@ void DataSource::RegisterDefaultSniffers() { RegisterSniffer(SniffMP3); RegisterSniffer(SniffMPEG4); RegisterSniffer(SniffAMR); + RegisterSniffer(SniffWAV); } } // namespace android diff --git a/media/libstagefright/ESDS.cpp b/media/libstagefright/ESDS.cpp index 53b92a0..28d338c 100644 --- a/media/libstagefright/ESDS.cpp +++ b/media/libstagefright/ESDS.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include <media/stagefright/ESDS.h> +#include "include/ESDS.h" #include <string.h> diff --git a/media/libstagefright/FileSource.cpp b/media/libstagefright/FileSource.cpp index f6b90b2..f318ee3 100644 --- a/media/libstagefright/FileSource.cpp +++ b/media/libstagefright/FileSource.cpp @@ -30,11 +30,11 @@ FileSource::~FileSource() { } } -status_t FileSource::InitCheck() const { +status_t FileSource::initCheck() const { return mFile != NULL ? OK : NO_INIT; } -ssize_t FileSource::read_at(off_t offset, void *data, size_t size) { +ssize_t FileSource::readAt(off_t offset, void *data, size_t size) { Mutex::Autolock autoLock(mLock); int err = fseeko(mFile, offset, SEEK_SET); diff --git a/media/libstagefright/HTTPDataSource.cpp b/media/libstagefright/HTTPDataSource.cpp index 4dedebd..5536801 100644 --- a/media/libstagefright/HTTPDataSource.cpp +++ b/media/libstagefright/HTTPDataSource.cpp @@ -14,17 +14,19 @@ * limitations under the License. */ +#include "include/stagefright_string.h" +#include "include/HTTPStream.h" + #include <stdlib.h> #include <media/stagefright/HTTPDataSource.h> -#include <media/stagefright/HTTPStream.h> #include <media/stagefright/MediaDebug.h> -#include <media/stagefright/stagefright_string.h> namespace android { HTTPDataSource::HTTPDataSource(const char *uri) - : mHost(NULL), + : mHttp(new HTTPStream), + mHost(NULL), mPort(0), mPath(NULL), mBuffer(malloc(kBufferSize)), @@ -65,33 +67,40 @@ HTTPDataSource::HTTPDataSource(const char *uri) mPort = port; mPath = strdup(path.c_str()); - status_t err = mHttp.connect(mHost, mPort); - CHECK_EQ(err, OK); + mInitCheck = mHttp->connect(mHost, mPort); } HTTPDataSource::HTTPDataSource(const char *host, int port, const char *path) - : mHost(strdup(host)), + : mHttp(new HTTPStream), + mHost(strdup(host)), mPort(port), mPath(strdup(path)), mBuffer(malloc(kBufferSize)), mBufferLength(0), mBufferOffset(0) { - status_t err = mHttp.connect(mHost, mPort); - CHECK_EQ(err, OK); + mInitCheck = mHttp->connect(mHost, mPort); +} + +status_t HTTPDataSource::initCheck() const { + return mInitCheck; } HTTPDataSource::~HTTPDataSource() { - mHttp.disconnect(); + mHttp->disconnect(); free(mBuffer); mBuffer = NULL; free(mPath); mPath = NULL; + + delete mHttp; + mHttp = NULL; } -ssize_t HTTPDataSource::read_at(off_t offset, void *data, size_t size) { - if (offset >= mBufferOffset && offset < mBufferOffset + mBufferLength) { +ssize_t HTTPDataSource::readAt(off_t offset, void *data, size_t size) { + if (offset >= mBufferOffset + && offset < (off_t)(mBufferOffset + mBufferLength)) { size_t num_bytes_available = mBufferLength - (offset - mBufferOffset); size_t copy = num_bytes_available; @@ -119,19 +128,19 @@ ssize_t HTTPDataSource::read_at(off_t offset, void *data, size_t size) { status_t err; int attempt = 1; for (;;) { - if ((err = mHttp.send("GET ")) != OK - || (err = mHttp.send(mPath)) != OK - || (err = mHttp.send(" HTTP/1.1\r\n")) != OK - || (err = mHttp.send(host)) != OK - || (err = mHttp.send(range)) != OK - || (err = mHttp.send("\r\n")) != OK - || (err = mHttp.receive_header(&http_status)) != OK) { + if ((err = mHttp->send("GET ")) != OK + || (err = mHttp->send(mPath)) != OK + || (err = mHttp->send(" HTTP/1.1\r\n")) != OK + || (err = mHttp->send(host)) != OK + || (err = mHttp->send(range)) != OK + || (err = mHttp->send("\r\n")) != OK + || (err = mHttp->receive_header(&http_status)) != OK) { if (attempt == 3) { return err; } - mHttp.connect(mHost, mPort); + mHttp->connect(mHost, mPort); ++attempt; } else { break; @@ -143,14 +152,14 @@ ssize_t HTTPDataSource::read_at(off_t offset, void *data, size_t size) { } string value; - if (!mHttp.find_header_value("Content-Length", &value)) { + if (!mHttp->find_header_value("Content-Length", &value)) { return UNKNOWN_ERROR; } char *end; unsigned long contentLength = strtoul(value.c_str(), &end, 10); - ssize_t num_bytes_received = mHttp.receive(mBuffer, contentLength); + ssize_t num_bytes_received = mHttp->receive(mBuffer, contentLength); if (num_bytes_received <= 0) { return num_bytes_received; diff --git a/media/libstagefright/HTTPStream.cpp b/media/libstagefright/HTTPStream.cpp index 6af7df9..02f9439 100644 --- a/media/libstagefright/HTTPStream.cpp +++ b/media/libstagefright/HTTPStream.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "include/HTTPStream.h" + #include <sys/socket.h> #include <arpa/inet.h> @@ -25,7 +27,6 @@ #include <string.h> #include <unistd.h> -#include <media/stagefright/HTTPStream.h> #include <media/stagefright/MediaDebug.h> namespace android { diff --git a/media/libstagefright/JPEGSource.cpp b/media/libstagefright/JPEGSource.cpp index d1dfd83..a4be2dd 100644 --- a/media/libstagefright/JPEGSource.cpp +++ b/media/libstagefright/JPEGSource.cpp @@ -119,7 +119,7 @@ status_t JPEGSource::read( MediaBuffer *buffer; mGroup->acquire_buffer(&buffer); - ssize_t n = mSource->read_at(mOffset, buffer->data(), mSize - mOffset); + ssize_t n = mSource->readAt(mOffset, buffer->data(), mSize - mOffset); if (n <= 0) { buffer->release(); @@ -156,13 +156,13 @@ status_t JPEGSource::parseJPEG() { for (;;) { uint8_t marker; - if (mSource->read_at(i++, &marker, 1) != 1) { + if (mSource->readAt(i++, &marker, 1) != 1) { return ERROR_IO; } CHECK_EQ(marker, 0xff); - if (mSource->read_at(i++, &marker, 1) != 1) { + if (mSource->readAt(i++, &marker, 1) != 1) { return ERROR_IO; } diff --git a/media/libstagefright/MP3Extractor.cpp b/media/libstagefright/MP3Extractor.cpp index 7fd699f..8dd8ea9 100644 --- a/media/libstagefright/MP3Extractor.cpp +++ b/media/libstagefright/MP3Extractor.cpp @@ -18,8 +18,9 @@ #define LOG_TAG "MP3Extractor" #include <utils/Log.h> +#include "include/MP3Extractor.h" + #include <media/stagefright/DataSource.h> -#include <media/stagefright/MP3Extractor.h> #include <media/stagefright/MediaBuffer.h> #include <media/stagefright/MediaBufferGroup.h> #include <media/stagefright/MediaDebug.h> @@ -147,7 +148,12 @@ static bool get_mp3_frame_size( *out_bitrate = bitrate; } - *frame_size = 144000 * bitrate / sampling_rate + padding; + if (version == 3 /* V1 */) { + *frame_size = 144000 * bitrate / sampling_rate + padding; + } else { + // V2 or V2.5 + *frame_size = 72000 * bitrate / sampling_rate + padding; + } } if (out_sampling_rate) { @@ -166,6 +172,33 @@ static bool get_mp3_frame_size( static bool Resync( const sp<DataSource> &source, uint32_t match_header, off_t *inout_pos, uint32_t *out_header) { + if (*inout_pos == 0) { + // Skip an optional ID3 header if syncing at the very beginning + // of the datasource. + + uint8_t id3header[10]; + if (source->readAt(0, id3header, sizeof(id3header)) + < (ssize_t)sizeof(id3header)) { + // If we can't even read these 10 bytes, we might as well bail out, + // even if there _were_ 10 bytes of valid mp3 audio data... + return false; + } + + if (id3header[0] == 'I' && id3header[1] == 'D' && id3header[2] == '3') { + // Skip the ID3v2 header. + + size_t len = + ((id3header[6] & 0x7f) << 21) + | ((id3header[7] & 0x7f) << 14) + | ((id3header[8] & 0x7f) << 7) + | (id3header[9] & 0x7f); + + len += 10; + + *inout_pos += len; + } + } + // Everything must match except for // protection, bitrate, padding, private bits and mode extension. const uint32_t kMask = 0xfffe0ccf; @@ -195,7 +228,7 @@ static bool Resync( buffer_length = buffer_length - buffer_offset; buffer_offset = 0; - ssize_t n = source->read_at( + ssize_t n = source->readAt( pos, &buffer[buffer_length], kMaxFrameSize - buffer_length); if (n <= 0) { @@ -232,7 +265,7 @@ static bool Resync( valid = true; for (int j = 0; j < 3; ++j) { uint8_t tmp[4]; - if (source->read_at(test_pos, tmp, 4) < 4) { + if (source->readAt(test_pos, tmp, 4) < 4) { valid = false; break; } @@ -338,10 +371,9 @@ MP3Extractor::MP3Extractor(const sp<DataSource> &source) off_t fileSize; if (mDataSource->getSize(&fileSize) == OK) { - mMeta->setInt32( + mMeta->setInt64( kKeyDuration, - 8 * (fileSize - mFirstFramePos) / bitrate); - mMeta->setInt32(kKeyTimeScale, 1000); + 8000LL * (fileSize - mFirstFramePos) / bitrate); } } } @@ -362,7 +394,7 @@ sp<MediaSource> MP3Extractor::getTrack(size_t index) { mMeta, mDataSource, mFirstFramePos, mFixedHeader); } -sp<MetaData> MP3Extractor::getTrackMetaData(size_t index) { +sp<MetaData> MP3Extractor::getTrackMetaData(size_t index, uint32_t flags) { if (mFirstFramePos < 0 || index != 0) { return NULL; } @@ -448,7 +480,7 @@ status_t MP3Source::read( size_t frame_size; for (;;) { - ssize_t n = mDataSource->read_at(mCurrentPos, buffer->data(), 4); + ssize_t n = mDataSource->readAt(mCurrentPos, buffer->data(), 4); if (n < 4) { buffer->release(); buffer = NULL; @@ -482,7 +514,7 @@ status_t MP3Source::read( CHECK(frame_size <= buffer->size()); - ssize_t n = mDataSource->read_at(mCurrentPos, buffer->data(), frame_size); + ssize_t n = mDataSource->readAt(mCurrentPos, buffer->data(), frame_size); if (n < (ssize_t)frame_size) { buffer->release(); buffer = NULL; @@ -492,8 +524,7 @@ status_t MP3Source::read( buffer->set_range(0, frame_size); - buffer->meta_data()->setInt32(kKeyTimeUnits, mCurrentTimeUs / 1000); - buffer->meta_data()->setInt32(kKeyTimeScale, 1000); + buffer->meta_data()->setInt64(kKeyTime, mCurrentTimeUs); mCurrentPos += frame_size; mCurrentTimeUs += 1152 * 1000000 / 44100; diff --git a/media/libstagefright/MPEG4Extractor.cpp b/media/libstagefright/MPEG4Extractor.cpp index 9174d19..9d17064 100644 --- a/media/libstagefright/MPEG4Extractor.cpp +++ b/media/libstagefright/MPEG4Extractor.cpp @@ -17,6 +17,9 @@ #define LOG_TAG "MPEG4Extractor" #include <utils/Log.h> +#include "include/MPEG4Extractor.h" +#include "include/SampleTable.h" + #include <arpa/inet.h> #include <ctype.h> @@ -25,14 +28,12 @@ #include <string.h> #include <media/stagefright/DataSource.h> -#include <media/stagefright/MPEG4Extractor.h> #include <media/stagefright/MediaBuffer.h> #include <media/stagefright/MediaBufferGroup.h> #include <media/stagefright/MediaDebug.h> #include <media/stagefright/MediaDefs.h> #include <media/stagefright/MediaSource.h> #include <media/stagefright/MetaData.h> -#include <media/stagefright/SampleTable.h> #include <media/stagefright/Utils.h> #include <utils/String8.h> @@ -43,6 +44,7 @@ public: // Caller retains ownership of both "dataSource" and "sampleTable". MPEG4Source(const sp<MetaData> &format, const sp<DataSource> &dataSource, + int32_t timeScale, const sp<SampleTable> &sampleTable); virtual status_t start(MetaData *params = NULL); @@ -177,7 +179,8 @@ size_t MPEG4Extractor::countTracks() { return n; } -sp<MetaData> MPEG4Extractor::getTrackMetaData(size_t index) { +sp<MetaData> MPEG4Extractor::getTrackMetaData( + size_t index, uint32_t flags) { status_t err; if ((err = readMetaData()) != OK) { return NULL; @@ -197,6 +200,25 @@ sp<MetaData> MPEG4Extractor::getTrackMetaData(size_t index) { return NULL; } + if ((flags & kIncludeExtensiveMetaData) + && !track->includes_expensive_metadata) { + track->includes_expensive_metadata = true; + + const char *mime; + CHECK(track->meta->findCString(kKeyMIMEType, &mime)); + if (!strncasecmp("video/", mime, 6)) { + uint32_t sampleIndex; + uint32_t sampleTime; + if (track->sampleTable->findThumbnailSample(&sampleIndex) == OK + && track->sampleTable->getDecodingTime( + sampleIndex, &sampleTime) == OK) { + track->meta->setInt64( + kKeyThumbnailTime, + ((int64_t)sampleTime * 1000000) / track->timescale); + } + } + } + return track->meta; } @@ -227,7 +249,7 @@ static void MakeFourCCString(uint32_t x, char *s) { status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { uint32_t hdr[2]; - if (mDataSource->read_at(*offset, hdr, 8) < 8) { + if (mDataSource->readAt(*offset, hdr, 8) < 8) { return ERROR_IO; } uint64_t chunk_size = ntohl(hdr[0]); @@ -235,7 +257,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { off_t data_offset = *offset + 8; if (chunk_size == 1) { - if (mDataSource->read_at(*offset + 8, &chunk_size, 8) < 8) { + if (mDataSource->readAt(*offset + 8, &chunk_size, 8) < 8) { return ERROR_IO; } chunk_size = ntoh64(chunk_size); @@ -252,7 +274,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { char buffer[256]; if (chunk_size <= sizeof(buffer)) { - if (mDataSource->read_at(*offset, buffer, chunk_size) < chunk_size) { + if (mDataSource->readAt(*offset, buffer, chunk_size) < chunk_size) { return ERROR_IO; } @@ -298,7 +320,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { CHECK(chunk_data_size >= 4); uint8_t version; - if (mDataSource->read_at(data_offset, &version, 1) < 1) { + if (mDataSource->readAt(data_offset, &version, 1) < 1) { return ERROR_IO; } @@ -312,7 +334,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { } uint8_t buffer[36 + 60]; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, buffer, sizeof(buffer)) < (ssize_t)sizeof(buffer)) { return ERROR_IO; } @@ -329,7 +351,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { } uint8_t buffer[24 + 60]; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, buffer, sizeof(buffer)) < (ssize_t)sizeof(buffer)) { return ERROR_IO; } @@ -351,6 +373,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { mLastTrack = track; track->meta = new MetaData; + track->includes_expensive_metadata = false; track->timescale = 0; track->sampleTable = new SampleTable(mDataSource); track->meta->setCString(kKeyMIMEType, "application/octet-stream"); @@ -366,7 +389,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { } uint8_t version; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, &version, sizeof(version)) < (ssize_t)sizeof(version)) { return ERROR_IO; @@ -383,18 +406,17 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { } uint32_t timescale; - if (mDataSource->read_at( + if (mDataSource->readAt( timescale_offset, ×cale, sizeof(timescale)) < (ssize_t)sizeof(timescale)) { return ERROR_IO; } mLastTrack->timescale = ntohl(timescale); - mLastTrack->meta->setInt32(kKeyTimeScale, mLastTrack->timescale); int64_t duration; if (version == 1) { - if (mDataSource->read_at( + if (mDataSource->readAt( timescale_offset + 4, &duration, sizeof(duration)) < (ssize_t)sizeof(duration)) { return ERROR_IO; @@ -402,14 +424,15 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { duration = ntoh64(duration); } else { int32_t duration32; - if (mDataSource->read_at( + if (mDataSource->readAt( timescale_offset + 4, &duration32, sizeof(duration32)) < (ssize_t)sizeof(duration32)) { return ERROR_IO; } duration = ntohl(duration32); } - mLastTrack->meta->setInt32(kKeyDuration, duration); + mLastTrack->meta->setInt64( + kKeyDuration, (duration * 1000000) / mLastTrack->timescale); *offset += chunk_size; break; @@ -422,7 +445,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { } uint8_t buffer[24]; - if (mDataSource->read_at(data_offset, buffer, 24) < 24) { + if (mDataSource->readAt(data_offset, buffer, 24) < 24) { return ERROR_IO; } @@ -449,7 +472,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { uint8_t buffer[8]; CHECK(chunk_data_size >= (off_t)sizeof(buffer)); - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, buffer, 8) < 8) { return ERROR_IO; } @@ -492,7 +515,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { return ERROR_MALFORMED; } - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, buffer, sizeof(buffer)) < (ssize_t)sizeof(buffer)) { return ERROR_IO; } @@ -544,7 +567,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { return ERROR_MALFORMED; } - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, buffer, sizeof(buffer)) < (ssize_t)sizeof(buffer)) { return ERROR_IO; } @@ -655,7 +678,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { return ERROR_BUFFER_TOO_SMALL; } - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, buffer, chunk_data_size) < chunk_data_size) { return ERROR_IO; } @@ -679,7 +702,7 @@ status_t MPEG4Extractor::parseChunk(off_t *offset, int depth) { return ERROR_BUFFER_TOO_SMALL; } - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, buffer, chunk_data_size) < chunk_data_size) { return ERROR_IO; } @@ -722,7 +745,7 @@ sp<MediaSource> MPEG4Extractor::getTrack(size_t index) { } return new MPEG4Source( - track->meta, mDataSource, track->sampleTable); + track->meta, mDataSource, track->timescale, track->sampleTable); } //////////////////////////////////////////////////////////////////////////////// @@ -730,10 +753,11 @@ sp<MediaSource> MPEG4Extractor::getTrack(size_t index) { MPEG4Source::MPEG4Source( const sp<MetaData> &format, const sp<DataSource> &dataSource, + int32_t timeScale, const sp<SampleTable> &sampleTable) : mFormat(format), mDataSource(dataSource), - mTimescale(0), + mTimescale(timeScale), mSampleTable(sampleTable), mCurrentSampleIndex(0), mIsAVC(false), @@ -746,9 +770,6 @@ MPEG4Source::MPEG4Source( bool success = mFormat->findCString(kKeyMIMEType, &mime); CHECK(success); - success = mFormat->findInt32(kKeyTimeScale, &mTimescale); - CHECK(success); - mIsAVC = !strcasecmp(mime, MEDIA_MIMETYPE_VIDEO_AVC); } @@ -868,7 +889,7 @@ status_t MPEG4Source::read( if (!mIsAVC || mWantsNALFragments) { if (newBuffer) { ssize_t num_bytes_read = - mDataSource->read_at(offset, (uint8_t *)mBuffer->data(), size); + mDataSource->readAt(offset, (uint8_t *)mBuffer->data(), size); if (num_bytes_read < (ssize_t)size) { mBuffer->release(); @@ -879,8 +900,8 @@ status_t MPEG4Source::read( mBuffer->set_range(0, size); mBuffer->meta_data()->clear(); - mBuffer->meta_data()->setInt32(kKeyTimeUnits, dts); - mBuffer->meta_data()->setInt32(kKeyTimeScale, mTimescale); + mBuffer->meta_data()->setInt64( + kKeyTime, ((int64_t)dts * 1000000) / mTimescale); ++mCurrentSampleIndex; } @@ -923,7 +944,7 @@ status_t MPEG4Source::read( // the start code (0x00 00 00 01). ssize_t num_bytes_read = - mDataSource->read_at(offset, mSrcBuffer, size); + mDataSource->readAt(offset, mSrcBuffer, size); if (num_bytes_read < (ssize_t)size) { mBuffer->release(); @@ -959,8 +980,8 @@ status_t MPEG4Source::read( mBuffer->set_range(0, dstOffset); mBuffer->meta_data()->clear(); - mBuffer->meta_data()->setInt32(kKeyTimeUnits, dts); - mBuffer->meta_data()->setInt32(kKeyTimeScale, mTimescale); + mBuffer->meta_data()->setInt64( + kKeyTime, ((int64_t)dts * 1000000) / mTimescale); ++mCurrentSampleIndex; *out = mBuffer; @@ -974,13 +995,14 @@ bool SniffMPEG4( const sp<DataSource> &source, String8 *mimeType, float *confidence) { uint8_t header[8]; - ssize_t n = source->read_at(4, header, sizeof(header)); + ssize_t n = source->readAt(4, header, sizeof(header)); if (n < (ssize_t)sizeof(header)) { return false; } if (!memcmp(header, "ftyp3gp", 7) || !memcmp(header, "ftypmp42", 8) - || !memcmp(header, "ftypisom", 8) || !memcmp(header, "ftypM4V ", 8)) { + || !memcmp(header, "ftypisom", 8) || !memcmp(header, "ftypM4V ", 8) + || !memcmp(header, "ftypM4A ", 8)) { *mimeType = MEDIA_MIMETYPE_CONTAINER_MPEG4; *confidence = 0.1; diff --git a/media/libstagefright/MPEG4Writer.cpp b/media/libstagefright/MPEG4Writer.cpp index fa35768..9a7a873 100644 --- a/media/libstagefright/MPEG4Writer.cpp +++ b/media/libstagefright/MPEG4Writer.cpp @@ -399,15 +399,11 @@ void MPEG4Writer::Track::threadEntry() { info.size = buffer->range_length(); info.offset = offset; - int32_t units, scale; - bool success = - buffer->meta_data()->findInt32(kKeyTimeUnits, &units); - CHECK(success); - success = - buffer->meta_data()->findInt32(kKeyTimeScale, &scale); - CHECK(success); - - info.timestamp = (int64_t)units * 1000 / scale; + int64_t timestampUs; + CHECK(buffer->meta_data()->findInt64(kKeyTime, ×tampUs)); + + // Our timestamp is in ms. + info.timestamp = (timestampUs + 500) / 1000; mSampleInfos.push_back(info); diff --git a/media/libstagefright/MediaBuffer.cpp b/media/libstagefright/MediaBuffer.cpp index f3c0e73..b973745 100644 --- a/media/libstagefright/MediaBuffer.cpp +++ b/media/libstagefright/MediaBuffer.cpp @@ -108,10 +108,10 @@ size_t MediaBuffer::range_length() const { } void MediaBuffer::set_range(size_t offset, size_t length) { - if (offset < 0 || offset + length > mSize) { + if (offset + length > mSize) { LOGE("offset = %d, length = %d, mSize = %d", offset, length, mSize); } - CHECK(offset >= 0 && offset + length <= mSize); + CHECK(offset + length <= mSize); mRangeOffset = offset; mRangeLength = length; diff --git a/media/libstagefright/MediaDefs.cpp b/media/libstagefright/MediaDefs.cpp index 87b5b24..04b1454 100644 --- a/media/libstagefright/MediaDefs.cpp +++ b/media/libstagefright/MediaDefs.cpp @@ -32,5 +32,6 @@ const char *MEDIA_MIMETYPE_AUDIO_AAC = "audio/mp4a-latm"; const char *MEDIA_MIMETYPE_AUDIO_RAW = "audio/raw"; const char *MEDIA_MIMETYPE_CONTAINER_MPEG4 = "video/mpeg4"; +const char *MEDIA_MIMETYPE_CONTAINER_WAV = "audio/wav"; } // namespace android diff --git a/media/libstagefright/MediaExtractor.cpp b/media/libstagefright/MediaExtractor.cpp index 8535f52..19a1f85 100644 --- a/media/libstagefright/MediaExtractor.cpp +++ b/media/libstagefright/MediaExtractor.cpp @@ -18,12 +18,17 @@ #define LOG_TAG "MediaExtractor" #include <utils/Log.h> -#include <media/stagefright/AMRExtractor.h> +#include "include/AMRExtractor.h" +#include "include/MP3Extractor.h" +#include "include/MPEG4Extractor.h" +#include "include/WAVExtractor.h" + +#include <media/stagefright/CachingDataSource.h> #include <media/stagefright/DataSource.h> +#include <media/stagefright/HTTPDataSource.h> #include <media/stagefright/MediaDefs.h> -#include <media/stagefright/MP3Extractor.h> -#include <media/stagefright/MPEG4Extractor.h> #include <media/stagefright/MediaExtractor.h> +#include <media/stagefright/MmapSource.h> #include <utils/String8.h> namespace android { @@ -41,7 +46,7 @@ sp<MediaExtractor> MediaExtractor::Create( } mime = tmp.string(); - LOGI("Autodetected media content as '%s' with confidence %.2f", + LOGV("Autodetected media content as '%s' with confidence %.2f", mime, confidence); } @@ -53,9 +58,32 @@ sp<MediaExtractor> MediaExtractor::Create( } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_NB) || !strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_WB)) { return new AMRExtractor(source); + } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WAV)) { + return new WAVExtractor(source); } return NULL; } +// static +sp<MediaExtractor> MediaExtractor::CreateFromURI( + const char *uri, const char *mime) { + sp<DataSource> source; + if (!strncasecmp("file://", uri, 7)) { + source = new MmapSource(uri + 7); + } else if (!strncasecmp("http://", uri, 7)) { + source = new HTTPDataSource(uri); + source = new CachingDataSource(source, 64 * 1024, 10); + } else { + // Assume it's a filename. + source = new MmapSource(uri); + } + + if (source == NULL || source->initCheck() != OK) { + return NULL; + } + + return Create(source, mime); +} + } // namespace android diff --git a/media/libstagefright/MediaPlayerImpl.cpp b/media/libstagefright/MediaPlayerImpl.cpp index 622ea7e..eb9fe7f 100644 --- a/media/libstagefright/MediaPlayerImpl.cpp +++ b/media/libstagefright/MediaPlayerImpl.cpp @@ -18,16 +18,17 @@ #define LOG_TAG "MediaPlayerImpl" #include "utils/Log.h" +#include "include/stagefright_string.h" +#include "include/HTTPStream.h" + #include <OMX_Component.h> #include <unistd.h> #include <media/stagefright/AudioPlayer.h> -#include <media/stagefright/CachingDataSource.h> // #include <media/stagefright/CameraSource.h> -#include <media/stagefright/HTTPDataSource.h> -#include <media/stagefright/HTTPStream.h> #include <media/stagefright/MediaDebug.h> +#include <media/stagefright/MediaDefs.h> #include <media/stagefright/MediaExtractor.h> #include <media/stagefright/MediaPlayerImpl.h> #include <media/stagefright/MetaData.h> @@ -69,18 +70,7 @@ MediaPlayerImpl::MediaPlayerImpl(const char *uri) mVideoDecoder = CameraSource::Create(); #endif } else { - sp<DataSource> source; - if (!strncasecmp("file://", uri, 7)) { - source = new MmapSource(uri + 7); - } else if (!strncasecmp("http://", uri, 7)) { - source = new HTTPDataSource(uri); - source = new CachingDataSource(source, 64 * 1024, 10); - } else { - // Assume it's a filename. - source = new MmapSource(uri); - } - - mExtractor = MediaExtractor::Create(source); + mExtractor = MediaExtractor::CreateFromURI(uri); if (mExtractor == NULL) { return; @@ -258,6 +248,13 @@ void MediaPlayerImpl::videoEntry() { status_t err = mVideoDecoder->read(&buffer, &options); CHECK((err == OK && buffer != NULL) || (err != OK && buffer == NULL)); + if (err == INFO_FORMAT_CHANGED) { + LOGI("format changed."); + depopulateISurface(); + populateISurface(); + continue; + } + if (err == ERROR_END_OF_STREAM || err != OK) { eof = true; continue; @@ -269,15 +266,9 @@ void MediaPlayerImpl::videoEntry() { continue; } - int32_t units, scale; - bool success = - buffer->meta_data()->findInt32(kKeyTimeUnits, &units); - CHECK(success); - success = - buffer->meta_data()->findInt32(kKeyTimeScale, &scale); - CHECK(success); + int64_t pts_us; + CHECK(buffer->meta_data()->findInt64(kKeyTime, &pts_us)); - int64_t pts_us = (int64_t)units * 1000000 / scale; { Mutex::Autolock autoLock(mLock); mVideoPosition = pts_us; @@ -391,12 +382,10 @@ void MediaPlayerImpl::init() { sp<MediaSource> source = mExtractor->getTrack(i); - int32_t units, scale; - if (meta->findInt32(kKeyDuration, &units) - && meta->findInt32(kKeyTimeScale, &scale)) { - int64_t duration_us = (int64_t)units * 1000000 / scale; - if (duration_us > mDuration) { - mDuration = duration_us; + int64_t durationUs; + if (meta->findInt64(kKeyDuration, &durationUs)) { + if (durationUs > mDuration) { + mDuration = durationUs; } } @@ -415,8 +404,15 @@ void MediaPlayerImpl::setAudioSource(const sp<MediaSource> &source) { sp<MetaData> meta = source->getFormat(); - mAudioDecoder = OMXCodec::Create( - mClient.interface(), meta, false /* createEncoder */, source); + const char *mime; + CHECK(meta->findCString(kKeyMIMEType, &mime)); + + if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_RAW)) { + mAudioDecoder = source; + } else { + mAudioDecoder = OMXCodec::Create( + mClient.interface(), meta, false /* createEncoder */, source); + } } void MediaPlayerImpl::setVideoSource(const sp<MediaSource> &source) { @@ -621,6 +617,9 @@ void MediaPlayerImpl::populateISurface() { success = success && meta->findInt32(kKeyHeight, &decodedHeight); CHECK(success); + LOGI("mVideoWidth=%d, mVideoHeight=%d, decodedWidth=%d, decodedHeight=%d", + mVideoWidth, mVideoHeight, decodedWidth, decodedHeight); + if (mSurface.get() != NULL) { mVideoRenderer = mClient.interface()->createRenderer( diff --git a/media/libstagefright/MetaData.cpp b/media/libstagefright/MetaData.cpp index 6b067cb..63b476e 100644 --- a/media/libstagefright/MetaData.cpp +++ b/media/libstagefright/MetaData.cpp @@ -58,6 +58,10 @@ bool MetaData::setInt32(uint32_t key, int32_t value) { return setData(key, TYPE_INT32, &value, sizeof(value)); } +bool MetaData::setInt64(uint32_t key, int64_t value) { + return setData(key, TYPE_INT64, &value, sizeof(value)); +} + bool MetaData::setFloat(uint32_t key, float value) { return setData(key, TYPE_FLOAT, &value, sizeof(value)); } @@ -94,6 +98,21 @@ bool MetaData::findInt32(uint32_t key, int32_t *value) { return true; } +bool MetaData::findInt64(uint32_t key, int64_t *value) { + uint32_t type; + const void *data; + size_t size; + if (!findData(key, &type, &data, &size) || type != TYPE_INT64) { + return false; + } + + CHECK_EQ(size, sizeof(*value)); + + *value = *(int64_t *)data; + + return true; +} + bool MetaData::findFloat(uint32_t key, float *value) { uint32_t type; const void *data; diff --git a/media/libstagefright/MmapSource.cpp b/media/libstagefright/MmapSource.cpp index 47d95f9..42749cf 100644 --- a/media/libstagefright/MmapSource.cpp +++ b/media/libstagefright/MmapSource.cpp @@ -34,7 +34,10 @@ MmapSource::MmapSource(const char *filename) mBase(NULL), mSize(0) { LOGV("MmapSource '%s'", filename); - CHECK(mFd >= 0); + + if (mFd < 0) { + return; + } off_t size = lseek(mFd, 0, SEEK_END); mSize = (size_t)size; @@ -78,12 +81,12 @@ MmapSource::~MmapSource() { } } -status_t MmapSource::InitCheck() const { +status_t MmapSource::initCheck() const { return mFd == -1 ? NO_INIT : OK; } -ssize_t MmapSource::read_at(off_t offset, void *data, size_t size) { - LOGV("read_at offset:%ld data:%p size:%d", offset, data, size); +ssize_t MmapSource::readAt(off_t offset, void *data, size_t size) { + LOGV("readAt offset:%ld data:%p size:%d", offset, data, size); CHECK(offset >= 0); size_t avail = 0; diff --git a/media/libstagefright/OMXCodec.cpp b/media/libstagefright/OMXCodec.cpp index ebf1e0c..6c0367a 100644 --- a/media/libstagefright/OMXCodec.cpp +++ b/media/libstagefright/OMXCodec.cpp @@ -18,11 +18,12 @@ #define LOG_TAG "OMXCodec" #include <utils/Log.h> +#include "include/ESDS.h" + #include <binder/IServiceManager.h> #include <binder/MemoryDealer.h> #include <binder/ProcessState.h> #include <media/IMediaPlayerService.h> -#include <media/stagefright/ESDS.h> #include <media/stagefright/MediaBuffer.h> #include <media/stagefright/MediaBufferGroup.h> #include <media/stagefright/MediaDebug.h> @@ -39,6 +40,8 @@ namespace android { +static const int OMX_QCOM_COLOR_FormatYVU420SemiPlanar = 0x7FA30C00; + struct CodecInfo { const char *mime; const char *codec; @@ -169,52 +172,37 @@ static void InitOMXParams(T *params) { params->nVersion.s.nStep = 0; } -// static -sp<OMXCodec> OMXCodec::Create( - const sp<IOMX> &omx, - const sp<MetaData> &meta, bool createEncoder, - const sp<MediaSource> &source, - const char *matchComponentName) { - const char *mime; - bool success = meta->findCString(kKeyMIMEType, &mime); - CHECK(success); - - const char *componentName = NULL; - sp<OMXCodecObserver> observer = new OMXCodecObserver; - IOMX::node_id node = 0; - for (int index = 0;; ++index) { - if (createEncoder) { - componentName = GetCodec( - kEncoderInfo, sizeof(kEncoderInfo) / sizeof(kEncoderInfo[0]), - mime, index); - } else { - componentName = GetCodec( - kDecoderInfo, sizeof(kDecoderInfo) / sizeof(kDecoderInfo[0]), - mime, index); - } +static bool IsSoftwareCodec(const char *componentName) { + if (!strncmp("OMX.PV.", componentName, 7)) { + return true; + } - if (!componentName) { - return NULL; - } + return false; +} - // If a specific codec is requested, skip the non-matching ones. - if (matchComponentName && strcmp(componentName, matchComponentName)) { - continue; - } +static int CompareSoftwareCodecsFirst( + const String8 *elem1, const String8 *elem2) { + bool isSoftwareCodec1 = IsSoftwareCodec(elem1->string()); + bool isSoftwareCodec2 = IsSoftwareCodec(elem2->string()); - LOGV("Attempting to allocate OMX node '%s'", componentName); + if (isSoftwareCodec1) { + if (isSoftwareCodec2) { return 0; } + return -1; + } - status_t err = omx->allocateNode(componentName, observer, &node); - if (err == OK) { - LOGI("Successfully allocated OMX node '%s'", componentName); - break; - } + if (isSoftwareCodec2) { + return 1; } + return 0; +} + +// static +uint32_t OMXCodec::getComponentQuirks(const char *componentName) { uint32_t quirks = 0; + if (!strcmp(componentName, "OMX.PV.avcdec")) { quirks |= kWantsNALFragments; - quirks |= kOutputDimensionsAre16Aligned; } if (!strcmp(componentName, "OMX.TI.MP3.decode")) { quirks |= kNeedsFlushBeforeDisable; @@ -222,20 +210,15 @@ sp<OMXCodec> OMXCodec::Create( if (!strcmp(componentName, "OMX.TI.AAC.decode")) { quirks |= kNeedsFlushBeforeDisable; quirks |= kRequiresFlushCompleteEmulation; - - // The following is currently necessary for proper shutdown - // behaviour, but NOT enabled by default in order to make the - // bug reproducible... - // quirks |= kRequiresFlushBeforeShutdown; } if (!strncmp(componentName, "OMX.qcom.video.encoder.", 23)) { quirks |= kRequiresLoadedToIdleAfterAllocation; quirks |= kRequiresAllocateBufferOnInputPorts; + quirks |= kRequiresAllocateBufferOnOutputPorts; } if (!strncmp(componentName, "OMX.qcom.video.decoder.", 23)) { // XXX Required on P....on only. quirks |= kRequiresAllocateBufferOnOutputPorts; - quirks |= kOutputDimensionsAre16Aligned; } if (!strncmp(componentName, "OMX.TI.", 7)) { @@ -248,8 +231,94 @@ sp<OMXCodec> OMXCodec::Create( quirks |= kRequiresAllocateBufferOnOutputPorts; } + return quirks; +} + +// static +void OMXCodec::findMatchingCodecs( + const char *mime, + bool createEncoder, const char *matchComponentName, + uint32_t flags, + Vector<String8> *matchingCodecs) { + matchingCodecs->clear(); + + for (int index = 0;; ++index) { + const char *componentName; + + if (createEncoder) { + componentName = GetCodec( + kEncoderInfo, + sizeof(kEncoderInfo) / sizeof(kEncoderInfo[0]), + mime, index); + } else { + componentName = GetCodec( + kDecoderInfo, + sizeof(kDecoderInfo) / sizeof(kDecoderInfo[0]), + mime, index); + } + + if (!componentName) { + break; + } + + // If a specific codec is requested, skip the non-matching ones. + if (matchComponentName && strcmp(componentName, matchComponentName)) { + continue; + } + + matchingCodecs->push(String8(componentName)); + } + + if (flags & kPreferSoftwareCodecs) { + matchingCodecs->sort(CompareSoftwareCodecsFirst); + } +} + +// static +sp<OMXCodec> OMXCodec::Create( + const sp<IOMX> &omx, + const sp<MetaData> &meta, bool createEncoder, + const sp<MediaSource> &source, + const char *matchComponentName, + uint32_t flags) { + const char *mime; + bool success = meta->findCString(kKeyMIMEType, &mime); + CHECK(success); + + Vector<String8> matchingCodecs; + findMatchingCodecs( + mime, createEncoder, matchComponentName, flags, &matchingCodecs); + + if (matchingCodecs.isEmpty()) { + return NULL; + } + + sp<OMXCodecObserver> observer = new OMXCodecObserver; + IOMX::node_id node = 0; + success = false; + + const char *componentName; + for (size_t i = 0; i < matchingCodecs.size(); ++i) { + componentName = matchingCodecs[i].string(); + + LOGV("Attempting to allocate OMX node '%s'", componentName); + + status_t err = omx->allocateNode(componentName, observer, &node); + if (err == OK) { + LOGV("Successfully allocated OMX node '%s'", componentName); + + success = true; + break; + } + } + + if (!success) { + return NULL; + } + sp<OMXCodec> codec = new OMXCodec( - omx, node, quirks, createEncoder, mime, componentName, + omx, node, getComponentQuirks(componentName), + createEncoder, mime, componentName, source); observer->setCodec(codec); @@ -331,7 +400,7 @@ sp<OMXCodec> OMXCodec::Create( size -= length; } - LOGI("AVC profile = %d (%s), level = %d", + LOGV("AVC profile = %d (%s), level = %d", (int)profile, AVCProfileToString(profile), (int)level / 10); if (!strcmp(componentName, "OMX.TI.Video.Decoder") @@ -452,7 +521,7 @@ status_t OMXCodec::setVideoPortFormatType( // CHECK_EQ(format.nIndex, index); #if 1 - CODEC_LOGI("portIndex: %ld, index: %ld, eCompressionFormat=%d eColorFormat=%d", + CODEC_LOGV("portIndex: %ld, index: %ld, eCompressionFormat=%d eColorFormat=%d", portIndex, index, format.eCompressionFormat, format.eColorFormat); #endif @@ -493,9 +562,25 @@ status_t OMXCodec::setVideoPortFormatType( return err; } +static size_t getFrameSize( + OMX_COLOR_FORMATTYPE colorFormat, int32_t width, int32_t height) { + switch (colorFormat) { + case OMX_COLOR_FormatYCbYCr: + case OMX_COLOR_FormatCbYCrY: + return width * height * 2; + + case OMX_COLOR_FormatYUV420SemiPlanar: + return (width * height * 3) / 2; + + default: + CHECK(!"Should not be here. Unsupported color format."); + break; + } +} + void OMXCodec::setVideoInputFormat( const char *mime, OMX_U32 width, OMX_U32 height) { - CODEC_LOGI("setVideoInputFormat width=%ld, height=%ld", width, height); + CODEC_LOGV("setVideoInputFormat width=%ld, height=%ld", width, height); OMX_VIDEO_CODINGTYPE compressionFormat = OMX_VIDEO_CodingUnused; if (!strcasecmp(MEDIA_MIMETYPE_VIDEO_AVC, mime)) { @@ -516,12 +601,13 @@ void OMXCodec::setVideoInputFormat( colorFormat = OMX_COLOR_FormatYUV420SemiPlanar; } - setVideoPortFormatType( + CHECK_EQ(setVideoPortFormatType( kPortIndexInput, OMX_VIDEO_CodingUnused, - colorFormat); + colorFormat), OK); - setVideoPortFormatType( - kPortIndexOutput, compressionFormat, OMX_COLOR_FormatUnused); + CHECK_EQ(setVideoPortFormatType( + kPortIndexOutput, compressionFormat, OMX_COLOR_FormatUnused), + OK); OMX_PARAM_PORTDEFINITIONTYPE def; InitOMXParams(&def); @@ -554,8 +640,8 @@ void OMXCodec::setVideoInputFormat( mNode, OMX_IndexParamPortDefinition, &def, sizeof(def)); CHECK_EQ(err, OK); - def.nBufferSize = (width * height * 2); // (width * height * 3) / 2; - CODEC_LOGI("Setting nBufferSize = %ld", def.nBufferSize); + def.nBufferSize = getFrameSize(colorFormat, width, height); + CODEC_LOGV("Setting nBufferSize = %ld", def.nBufferSize); CHECK_EQ(def.eDomain, OMX_PortDomainVideo); @@ -564,14 +650,108 @@ void OMXCodec::setVideoInputFormat( video_def->eCompressionFormat = OMX_VIDEO_CodingUnused; video_def->eColorFormat = colorFormat; + video_def->xFramerate = 24 << 16; // XXX crucial! + err = mOMX->setParameter( mNode, OMX_IndexParamPortDefinition, &def, sizeof(def)); CHECK_EQ(err, OK); + + switch (compressionFormat) { + case OMX_VIDEO_CodingMPEG4: + { + CHECK_EQ(setupMPEG4EncoderParameters(), OK); + break; + } + + case OMX_VIDEO_CodingH263: + break; + + default: + CHECK(!"Support for this compressionFormat to be implemented."); + break; + } +} + +status_t OMXCodec::setupMPEG4EncoderParameters() { + OMX_VIDEO_PARAM_MPEG4TYPE mpeg4type; + InitOMXParams(&mpeg4type); + mpeg4type.nPortIndex = kPortIndexOutput; + + status_t err = mOMX->getParameter( + mNode, OMX_IndexParamVideoMpeg4, &mpeg4type, sizeof(mpeg4type)); + CHECK_EQ(err, OK); + + mpeg4type.nSliceHeaderSpacing = 0; + mpeg4type.bSVH = OMX_FALSE; + mpeg4type.bGov = OMX_FALSE; + + mpeg4type.nAllowedPictureTypes = + OMX_VIDEO_PictureTypeI | OMX_VIDEO_PictureTypeP; + + mpeg4type.nPFrames = 23; + mpeg4type.nBFrames = 0; + + mpeg4type.nIDCVLCThreshold = 0; + mpeg4type.bACPred = OMX_TRUE; + mpeg4type.nMaxPacketSize = 256; + mpeg4type.nTimeIncRes = 1000; + mpeg4type.nHeaderExtension = 0; + mpeg4type.bReversibleVLC = OMX_FALSE; + + mpeg4type.eProfile = OMX_VIDEO_MPEG4ProfileCore; + mpeg4type.eLevel = OMX_VIDEO_MPEG4Level2; + + err = mOMX->setParameter( + mNode, OMX_IndexParamVideoMpeg4, &mpeg4type, sizeof(mpeg4type)); + CHECK_EQ(err, OK); + + // ---------------- + + OMX_VIDEO_PARAM_BITRATETYPE bitrateType; + InitOMXParams(&bitrateType); + bitrateType.nPortIndex = kPortIndexOutput; + + err = mOMX->getParameter( + mNode, OMX_IndexParamVideoBitrate, + &bitrateType, sizeof(bitrateType)); + CHECK_EQ(err, OK); + + bitrateType.eControlRate = OMX_Video_ControlRateVariable; + bitrateType.nTargetBitrate = 1000000; + + err = mOMX->setParameter( + mNode, OMX_IndexParamVideoBitrate, + &bitrateType, sizeof(bitrateType)); + CHECK_EQ(err, OK); + + // ---------------- + + OMX_VIDEO_PARAM_ERRORCORRECTIONTYPE errorCorrectionType; + InitOMXParams(&errorCorrectionType); + errorCorrectionType.nPortIndex = kPortIndexOutput; + + err = mOMX->getParameter( + mNode, OMX_IndexParamVideoErrorCorrection, + &errorCorrectionType, sizeof(errorCorrectionType)); + CHECK_EQ(err, OK); + + errorCorrectionType.bEnableHEC = OMX_FALSE; + errorCorrectionType.bEnableResync = OMX_TRUE; + errorCorrectionType.nResynchMarkerSpacing = 256; + errorCorrectionType.bEnableDataPartitioning = OMX_FALSE; + errorCorrectionType.bEnableRVLC = OMX_FALSE; + + err = mOMX->setParameter( + mNode, OMX_IndexParamVideoErrorCorrection, + &errorCorrectionType, sizeof(errorCorrectionType)); + CHECK_EQ(err, OK); + + return OK; } void OMXCodec::setVideoOutputFormat( const char *mime, OMX_U32 width, OMX_U32 height) { - CODEC_LOGI("setVideoOutputFormat width=%ld, height=%ld", width, height); + CODEC_LOGV("setVideoOutputFormat width=%ld, height=%ld", width, height); OMX_VIDEO_CODINGTYPE compressionFormat = OMX_VIDEO_CodingUnused; if (!strcasecmp(MEDIA_MIMETYPE_VIDEO_AVC, mime)) { @@ -639,6 +819,7 @@ void OMXCodec::setVideoOutputFormat( video_def->nFrameWidth = width; video_def->nFrameHeight = height; + video_def->eCompressionFormat = compressionFormat; video_def->eColorFormat = OMX_COLOR_FormatUnused; err = mOMX->setParameter( @@ -687,6 +868,7 @@ OMXCodec::OMXCodec( mInitialBufferSubmit(true), mSignalledEOS(false), mNoMoreOutputData(false), + mOutputPortSettingsHaveChanged(false), mSeekTimeUs(-1) { mPortStatus[kPortIndexInput] = ENABLED; mPortStatus[kPortIndexOutput] = ENABLED; @@ -990,12 +1172,8 @@ void OMXCodec::on_message(const omx_message &msg) { buffer->meta_data()->clear(); - buffer->meta_data()->setInt32( - kKeyTimeUnits, - (msg.u.extended_buffer_data.timestamp + 500) / 1000); - - buffer->meta_data()->setInt32( - kKeyTimeScale, 1000); + buffer->meta_data()->setInt64( + kKeyTime, msg.u.extended_buffer_data.timestamp); if (msg.u.extended_buffer_data.flags & OMX_BUFFERFLAG_SYNCFRAME) { buffer->meta_data()->setInt32(kKeyIsSyncFrame, true); @@ -1064,6 +1242,71 @@ void OMXCodec::onEvent(OMX_EVENTTYPE event, OMX_U32 data1, OMX_U32 data2) { } } +// Has the format changed in any way that the client would have to be aware of? +static bool formatHasNotablyChanged( + const sp<MetaData> &from, const sp<MetaData> &to) { + if (from.get() == NULL && to.get() == NULL) { + return false; + } + + if ((from.get() == NULL && to.get() != NULL) + || (from.get() != NULL && to.get() == NULL)) { + return true; + } + + const char *mime_from, *mime_to; + CHECK(from->findCString(kKeyMIMEType, &mime_from)); + CHECK(to->findCString(kKeyMIMEType, &mime_to)); + + if (strcasecmp(mime_from, mime_to)) { + return true; + } + + if (!strcasecmp(mime_from, MEDIA_MIMETYPE_VIDEO_RAW)) { + int32_t colorFormat_from, colorFormat_to; + CHECK(from->findInt32(kKeyColorFormat, &colorFormat_from)); + CHECK(to->findInt32(kKeyColorFormat, &colorFormat_to)); + + if (colorFormat_from != colorFormat_to) { + return true; + } + + int32_t width_from, width_to; + CHECK(from->findInt32(kKeyWidth, &width_from)); + CHECK(to->findInt32(kKeyWidth, &width_to)); + + if (width_from != width_to) { + return true; + } + + int32_t height_from, height_to; + CHECK(from->findInt32(kKeyHeight, &height_from)); + CHECK(to->findInt32(kKeyHeight, &height_to)); + + if (height_from != height_to) { + return true; + } + } else if (!strcasecmp(mime_from, MEDIA_MIMETYPE_AUDIO_RAW)) { + int32_t numChannels_from, numChannels_to; + CHECK(from->findInt32(kKeyChannelCount, &numChannels_from)); + CHECK(to->findInt32(kKeyChannelCount, &numChannels_to)); + + if (numChannels_from != numChannels_to) { + return true; + } + + int32_t sampleRate_from, sampleRate_to; + CHECK(from->findInt32(kKeySampleRate, &sampleRate_from)); + CHECK(to->findInt32(kKeySampleRate, &sampleRate_to)); + + if (sampleRate_from != sampleRate_to) { + return true; + } + } + + return false; +} + void OMXCodec::onCmdComplete(OMX_COMMANDTYPE cmd, OMX_U32 data) { switch (cmd) { case OMX_CommandStateSet: @@ -1086,6 +1329,15 @@ void OMXCodec::onCmdComplete(OMX_COMMANDTYPE cmd, OMX_U32 data) { if (mState == RECONFIGURING) { CHECK_EQ(portIndex, kPortIndexOutput); + sp<MetaData> oldOutputFormat = mOutputFormat; + initOutputFormat(mSource->getFormat()); + + // Don't notify clients if the output port settings change + // wasn't of importance to them, i.e. it may be that just the + // number of buffers has changed and nothing else. + mOutputPortSettingsHaveChanged = + formatHasNotablyChanged(oldOutputFormat, mOutputFormat); + enablePortAsync(portIndex); status_t err = allocateBuffersOnPort(portIndex); @@ -1443,7 +1695,7 @@ void OMXCodec::drainInputBuffer(BufferInfo *info) { } OMX_U32 flags = OMX_BUFFERFLAG_ENDOFFRAME; - OMX_TICKS timestamp = 0; + OMX_TICKS timestampUs = 0; size_t srcLength = 0; if (err != OK) { @@ -1463,15 +1715,11 @@ void OMXCodec::drainInputBuffer(BufferInfo *info) { (const uint8_t *)srcBuffer->data() + srcBuffer->range_offset(), srcLength); - int32_t units, scale; - if (srcBuffer->meta_data()->findInt32(kKeyTimeUnits, &units) - && srcBuffer->meta_data()->findInt32(kKeyTimeScale, &scale)) { - timestamp = ((OMX_TICKS)units * 1000000) / scale; - - CODEC_LOGV("Calling empty_buffer on buffer %p (length %d)", + if (srcBuffer->meta_data()->findInt64(kKeyTime, ×tampUs)) { + CODEC_LOGV("Calling emptyBuffer on buffer %p (length %d)", info->mBuffer, srcLength); - CODEC_LOGV("Calling empty_buffer with timestamp %lld us (%.2f secs)", - timestamp, timestamp / 1E6); + CODEC_LOGV("Calling emptyBuffer with timestamp %lld us (%.2f secs)", + timestampUs, timestampUs / 1E6); } } @@ -1482,7 +1730,7 @@ void OMXCodec::drainInputBuffer(BufferInfo *info) { err = mOMX->emptyBuffer( mNode, info->mBuffer, 0, srcLength, - flags, timestamp); + flags, timestampUs); if (err != OK) { setState(ERROR); @@ -1794,6 +2042,7 @@ status_t OMXCodec::start(MetaData *) { mInitialBufferSubmit = true; mSignalledEOS = false; mNoMoreOutputData = false; + mOutputPortSettingsHaveChanged = false; mSeekTimeUs = -1; mFilledBuffers.clear(); @@ -1864,6 +2113,8 @@ status_t OMXCodec::stop() { } sp<MetaData> OMXCodec::getFormat() { + Mutex::Autolock autoLock(mLock); + return mOutputFormat; } @@ -1941,6 +2192,12 @@ status_t OMXCodec::read( return ERROR_END_OF_STREAM; } + if (mOutputPortSettingsHaveChanged) { + mOutputPortSettingsHaveChanged = false; + + return INFO_FORMAT_CHANGED; + } + size_t index = *mFilledBuffers.begin(); mFilledBuffers.erase(mFilledBuffers.begin()); @@ -2041,8 +2298,6 @@ static const char *colorFormatString(OMX_COLOR_FORMATTYPE type) { size_t numNames = sizeof(kNames) / sizeof(kNames[0]); - static const int OMX_QCOM_COLOR_FormatYVU420SemiPlanar = 0x7FA30C00; - if (type == OMX_QCOM_COLOR_FormatYVU420SemiPlanar) { return "OMX_QCOM_COLOR_FormatYVU420SemiPlanar"; } else if (type < 0 || (size_t)type >= numNames) { @@ -2410,7 +2665,7 @@ void OMXCodec::initOutputFormat(const sp<MetaData> &inputFormat) { CHECK(!"Unknown compression format."); } - if (mQuirks & kOutputDimensionsAre16Aligned) { + if (!strcmp(mComponentName, "OMX.PV.avcdec")) { // This component appears to be lying to me. mOutputFormat->setInt32( kKeyWidth, (video_def->nFrameWidth + 15) & -16); diff --git a/media/libstagefright/SampleTable.cpp b/media/libstagefright/SampleTable.cpp index 8efa7c7..4aec0e9 100644 --- a/media/libstagefright/SampleTable.cpp +++ b/media/libstagefright/SampleTable.cpp @@ -17,11 +17,12 @@ #define LOG_TAG "SampleTable" #include <utils/Log.h> +#include "include/SampleTable.h" + #include <arpa/inet.h> #include <media/stagefright/DataSource.h> #include <media/stagefright/MediaDebug.h> -#include <media/stagefright/SampleTable.h> #include <media/stagefright/Utils.h> namespace android { @@ -54,7 +55,7 @@ SampleTable::~SampleTable() { } status_t SampleTable::setChunkOffsetParams( - uint32_t type, off_t data_offset, off_t data_size) { + uint32_t type, off_t data_offset, size_t data_size) { if (mChunkOffsetOffset >= 0) { return ERROR_MALFORMED; } @@ -69,7 +70,7 @@ status_t SampleTable::setChunkOffsetParams( } uint8_t header[8]; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, header, sizeof(header)) < (ssize_t)sizeof(header)) { return ERROR_IO; } @@ -95,7 +96,7 @@ status_t SampleTable::setChunkOffsetParams( } status_t SampleTable::setSampleToChunkParams( - off_t data_offset, off_t data_size) { + off_t data_offset, size_t data_size) { if (mSampleToChunkOffset >= 0) { return ERROR_MALFORMED; } @@ -107,7 +108,7 @@ status_t SampleTable::setSampleToChunkParams( } uint8_t header[8]; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, header, sizeof(header)) < (ssize_t)sizeof(header)) { return ERROR_IO; } @@ -127,7 +128,7 @@ status_t SampleTable::setSampleToChunkParams( } status_t SampleTable::setSampleSizeParams( - uint32_t type, off_t data_offset, off_t data_size) { + uint32_t type, off_t data_offset, size_t data_size) { if (mSampleSizeOffset >= 0) { return ERROR_MALFORMED; } @@ -141,7 +142,7 @@ status_t SampleTable::setSampleSizeParams( } uint8_t header[12]; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, header, sizeof(header)) < (ssize_t)sizeof(header)) { return ERROR_IO; } @@ -187,13 +188,13 @@ status_t SampleTable::setSampleSizeParams( } status_t SampleTable::setTimeToSampleParams( - off_t data_offset, off_t data_size) { + off_t data_offset, size_t data_size) { if (mTimeToSample != NULL || data_size < 8) { return ERROR_MALFORMED; } uint8_t header[8]; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, header, sizeof(header)) < (ssize_t)sizeof(header)) { return ERROR_IO; } @@ -207,7 +208,7 @@ status_t SampleTable::setTimeToSampleParams( mTimeToSample = new uint32_t[mTimeToSampleCount * 2]; size_t size = sizeof(uint32_t) * mTimeToSampleCount * 2; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset + 8, mTimeToSample, size) < (ssize_t)size) { return ERROR_IO; } @@ -219,7 +220,7 @@ status_t SampleTable::setTimeToSampleParams( return OK; } -status_t SampleTable::setSyncSampleParams(off_t data_offset, off_t data_size) { +status_t SampleTable::setSyncSampleParams(off_t data_offset, size_t data_size) { if (mSyncSampleOffset >= 0 || data_size < 8) { return ERROR_MALFORMED; } @@ -227,7 +228,7 @@ status_t SampleTable::setSyncSampleParams(off_t data_offset, off_t data_size) { mSyncSampleOffset = data_offset; uint8_t header[8]; - if (mDataSource->read_at( + if (mDataSource->readAt( data_offset, header, sizeof(header)) < (ssize_t)sizeof(header)) { return ERROR_IO; } @@ -263,7 +264,7 @@ status_t SampleTable::getChunkOffset(uint32_t chunk_index, off_t *offset) { if (mChunkOffsetType == kChunkOffsetType32) { uint32_t offset32; - if (mDataSource->read_at( + if (mDataSource->readAt( mChunkOffsetOffset + 8 + 4 * chunk_index, &offset32, sizeof(offset32)) < (ssize_t)sizeof(offset32)) { @@ -275,7 +276,7 @@ status_t SampleTable::getChunkOffset(uint32_t chunk_index, off_t *offset) { CHECK_EQ(mChunkOffsetType, kChunkOffsetType64); uint64_t offset64; - if (mDataSource->read_at( + if (mDataSource->readAt( mChunkOffsetOffset + 8 + 8 * chunk_index, &offset64, sizeof(offset64)) < (ssize_t)sizeof(offset64)) { @@ -312,7 +313,7 @@ status_t SampleTable::getChunkForSample( uint32_t index = 0; while (index < mNumSampleToChunkOffsets) { uint8_t buffer[12]; - if (mDataSource->read_at(mSampleToChunkOffset + 8 + index * 12, + if (mDataSource->readAt(mSampleToChunkOffset + 8 + index * 12, buffer, sizeof(buffer)) < (ssize_t)sizeof(buffer)) { return ERROR_IO; } @@ -361,7 +362,7 @@ status_t SampleTable::getSampleSize( switch (mSampleSizeFieldSize) { case 32: { - if (mDataSource->read_at( + if (mDataSource->readAt( mSampleSizeOffset + 12 + 4 * sample_index, sample_size, sizeof(*sample_size)) < (ssize_t)sizeof(*sample_size)) { return ERROR_IO; @@ -374,7 +375,7 @@ status_t SampleTable::getSampleSize( case 16: { uint16_t x; - if (mDataSource->read_at( + if (mDataSource->readAt( mSampleSizeOffset + 12 + 2 * sample_index, &x, sizeof(x)) < (ssize_t)sizeof(x)) { return ERROR_IO; @@ -387,7 +388,7 @@ status_t SampleTable::getSampleSize( case 8: { uint8_t x; - if (mDataSource->read_at( + if (mDataSource->readAt( mSampleSizeOffset + 12 + sample_index, &x, sizeof(x)) < (ssize_t)sizeof(x)) { return ERROR_IO; @@ -402,7 +403,7 @@ status_t SampleTable::getSampleSize( CHECK_EQ(mSampleSizeFieldSize, 4); uint8_t x; - if (mDataSource->read_at( + if (mDataSource->readAt( mSampleSizeOffset + 12 + sample_index / 2, &x, sizeof(x)) < (ssize_t)sizeof(x)) { return ERROR_IO; @@ -553,7 +554,7 @@ status_t SampleTable::findClosestSyncSample( uint32_t right = mNumSyncSamples; while (left < right) { uint32_t mid = (left + right) / 2; - if (mDataSource->read_at( + if (mDataSource->readAt( mSyncSampleOffset + 8 + (mid - 1) * 4, &x, 4) != 4) { return ERROR_IO; } @@ -574,5 +575,52 @@ status_t SampleTable::findClosestSyncSample( return OK; } +status_t SampleTable::findThumbnailSample(uint32_t *sample_index) { + if (mSyncSampleOffset < 0) { + // All samples are sync-samples. + *sample_index = 0; + return OK; + } + + uint32_t bestSampleIndex = 0; + size_t maxSampleSize = 0; + + static const size_t kMaxNumSyncSamplesToScan = 20; + + // Consider the first kMaxNumSyncSamplesToScan sync samples and + // pick the one with the largest (compressed) size as the thumbnail. + + size_t numSamplesToScan = mNumSyncSamples; + if (numSamplesToScan > kMaxNumSyncSamplesToScan) { + numSamplesToScan = kMaxNumSyncSamplesToScan; + } + + for (size_t i = 0; i < numSamplesToScan; ++i) { + uint32_t x; + if (mDataSource->readAt( + mSyncSampleOffset + 8 + i * 4, &x, 4) != 4) { + return ERROR_IO; + } + x = ntohl(x); + --x; + + // Now x is a sample index. + size_t sampleSize; + status_t err = getSampleSize(x, &sampleSize); + if (err != OK) { + return err; + } + + if (i == 0 || sampleSize > maxSampleSize) { + bestSampleIndex = x; + maxSampleSize = sampleSize; + } + } + + *sample_index = bestSampleIndex; + + return OK; +} + } // namespace android diff --git a/media/libstagefright/ShoutcastSource.cpp b/media/libstagefright/ShoutcastSource.cpp index 346b5aa..ec25430 100644 --- a/media/libstagefright/ShoutcastSource.cpp +++ b/media/libstagefright/ShoutcastSource.cpp @@ -14,16 +14,17 @@ * limitations under the License. */ +#include "include/stagefright_string.h" +#include "include/HTTPStream.h" + #include <stdlib.h> -#include <media/stagefright/HTTPStream.h> #include <media/stagefright/MediaBuffer.h> #include <media/stagefright/MediaBufferGroup.h> #include <media/stagefright/MediaDebug.h> #include <media/stagefright/MediaDefs.h> #include <media/stagefright/MetaData.h> #include <media/stagefright/ShoutcastSource.h> -#include <media/stagefright/stagefright_string.h> namespace android { diff --git a/media/libstagefright/TimedEventQueue.cpp b/media/libstagefright/TimedEventQueue.cpp index 3d85f75..dd8005c 100644 --- a/media/libstagefright/TimedEventQueue.cpp +++ b/media/libstagefright/TimedEventQueue.cpp @@ -22,10 +22,11 @@ #define LOG_TAG "TimedEventQueue" #include <utils/Log.h> +#include "include/TimedEventQueue.h" + #include <sys/time.h> #include <media/stagefright/MediaDebug.h> -#include <media/stagefright/TimedEventQueue.h> namespace android { diff --git a/media/libstagefright/WAVExtractor.cpp b/media/libstagefright/WAVExtractor.cpp new file mode 100644 index 0000000..542c764 --- /dev/null +++ b/media/libstagefright/WAVExtractor.cpp @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "WAVExtractor" +#include <utils/Log.h> + +#include "include/WAVExtractor.h" + +#include <media/stagefright/DataSource.h> +#include <media/stagefright/MediaBufferGroup.h> +#include <media/stagefright/MediaDebug.h> +#include <media/stagefright/MediaDefs.h> +#include <media/stagefright/MediaErrors.h> +#include <media/stagefright/MediaSource.h> +#include <media/stagefright/MetaData.h> +#include <utils/String8.h> + +namespace android { + +static uint16_t WAVE_FORMAT_PCM = 1; + +static uint32_t U32_LE_AT(const uint8_t *ptr) { + return ptr[3] << 24 | ptr[2] << 16 | ptr[1] << 8 | ptr[0]; +} + +static uint16_t U16_LE_AT(const uint8_t *ptr) { + return ptr[1] << 8 | ptr[0]; +} + +struct WAVSource : public MediaSource { + WAVSource( + const sp<DataSource> &dataSource, + int32_t sampleRate, int32_t numChannels, + off_t offset, size_t size); + + virtual status_t start(MetaData *params = NULL); + virtual status_t stop(); + virtual sp<MetaData> getFormat(); + + virtual status_t read( + MediaBuffer **buffer, const ReadOptions *options = NULL); + +protected: + virtual ~WAVSource(); + +private: + static const size_t kMaxFrameSize; + + sp<DataSource> mDataSource; + int32_t mSampleRate; + int32_t mNumChannels; + off_t mOffset; + size_t mSize; + bool mStarted; + MediaBufferGroup *mGroup; + off_t mCurrentPos; + + WAVSource(const WAVSource &); + WAVSource &operator=(const WAVSource &); +}; + +WAVExtractor::WAVExtractor(const sp<DataSource> &source) + : mDataSource(source), + mValidFormat(false) { + mInitCheck = init(); +} + +WAVExtractor::~WAVExtractor() { +} + +size_t WAVExtractor::countTracks() { + return mInitCheck == OK ? 1 : 0; +} + +sp<MediaSource> WAVExtractor::getTrack(size_t index) { + if (mInitCheck != OK || index > 0) { + return NULL; + } + + return new WAVSource( + mDataSource, mSampleRate, mNumChannels, mDataOffset, mDataSize); +} + +sp<MetaData> WAVExtractor::getTrackMetaData( + size_t index, uint32_t flags) { + if (mInitCheck != OK || index > 0) { + return NULL; + } + + sp<MetaData> meta = new MetaData; + meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_RAW); + meta->setInt32(kKeyChannelCount, mNumChannels); + meta->setInt32(kKeySampleRate, mSampleRate); + + int64_t durationUs = + 1000000LL * (mDataSize / (mNumChannels * 2)) / mSampleRate; + + meta->setInt64(kKeyDuration, durationUs); + + return meta; +} + +status_t WAVExtractor::init() { + uint8_t header[12]; + if (mDataSource->readAt( + 0, header, sizeof(header)) < (ssize_t)sizeof(header)) { + return NO_INIT; + } + + if (memcmp(header, "RIFF", 4) || memcmp(&header[8], "WAVE", 4)) { + return NO_INIT; + } + + size_t totalSize = U32_LE_AT(&header[4]); + + off_t offset = 12; + size_t remainingSize = totalSize; + while (remainingSize >= 8) { + uint8_t chunkHeader[8]; + if (mDataSource->readAt(offset, chunkHeader, 8) < 8) { + return NO_INIT; + } + + remainingSize -= 8; + offset += 8; + + uint32_t chunkSize = U32_LE_AT(&chunkHeader[4]); + + if (chunkSize > remainingSize) { + return NO_INIT; + } + + if (!memcmp(chunkHeader, "fmt ", 4)) { + if (chunkSize < 16) { + return NO_INIT; + } + + uint8_t formatSpec[16]; + if (mDataSource->readAt(offset, formatSpec, 16) < 16) { + return NO_INIT; + } + + uint16_t format = U16_LE_AT(formatSpec); + if (format != WAVE_FORMAT_PCM) { + return ERROR_UNSUPPORTED; + } + + mNumChannels = U16_LE_AT(&formatSpec[2]); + if (mNumChannels != 1 && mNumChannels != 2) { + return ERROR_UNSUPPORTED; + } + + mSampleRate = U32_LE_AT(&formatSpec[4]); + + if (U16_LE_AT(&formatSpec[14]) != 16) { + return ERROR_UNSUPPORTED; + } + + mValidFormat = true; + } else if (!memcmp(chunkHeader, "data", 4)) { + if (mValidFormat) { + mDataOffset = offset; + mDataSize = chunkSize; + + return OK; + } + } + + offset += chunkSize; + } + + return NO_INIT; +} + +const size_t WAVSource::kMaxFrameSize = 32768; + +WAVSource::WAVSource( + const sp<DataSource> &dataSource, + int32_t sampleRate, int32_t numChannels, + off_t offset, size_t size) + : mDataSource(dataSource), + mSampleRate(sampleRate), + mNumChannels(numChannels), + mOffset(offset), + mSize(size), + mStarted(false), + mGroup(NULL) { +} + +WAVSource::~WAVSource() { + if (mStarted) { + stop(); + } +} + +status_t WAVSource::start(MetaData *params) { + LOGV("WAVSource::start"); + + CHECK(!mStarted); + + mGroup = new MediaBufferGroup; + mGroup->add_buffer(new MediaBuffer(kMaxFrameSize)); + + mCurrentPos = mOffset; + + mStarted = true; + + return OK; +} + +status_t WAVSource::stop() { + LOGV("WAVSource::stop"); + + CHECK(mStarted); + + delete mGroup; + mGroup = NULL; + + mStarted = false; + + return OK; +} + +sp<MetaData> WAVSource::getFormat() { + LOGV("WAVSource::getFormat"); + + sp<MetaData> meta = new MetaData; + meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_RAW); + meta->setInt32(kKeyChannelCount, mNumChannels); + meta->setInt32(kKeySampleRate, mSampleRate); + + int64_t durationUs = + 1000000LL * (mSize / (mNumChannels * 2)) / mSampleRate; + + meta->setInt64(kKeyDuration, durationUs); + + return meta; +} + +status_t WAVSource::read( + MediaBuffer **out, const ReadOptions *options) { + *out = NULL; + + int64_t seekTimeUs; + if (options != NULL && options->getSeekTo(&seekTimeUs)) { + int64_t pos = (seekTimeUs * mSampleRate) / 1000000 * mNumChannels * 2; + if (pos > mSize) { + pos = mSize; + } + mCurrentPos = pos + mOffset; + } + + MediaBuffer *buffer; + status_t err = mGroup->acquire_buffer(&buffer); + if (err != OK) { + return err; + } + + ssize_t n = mDataSource->readAt( + mCurrentPos, buffer->data(), kMaxFrameSize); + + if (n <= 0) { + buffer->release(); + buffer = NULL; + + return ERROR_END_OF_STREAM; + } + + mCurrentPos += n; + + buffer->set_range(0, n); + buffer->meta_data()->setInt64( + kKeyTime, + 1000000LL * (mCurrentPos - mOffset) + / (mNumChannels * 2) / mSampleRate); + + + *out = buffer; + + return OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +bool SniffWAV( + const sp<DataSource> &source, String8 *mimeType, float *confidence) { + char header[12]; + if (source->readAt(0, header, sizeof(header)) < (ssize_t)sizeof(header)) { + return false; + } + + if (memcmp(header, "RIFF", 4) || memcmp(&header[8], "WAVE", 4)) { + return false; + } + + *mimeType = MEDIA_MIMETYPE_CONTAINER_WAV; + *confidence = 0.3f; + + return true; +} + +} // namespace android + diff --git a/include/media/stagefright/AMRExtractor.h b/media/libstagefright/include/AMRExtractor.h index c8710d3..debf006 100644 --- a/include/media/stagefright/AMRExtractor.h +++ b/media/libstagefright/include/AMRExtractor.h @@ -30,7 +30,7 @@ public: virtual size_t countTracks(); virtual sp<MediaSource> getTrack(size_t index); - virtual sp<MetaData> getTrackMetaData(size_t index); + virtual sp<MetaData> getTrackMetaData(size_t index, uint32_t flags); static sp<MetaData> makeAMRFormat(bool isWide); diff --git a/include/media/stagefright/ESDS.h b/media/libstagefright/include/ESDS.h index 01bcd18..01bcd18 100644 --- a/include/media/stagefright/ESDS.h +++ b/media/libstagefright/include/ESDS.h diff --git a/include/media/stagefright/HTTPStream.h b/media/libstagefright/include/HTTPStream.h index 72e796c..43ef614 100644 --- a/include/media/stagefright/HTTPStream.h +++ b/media/libstagefright/include/HTTPStream.h @@ -18,10 +18,11 @@ #define HTTP_STREAM_H_ +#include "stagefright_string.h" + #include <sys/types.h> #include <media/stagefright/MediaErrors.h> -#include <media/stagefright/stagefright_string.h> #include <utils/KeyedVector.h> namespace android { diff --git a/include/media/stagefright/MP3Extractor.h b/media/libstagefright/include/MP3Extractor.h index 11ba01d..074973b 100644 --- a/include/media/stagefright/MP3Extractor.h +++ b/media/libstagefright/include/MP3Extractor.h @@ -32,7 +32,7 @@ public: virtual size_t countTracks(); virtual sp<MediaSource> getTrack(size_t index); - virtual sp<MetaData> getTrackMetaData(size_t index); + virtual sp<MetaData> getTrackMetaData(size_t index, uint32_t flags); protected: virtual ~MP3Extractor(); diff --git a/include/media/stagefright/MPEG4Extractor.h b/media/libstagefright/include/MPEG4Extractor.h index 932e30f..ce4736d 100644 --- a/include/media/stagefright/MPEG4Extractor.h +++ b/media/libstagefright/include/MPEG4Extractor.h @@ -33,7 +33,7 @@ public: size_t countTracks(); sp<MediaSource> getTrack(size_t index); - sp<MetaData> getTrackMetaData(size_t index); + sp<MetaData> getTrackMetaData(size_t index, uint32_t flags); protected: virtual ~MPEG4Extractor(); @@ -44,6 +44,7 @@ private: sp<MetaData> meta; uint32_t timescale; sp<SampleTable> sampleTable; + bool includes_expensive_metadata; }; sp<DataSource> mDataSource; diff --git a/media/libstagefright/include/OMX.h b/media/libstagefright/include/OMX.h index d0bd61e..a4b62b2 100644 --- a/media/libstagefright/include/OMX.h +++ b/media/libstagefright/include/OMX.h @@ -99,7 +99,7 @@ public: OMX_IN OMX_U32 nData1, OMX_IN OMX_U32 nData2, OMX_IN OMX_PTR pEventData); - + OMX_ERRORTYPE OnEmptyBufferDone( node_id node, OMX_IN OMX_BUFFERHEADERTYPE *pBuffer); diff --git a/include/media/stagefright/QComHardwareRenderer.h b/media/libstagefright/include/QComHardwareRenderer.h index 8292dd5..8292dd5 100644 --- a/include/media/stagefright/QComHardwareRenderer.h +++ b/media/libstagefright/include/QComHardwareRenderer.h diff --git a/include/media/stagefright/SampleTable.h b/media/libstagefright/include/SampleTable.h index 808d142..ead3431 100644 --- a/include/media/stagefright/SampleTable.h +++ b/media/libstagefright/include/SampleTable.h @@ -35,17 +35,17 @@ public: // type can be 'stco' or 'co64'. status_t setChunkOffsetParams( - uint32_t type, off_t data_offset, off_t data_size); + uint32_t type, off_t data_offset, size_t data_size); - status_t setSampleToChunkParams(off_t data_offset, off_t data_size); + status_t setSampleToChunkParams(off_t data_offset, size_t data_size); // type can be 'stsz' or 'stz2'. status_t setSampleSizeParams( - uint32_t type, off_t data_offset, off_t data_size); + uint32_t type, off_t data_offset, size_t data_size); - status_t setTimeToSampleParams(off_t data_offset, off_t data_size); + status_t setTimeToSampleParams(off_t data_offset, size_t data_size); - status_t setSyncSampleParams(off_t data_offset, off_t data_size); + status_t setSyncSampleParams(off_t data_offset, size_t data_size); //////////////////////////////////////////////////////////////////////////// @@ -75,6 +75,8 @@ public: status_t findClosestSyncSample( uint32_t start_sample_index, uint32_t *sample_index); + status_t findThumbnailSample(uint32_t *sample_index); + protected: ~SampleTable(); diff --git a/include/media/stagefright/SoftwareRenderer.h b/media/libstagefright/include/SoftwareRenderer.h index 1545493..9eed089 100644 --- a/include/media/stagefright/SoftwareRenderer.h +++ b/media/libstagefright/include/SoftwareRenderer.h @@ -18,7 +18,7 @@ #define SOFTWARE_RENDERER_H_ -#include <OMX_Video.h> +#include <media/stagefright/ColorConverter.h> #include <media/stagefright/VideoRenderer.h> #include <utils/RefBase.h> @@ -41,13 +41,8 @@ public: const void *data, size_t size, void *platformPrivate); private: - uint8_t *initClip(); - - void renderCbYCrY(const void *data, size_t size); - void renderYUV420Planar(const void *data, size_t size); - void renderQCOMYUV420SemiPlanar(const void *data, size_t size); - OMX_COLOR_FORMATTYPE mColorFormat; + ColorConverter mConverter; sp<ISurface> mISurface; size_t mDisplayWidth, mDisplayHeight; size_t mDecodedWidth, mDecodedHeight; @@ -55,8 +50,6 @@ private: sp<MemoryHeapBase> mMemoryHeap; int mIndex; - uint8_t *mClip; - SoftwareRenderer(const SoftwareRenderer &); SoftwareRenderer &operator=(const SoftwareRenderer &); }; diff --git a/include/media/stagefright/TIHardwareRenderer.h b/media/libstagefright/include/TIHardwareRenderer.h index ef42648..ef42648 100644 --- a/include/media/stagefright/TIHardwareRenderer.h +++ b/media/libstagefright/include/TIHardwareRenderer.h diff --git a/include/media/stagefright/TimedEventQueue.h b/media/libstagefright/include/TimedEventQueue.h index a264421..a264421 100644 --- a/include/media/stagefright/TimedEventQueue.h +++ b/media/libstagefright/include/TimedEventQueue.h diff --git a/media/libstagefright/include/WAVExtractor.h b/media/libstagefright/include/WAVExtractor.h new file mode 100644 index 0000000..10b9700 --- /dev/null +++ b/media/libstagefright/include/WAVExtractor.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef WAV_EXTRACTOR_H_ + +#define WAV_EXTRACTOR_H_ + +#include <media/stagefright/MediaExtractor.h> + +namespace android { + +class DataSource; +class String8; + +class WAVExtractor : public MediaExtractor { +public: + // Extractor assumes ownership of "source". + WAVExtractor(const sp<DataSource> &source); + + virtual size_t countTracks(); + virtual sp<MediaSource> getTrack(size_t index); + virtual sp<MetaData> getTrackMetaData(size_t index, uint32_t flags); + +protected: + virtual ~WAVExtractor(); + +private: + sp<DataSource> mDataSource; + status_t mInitCheck; + bool mValidFormat; + uint16_t mNumChannels; + uint32_t mSampleRate; + off_t mDataOffset; + size_t mDataSize; + + status_t init(); + + WAVExtractor(const WAVExtractor &); + WAVExtractor &operator=(const WAVExtractor &); +}; + +bool SniffWAV( + const sp<DataSource> &source, String8 *mimeType, float *confidence); + +} // namespace android + +#endif // WAV_EXTRACTOR_H_ + diff --git a/include/media/stagefright/stagefright_string.h b/media/libstagefright/include/stagefright_string.h index 1ed4c86..5dc7116 100644 --- a/include/media/stagefright/stagefright_string.h +++ b/media/libstagefright/include/stagefright_string.h @@ -14,9 +14,9 @@ * limitations under the License. */ -#ifndef STAGEFRIGHT_STRING_H_ +#ifndef STRING_H_ -#define STAGEFRIGHT_STRING_H_ +#define STRING_H_ #include <utils/String8.h> @@ -51,4 +51,4 @@ private: } // namespace android -#endif // STAGEFRIGHT_STRING_H_ +#endif // STRING_H_ diff --git a/media/libstagefright/omx/Android.mk b/media/libstagefright/omx/Android.mk index 20fb4f3..edbc04e 100644 --- a/media/libstagefright/omx/Android.mk +++ b/media/libstagefright/omx/Android.mk @@ -10,6 +10,7 @@ LOCAL_C_INCLUDES += $(TOP)/hardware/ti/omap3/liboverlay LOCAL_C_INCLUDES += $(JNI_H_INCLUDE) LOCAL_SRC_FILES:= \ + ColorConverter.cpp \ OMX.cpp \ OMXNodeInstance.cpp \ QComHardwareRenderer.cpp \ diff --git a/media/libstagefright/omx/ColorConverter.cpp b/media/libstagefright/omx/ColorConverter.cpp new file mode 100644 index 0000000..e74782f --- /dev/null +++ b/media/libstagefright/omx/ColorConverter.cpp @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <media/stagefright/ColorConverter.h> +#include <media/stagefright/MediaDebug.h> + +namespace android { + +static const int OMX_QCOM_COLOR_FormatYVU420SemiPlanar = 0x7FA30C00; + +ColorConverter::ColorConverter( + OMX_COLOR_FORMATTYPE from, OMX_COLOR_FORMATTYPE to) + : mSrcFormat(from), + mDstFormat(to), + mClip(NULL) { +} + +ColorConverter::~ColorConverter() { + delete[] mClip; + mClip = NULL; +} + +bool ColorConverter::isValid() const { + if (mDstFormat != OMX_COLOR_Format16bitRGB565) { + return false; + } + + switch (mSrcFormat) { + case OMX_COLOR_FormatYUV420Planar: + case OMX_COLOR_FormatCbYCrY: + case OMX_QCOM_COLOR_FormatYVU420SemiPlanar: + return true; + + default: + return false; + } +} + +void ColorConverter::convert( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip) { + CHECK_EQ(mDstFormat, OMX_COLOR_Format16bitRGB565); + + switch (mSrcFormat) { + case OMX_COLOR_FormatYUV420Planar: + convertYUV420Planar( + width, height, srcBits, srcSkip, dstBits, dstSkip); + break; + + case OMX_COLOR_FormatCbYCrY: + convertCbYCrY( + width, height, srcBits, srcSkip, dstBits, dstSkip); + break; + + case OMX_QCOM_COLOR_FormatYVU420SemiPlanar: + convertQCOMYUV420SemiPlanar( + width, height, srcBits, srcSkip, dstBits, dstSkip); + break; + + default: + { + CHECK(!"Should not be here. Unknown color conversion."); + break; + } + } +} + +void ColorConverter::convertCbYCrY( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip) { + CHECK_EQ(srcSkip, 0); // Doesn't really make sense for YUV formats. + CHECK(dstSkip >= width * 2); + CHECK((dstSkip & 3) == 0); + + uint8_t *kAdjustedClip = initClip(); + + uint32_t *dst_ptr = (uint32_t *)dstBits; + + const uint8_t *src = (const uint8_t *)srcBits; + + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; x += 2) { + signed y1 = (signed)src[2 * x + 1] - 16; + signed y2 = (signed)src[2 * x + 3] - 16; + signed u = (signed)src[2 * x] - 128; + signed v = (signed)src[2 * x + 2] - 128; + + signed u_b = u * 517; + signed u_g = -u * 100; + signed v_g = -v * 208; + signed v_r = v * 409; + + signed tmp1 = y1 * 298; + signed b1 = (tmp1 + u_b) / 256; + signed g1 = (tmp1 + v_g + u_g) / 256; + signed r1 = (tmp1 + v_r) / 256; + + signed tmp2 = y2 * 298; + signed b2 = (tmp2 + u_b) / 256; + signed g2 = (tmp2 + v_g + u_g) / 256; + signed r2 = (tmp2 + v_r) / 256; + + uint32_t rgb1 = + ((kAdjustedClip[r1] >> 3) << 11) + | ((kAdjustedClip[g1] >> 2) << 5) + | (kAdjustedClip[b1] >> 3); + + uint32_t rgb2 = + ((kAdjustedClip[r2] >> 3) << 11) + | ((kAdjustedClip[g2] >> 2) << 5) + | (kAdjustedClip[b2] >> 3); + + dst_ptr[x / 2] = (rgb2 << 16) | rgb1; + } + + src += width * 2; + dst_ptr += dstSkip / 4; + } +} + +void ColorConverter::convertYUV420Planar( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip) { + CHECK_EQ(srcSkip, 0); // Doesn't really make sense for YUV formats. + CHECK(dstSkip >= width * 2); + CHECK((dstSkip & 3) == 0); + + uint8_t *kAdjustedClip = initClip(); + + uint32_t *dst_ptr = (uint32_t *)dstBits; + const uint8_t *src_y = (const uint8_t *)srcBits; + + const uint8_t *src_u = + (const uint8_t *)src_y + width * height; + + const uint8_t *src_v = + (const uint8_t *)src_u + (width / 2) * (height / 2); + + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; x += 2) { + // B = 1.164 * (Y - 16) + 2.018 * (U - 128) + // G = 1.164 * (Y - 16) - 0.813 * (V - 128) - 0.391 * (U - 128) + // R = 1.164 * (Y - 16) + 1.596 * (V - 128) + + // B = 298/256 * (Y - 16) + 517/256 * (U - 128) + // G = .................. - 208/256 * (V - 128) - 100/256 * (U - 128) + // R = .................. + 409/256 * (V - 128) + + // min_B = (298 * (- 16) + 517 * (- 128)) / 256 = -277 + // min_G = (298 * (- 16) - 208 * (255 - 128) - 100 * (255 - 128)) / 256 = -172 + // min_R = (298 * (- 16) + 409 * (- 128)) / 256 = -223 + + // max_B = (298 * (255 - 16) + 517 * (255 - 128)) / 256 = 534 + // max_G = (298 * (255 - 16) - 208 * (- 128) - 100 * (- 128)) / 256 = 432 + // max_R = (298 * (255 - 16) + 409 * (255 - 128)) / 256 = 481 + + // clip range -278 .. 535 + + signed y1 = (signed)src_y[x] - 16; + signed y2 = (signed)src_y[x + 1] - 16; + + signed u = (signed)src_u[x / 2] - 128; + signed v = (signed)src_v[x / 2] - 128; + + signed u_b = u * 517; + signed u_g = -u * 100; + signed v_g = -v * 208; + signed v_r = v * 409; + + signed tmp1 = y1 * 298; + signed b1 = (tmp1 + u_b) / 256; + signed g1 = (tmp1 + v_g + u_g) / 256; + signed r1 = (tmp1 + v_r) / 256; + + signed tmp2 = y2 * 298; + signed b2 = (tmp2 + u_b) / 256; + signed g2 = (tmp2 + v_g + u_g) / 256; + signed r2 = (tmp2 + v_r) / 256; + + uint32_t rgb1 = + ((kAdjustedClip[r1] >> 3) << 11) + | ((kAdjustedClip[g1] >> 2) << 5) + | (kAdjustedClip[b1] >> 3); + + uint32_t rgb2 = + ((kAdjustedClip[r2] >> 3) << 11) + | ((kAdjustedClip[g2] >> 2) << 5) + | (kAdjustedClip[b2] >> 3); + + dst_ptr[x / 2] = (rgb2 << 16) | rgb1; + } + + src_y += width; + + if (y & 1) { + src_u += width / 2; + src_v += width / 2; + } + + dst_ptr += dstSkip / 4; + } +} + +void ColorConverter::convertQCOMYUV420SemiPlanar( + size_t width, size_t height, + const void *srcBits, size_t srcSkip, + void *dstBits, size_t dstSkip) { + CHECK_EQ(srcSkip, 0); // Doesn't really make sense for YUV formats. + CHECK(dstSkip >= width * 2); + CHECK((dstSkip & 3) == 0); + + uint8_t *kAdjustedClip = initClip(); + + uint32_t *dst_ptr = (uint32_t *)dstBits; + const uint8_t *src_y = (const uint8_t *)srcBits; + + const uint8_t *src_u = + (const uint8_t *)src_y + width * height; + + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; x += 2) { + signed y1 = (signed)src_y[x] - 16; + signed y2 = (signed)src_y[x + 1] - 16; + + signed u = (signed)src_u[x & ~1] - 128; + signed v = (signed)src_u[(x & ~1) + 1] - 128; + + signed u_b = u * 517; + signed u_g = -u * 100; + signed v_g = -v * 208; + signed v_r = v * 409; + + signed tmp1 = y1 * 298; + signed b1 = (tmp1 + u_b) / 256; + signed g1 = (tmp1 + v_g + u_g) / 256; + signed r1 = (tmp1 + v_r) / 256; + + signed tmp2 = y2 * 298; + signed b2 = (tmp2 + u_b) / 256; + signed g2 = (tmp2 + v_g + u_g) / 256; + signed r2 = (tmp2 + v_r) / 256; + + uint32_t rgb1 = + ((kAdjustedClip[b1] >> 3) << 11) + | ((kAdjustedClip[g1] >> 2) << 5) + | (kAdjustedClip[r1] >> 3); + + uint32_t rgb2 = + ((kAdjustedClip[b2] >> 3) << 11) + | ((kAdjustedClip[g2] >> 2) << 5) + | (kAdjustedClip[r2] >> 3); + + dst_ptr[x / 2] = (rgb2 << 16) | rgb1; + } + + src_y += width; + + if (y & 1) { + src_u += width; + } + + dst_ptr += dstSkip / 4; + } +} + +uint8_t *ColorConverter::initClip() { + static const signed kClipMin = -278; + static const signed kClipMax = 535; + + if (mClip == NULL) { + mClip = new uint8_t[kClipMax - kClipMin + 1]; + + for (signed i = kClipMin; i <= kClipMax; ++i) { + mClip[i - kClipMin] = (i < 0) ? 0 : (i > 255) ? 255 : (uint8_t)i; + } + } + + return &mClip[-kClipMin]; +} + +} // namespace android diff --git a/media/libstagefright/omx/OMX.cpp b/media/libstagefright/omx/OMX.cpp index 9ac0d44..e361018 100644 --- a/media/libstagefright/omx/OMX.cpp +++ b/media/libstagefright/omx/OMX.cpp @@ -24,12 +24,12 @@ #include "pv_omxcore.h" #include "../include/OMXNodeInstance.h" +#include "../include/QComHardwareRenderer.h" +#include "../include/SoftwareRenderer.h" +#include "../include/TIHardwareRenderer.h" #include <binder/IMemory.h> #include <media/stagefright/MediaDebug.h> -#include <media/stagefright/QComHardwareRenderer.h> -#include <media/stagefright/SoftwareRenderer.h> -#include <media/stagefright/TIHardwareRenderer.h> #include <media/stagefright/VideoRenderer.h> #include <OMX_Component.h> @@ -235,7 +235,7 @@ status_t OMX::allocateNode( &OMXNodeInstance::kCallbacks); if (err != OMX_ErrorNone) { - LOGE("FAILED to allocate omx component '%s'", name); + LOGV("FAILED to allocate omx component '%s'", name); instance->onGetHandleFailed(); diff --git a/media/libstagefright/omx/QComHardwareRenderer.cpp b/media/libstagefright/omx/QComHardwareRenderer.cpp index c65d1f3..837b6a4 100644 --- a/media/libstagefright/omx/QComHardwareRenderer.cpp +++ b/media/libstagefright/omx/QComHardwareRenderer.cpp @@ -14,10 +14,11 @@ * limitations under the License. */ +#include "../include/QComHardwareRenderer.h" + #include <binder/MemoryHeapBase.h> #include <binder/MemoryHeapPmem.h> #include <media/stagefright/MediaDebug.h> -#include <media/stagefright/QComHardwareRenderer.h> #include <ui/ISurface.h> namespace android { diff --git a/media/libstagefright/omx/SoftwareRenderer.cpp b/media/libstagefright/omx/SoftwareRenderer.cpp index 4ed6869..ef6ede0 100644 --- a/media/libstagefright/omx/SoftwareRenderer.cpp +++ b/media/libstagefright/omx/SoftwareRenderer.cpp @@ -17,21 +17,21 @@ #define LOG_TAG "SoftwareRenderer" #include <utils/Log.h> +#include "../include/SoftwareRenderer.h" + #include <binder/MemoryHeapBase.h> #include <media/stagefright/MediaDebug.h> -#include <media/stagefright/SoftwareRenderer.h> #include <ui/ISurface.h> namespace android { -#define QCOM_YUV 0 - SoftwareRenderer::SoftwareRenderer( OMX_COLOR_FORMATTYPE colorFormat, const sp<ISurface> &surface, size_t displayWidth, size_t displayHeight, size_t decodedWidth, size_t decodedHeight) : mColorFormat(colorFormat), + mConverter(colorFormat, OMX_COLOR_Format16bitRGB565), mISurface(surface), mDisplayWidth(displayWidth), mDisplayHeight(displayHeight), @@ -39,12 +39,12 @@ SoftwareRenderer::SoftwareRenderer( mDecodedHeight(decodedHeight), mFrameSize(mDecodedWidth * mDecodedHeight * 2), // RGB565 mMemoryHeap(new MemoryHeapBase(2 * mFrameSize)), - mIndex(0), - mClip(NULL) { + mIndex(0) { CHECK(mISurface.get() != NULL); CHECK(mDecodedWidth > 0); CHECK(mDecodedHeight > 0); CHECK(mMemoryHeap->heapID() >= 0); + CHECK(mConverter.isValid()); ISurface::BufferHeap bufferHeap( mDisplayWidth, mDisplayHeight, @@ -58,278 +58,19 @@ SoftwareRenderer::SoftwareRenderer( SoftwareRenderer::~SoftwareRenderer() { mISurface->unregisterBuffers(); - - delete[] mClip; - mClip = NULL; } void SoftwareRenderer::render( const void *data, size_t size, void *platformPrivate) { - static const int OMX_QCOM_COLOR_FormatYVU420SemiPlanar = 0x7FA30C00; - - switch (mColorFormat) { - case OMX_COLOR_FormatYUV420Planar: - return renderYUV420Planar(data, size); - - case OMX_COLOR_FormatCbYCrY: - return renderCbYCrY(data, size); - - case OMX_QCOM_COLOR_FormatYVU420SemiPlanar: - return renderQCOMYUV420SemiPlanar(data, size); - - default: - { - LOGW("Cannot render color format %ld", mColorFormat); - break; - } - } -} - -void SoftwareRenderer::renderYUV420Planar( - const void *data, size_t size) { - if (size != (mDecodedHeight * mDecodedWidth * 3) / 2) { - LOGE("size is %d, expected %d", - size, (mDecodedHeight * mDecodedWidth * 3) / 2); - } - CHECK(size >= (mDecodedWidth * mDecodedHeight * 3) / 2); - - uint8_t *kAdjustedClip = initClip(); - size_t offset = mIndex * mFrameSize; - void *dst = (uint8_t *)mMemoryHeap->getBase() + offset; - uint32_t *dst_ptr = (uint32_t *)dst; - - const uint8_t *src_y = (const uint8_t *)data; - - const uint8_t *src_u = - (const uint8_t *)src_y + mDecodedWidth * mDecodedHeight; - -#if !QCOM_YUV - const uint8_t *src_v = - (const uint8_t *)src_u + (mDecodedWidth / 2) * (mDecodedHeight / 2); -#endif - - for (size_t y = 0; y < mDecodedHeight; ++y) { - for (size_t x = 0; x < mDecodedWidth; x += 2) { - // B = 1.164 * (Y - 16) + 2.018 * (U - 128) - // G = 1.164 * (Y - 16) - 0.813 * (V - 128) - 0.391 * (U - 128) - // R = 1.164 * (Y - 16) + 1.596 * (V - 128) - - // B = 298/256 * (Y - 16) + 517/256 * (U - 128) - // G = .................. - 208/256 * (V - 128) - 100/256 * (U - 128) - // R = .................. + 409/256 * (V - 128) - - // min_B = (298 * (- 16) + 517 * (- 128)) / 256 = -277 - // min_G = (298 * (- 16) - 208 * (255 - 128) - 100 * (255 - 128)) / 256 = -172 - // min_R = (298 * (- 16) + 409 * (- 128)) / 256 = -223 - - // max_B = (298 * (255 - 16) + 517 * (255 - 128)) / 256 = 534 - // max_G = (298 * (255 - 16) - 208 * (- 128) - 100 * (- 128)) / 256 = 432 - // max_R = (298 * (255 - 16) + 409 * (255 - 128)) / 256 = 481 - - // clip range -278 .. 535 - - signed y1 = (signed)src_y[x] - 16; - signed y2 = (signed)src_y[x + 1] - 16; - -#if QCOM_YUV - signed u = (signed)src_u[x & ~1] - 128; - signed v = (signed)src_u[(x & ~1) + 1] - 128; -#else - signed u = (signed)src_u[x / 2] - 128; - signed v = (signed)src_v[x / 2] - 128; -#endif - - signed u_b = u * 517; - signed u_g = -u * 100; - signed v_g = -v * 208; - signed v_r = v * 409; - - signed tmp1 = y1 * 298; - signed b1 = (tmp1 + u_b) / 256; - signed g1 = (tmp1 + v_g + u_g) / 256; - signed r1 = (tmp1 + v_r) / 256; - - signed tmp2 = y2 * 298; - signed b2 = (tmp2 + u_b) / 256; - signed g2 = (tmp2 + v_g + u_g) / 256; - signed r2 = (tmp2 + v_r) / 256; - - uint32_t rgb1 = - ((kAdjustedClip[r1] >> 3) << 11) - | ((kAdjustedClip[g1] >> 2) << 5) - | (kAdjustedClip[b1] >> 3); - - uint32_t rgb2 = - ((kAdjustedClip[r2] >> 3) << 11) - | ((kAdjustedClip[g2] >> 2) << 5) - | (kAdjustedClip[b2] >> 3); - - dst_ptr[x / 2] = (rgb2 << 16) | rgb1; - } - - src_y += mDecodedWidth; - - if (y & 1) { -#if QCOM_YUV - src_u += mDecodedWidth; -#else - src_u += mDecodedWidth / 2; - src_v += mDecodedWidth / 2; -#endif - } - - dst_ptr += mDecodedWidth / 2; - } - - mISurface->postBuffer(offset); - mIndex = 1 - mIndex; -} - -void SoftwareRenderer::renderCbYCrY( - const void *data, size_t size) { - if (size != (mDecodedHeight * mDecodedWidth * 2)) { - LOGE("size is %d, expected %d", - size, (mDecodedHeight * mDecodedWidth * 2)); - } - CHECK(size >= (mDecodedWidth * mDecodedHeight * 2)); - - uint8_t *kAdjustedClip = initClip(); - - size_t offset = mIndex * mFrameSize; - void *dst = (uint8_t *)mMemoryHeap->getBase() + offset; - uint32_t *dst_ptr = (uint32_t *)dst; - - const uint8_t *src = (const uint8_t *)data; - - for (size_t y = 0; y < mDecodedHeight; ++y) { - for (size_t x = 0; x < mDecodedWidth; x += 2) { - signed y1 = (signed)src[2 * x + 1] - 16; - signed y2 = (signed)src[2 * x + 3] - 16; - signed u = (signed)src[2 * x] - 128; - signed v = (signed)src[2 * x + 2] - 128; - - signed u_b = u * 517; - signed u_g = -u * 100; - signed v_g = -v * 208; - signed v_r = v * 409; - - signed tmp1 = y1 * 298; - signed b1 = (tmp1 + u_b) / 256; - signed g1 = (tmp1 + v_g + u_g) / 256; - signed r1 = (tmp1 + v_r) / 256; - - signed tmp2 = y2 * 298; - signed b2 = (tmp2 + u_b) / 256; - signed g2 = (tmp2 + v_g + u_g) / 256; - signed r2 = (tmp2 + v_r) / 256; - - uint32_t rgb1 = - ((kAdjustedClip[r1] >> 3) << 11) - | ((kAdjustedClip[g1] >> 2) << 5) - | (kAdjustedClip[b1] >> 3); - - uint32_t rgb2 = - ((kAdjustedClip[r2] >> 3) << 11) - | ((kAdjustedClip[g2] >> 2) << 5) - | (kAdjustedClip[b2] >> 3); - - dst_ptr[x / 2] = (rgb2 << 16) | rgb1; - } - - src += mDecodedWidth * 2; - dst_ptr += mDecodedWidth / 2; - } - - mISurface->postBuffer(offset); - mIndex = 1 - mIndex; -} - -void SoftwareRenderer::renderQCOMYUV420SemiPlanar( - const void *data, size_t size) { - if (size != (mDecodedHeight * mDecodedWidth * 3) / 2) { - LOGE("size is %d, expected %d", - size, (mDecodedHeight * mDecodedWidth * 3) / 2); - } - CHECK(size >= (mDecodedWidth * mDecodedHeight * 3) / 2); - - uint8_t *kAdjustedClip = initClip(); - - size_t offset = mIndex * mFrameSize; - - void *dst = (uint8_t *)mMemoryHeap->getBase() + offset; - - uint32_t *dst_ptr = (uint32_t *)dst; - - const uint8_t *src_y = (const uint8_t *)data; - - const uint8_t *src_u = - (const uint8_t *)src_y + mDecodedWidth * mDecodedHeight; - - for (size_t y = 0; y < mDecodedHeight; ++y) { - for (size_t x = 0; x < mDecodedWidth; x += 2) { - signed y1 = (signed)src_y[x] - 16; - signed y2 = (signed)src_y[x + 1] - 16; - - signed u = (signed)src_u[x & ~1] - 128; - signed v = (signed)src_u[(x & ~1) + 1] - 128; - - signed u_b = u * 517; - signed u_g = -u * 100; - signed v_g = -v * 208; - signed v_r = v * 409; - - signed tmp1 = y1 * 298; - signed b1 = (tmp1 + u_b) / 256; - signed g1 = (tmp1 + v_g + u_g) / 256; - signed r1 = (tmp1 + v_r) / 256; - - signed tmp2 = y2 * 298; - signed b2 = (tmp2 + u_b) / 256; - signed g2 = (tmp2 + v_g + u_g) / 256; - signed r2 = (tmp2 + v_r) / 256; - - uint32_t rgb1 = - ((kAdjustedClip[b1] >> 3) << 11) - | ((kAdjustedClip[g1] >> 2) << 5) - | (kAdjustedClip[r1] >> 3); - - uint32_t rgb2 = - ((kAdjustedClip[b2] >> 3) << 11) - | ((kAdjustedClip[g2] >> 2) << 5) - | (kAdjustedClip[r2] >> 3); - - dst_ptr[x / 2] = (rgb2 << 16) | rgb1; - } - - src_y += mDecodedWidth; - - if (y & 1) { - src_u += mDecodedWidth; - } - - dst_ptr += mDecodedWidth / 2; - } + mConverter.convert( + mDecodedWidth, mDecodedHeight, + data, 0, dst, 2 * mDecodedWidth); mISurface->postBuffer(offset); mIndex = 1 - mIndex; } -uint8_t *SoftwareRenderer::initClip() { - static const signed kClipMin = -278; - static const signed kClipMax = 535; - - if (mClip == NULL) { - mClip = new uint8_t[kClipMax - kClipMin + 1]; - - for (signed i = kClipMin; i <= kClipMax; ++i) { - mClip[i - kClipMin] = (i < 0) ? 0 : (i > 255) ? 255 : (uint8_t)i; - } - } - - return &mClip[-kClipMin]; -} - } // namespace android diff --git a/media/libstagefright/omx/TIHardwareRenderer.cpp b/media/libstagefright/omx/TIHardwareRenderer.cpp index ebade4a..6dde86a 100644 --- a/media/libstagefright/omx/TIHardwareRenderer.cpp +++ b/media/libstagefright/omx/TIHardwareRenderer.cpp @@ -17,7 +17,8 @@ #define LOG_TAG "TIHardwareRenderer" #include <utils/Log.h> -#include <media/stagefright/TIHardwareRenderer.h> +#include "../include/TIHardwareRenderer.h" + #include <media/stagefright/MediaDebug.h> #include <ui/ISurface.h> #include <ui/Overlay.h> diff --git a/media/libstagefright/stagefright_string.cpp b/media/libstagefright/string.cpp index 2aedb80..bd6204b 100644 --- a/media/libstagefright/stagefright_string.cpp +++ b/media/libstagefright/string.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include <media/stagefright/stagefright_string.h> +#include "include/stagefright_string.h" namespace android { diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java index e66e560..fa0986a 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java @@ -226,8 +226,7 @@ public class CameraTest extends ActivityInstrumentationTestCase<MediaFrameworkTe * Test case 1: Take a picture and verify all the callback * functions are called properly. */ - // TODO: add this back to LargeTest once bug 2141755 is fixed - // @LargeTest + @LargeTest public void testTakePicture() throws Exception { synchronized (lock) { initializeMessageLooper(); diff --git a/opengl/java/android/opengl/GLSurfaceView.java b/opengl/java/android/opengl/GLSurfaceView.java index 0330163..bb90ef8 100644 --- a/opengl/java/android/opengl/GLSurfaceView.java +++ b/opengl/java/android/opengl/GLSurfaceView.java @@ -18,7 +18,6 @@ package android.opengl; import java.io.Writer; import java.util.ArrayList; -import java.util.concurrent.Semaphore; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL11; @@ -30,6 +29,8 @@ import javax.microedition.khronos.opengles.GL; import javax.microedition.khronos.opengles.GL10; import android.content.Context; +import android.content.pm.ConfigurationInfo; +import android.os.SystemProperties; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; @@ -682,7 +683,10 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { int[] num_config = new int[1]; - egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config); + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } int numConfigs = num_config[0]; @@ -692,8 +696,10 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } EGLConfig[] configs = new EGLConfig[numConfigs]; - egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, - num_config); + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } EGLConfig config = chooseConfig(egl, display, configs); if (config == null) { throw new IllegalArgumentException("No config chosen"); @@ -818,11 +824,17 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback */ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + /* * We can now initialize EGL for that display */ int[] version = new int[2]; - mEgl.eglInitialize(mEglDisplay, version); + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); /* @@ -1077,6 +1089,7 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } if (changed) { gl = (GL10) mEglHelper.createSurface(getHolder()); + sGLThreadManager.checkGLDriver(gl); tellRendererSurfaceChanged = true; } if (tellRendererSurfaceCreated) { @@ -1286,37 +1299,80 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } } - static class GLThreadManager { + private static class GLThreadManager { public boolean shouldHaveEgl(GLThread thread) { - synchronized(this) { - return thread == mMostRecentGLThread || mMostRecentGLThread == null; + if (mMultipleGLESContextsAllowed) { + return true; + } else { + synchronized(this) { + return thread == mMostRecentGLThread || mMostRecentGLThread == null; + } } } + public void start(GLThread thread) throws InterruptedException { + if (! mGLESVersionCheckComplete) { + mGLESVersion = SystemProperties.getInt( + "ro.opengles.version", + ConfigurationInfo.GL_ES_VERSION_UNDEFINED); + if (mGLESVersion >= kGLES_20) { + mMultipleGLESContextsAllowed = true; + } + mGLESVersionCheckComplete = true; + } + GLThread oldThread = null; synchronized(this) { oldThread = mMostRecentGLThread; mMostRecentGLThread = thread; } - if (oldThread != null) { + if (oldThread != null && ! mMultipleGLESContextsAllowed) { synchronized(oldThread) { oldThread.notifyAll(); } } - sEglSemaphore.acquire(); - } - public void end(GLThread thread) { - sEglSemaphore.release(); + synchronized(this) { - if (mMostRecentGLThread == thread) { - mMostRecentGLThread = null; + while ((! mMultipleGLESContextsAllowed) + && mGLContextCount > 0) { + wait(); } + + mGLContextCount++; } } + + public synchronized void end(GLThread thread) { + mGLContextCount--; + notifyAll(); + if (mMostRecentGLThread == thread) { + mMostRecentGLThread = null; + } + } + + public synchronized void checkGLDriver(GL10 gl) { + if (! mGLESDriverCheckComplete) { + if (mGLESVersion < kGLES_20) { + String renderer = gl.glGetString(GL10.GL_RENDERER); + mMultipleGLESContextsAllowed = + ! renderer.startsWith(kMSM7K_RENDERER_PREFIX); + notifyAll(); + } + mGLESDriverCheckComplete = true; + } + } + + private boolean mGLESVersionCheckComplete; + private int mGLESVersion; private GLThread mMostRecentGLThread; - } + private boolean mGLESDriverCheckComplete; + private boolean mMultipleGLESContextsAllowed; + private int mGLContextCount; + private static final int kGLES_20 = 0x20000; + private static final String kMSM7K_RENDERER_PREFIX = + "Q3Dimension MSM7500 "; + }; - private static final Semaphore sEglSemaphore = new Semaphore(1); private static final GLThreadManager sGLThreadManager = new GLThreadManager(); private boolean mSizeChanged = true; diff --git a/opengl/tests/gl2_jni/src/com/android/gl2jni/GL2JNIView.java b/opengl/tests/gl2_jni/src/com/android/gl2jni/GL2JNIView.java index 2dae090..72b1dfb 100644 --- a/opengl/tests/gl2_jni/src/com/android/gl2jni/GL2JNIView.java +++ b/opengl/tests/gl2_jni/src/com/android/gl2jni/GL2JNIView.java @@ -56,19 +56,22 @@ import javax.microedition.khronos.opengles.GL10; */ class GL2JNIView extends GLSurfaceView { private static String TAG = "GL2JNIView"; - GL2JNIView(Context context) { + + public GL2JNIView(Context context) { super(context); - init(); + init(false, 0, 0); } - public GL2JNIView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); + public GL2JNIView(Context context, boolean translucent, int depth, int stencil) { + super(context); + init(translucent, depth, stencil); } - private void init() { + private void init(boolean translucent, int depth, int stencil) { setEGLContextFactory(new ContextFactory()); - setEGLConfigChooser(new ConfigChooser()); + setEGLConfigChooser( translucent ? + new ConfigChooser(8,8,8,8, depth, stencil) : + new ConfigChooser(5,6,5,0, depth, stencil)); setRenderer(new Renderer()); } @@ -105,6 +108,16 @@ class GL2JNIView extends GLSurfaceView { EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL10.EGL_NONE }; + + public ConfigChooser(int r, int g, int b, int a, int depth, int stencil) { + mRedSize = r; + mGreenSize = g; + mBlueSize = b; + mAlphaSize = a; + mDepthSize = depth; + mStencilSize = stencil; + } + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { int[] num_config = new int[1]; @@ -112,14 +125,158 @@ class GL2JNIView extends GLSurfaceView { int numConfigs = num_config[0]; - Log.w(TAG, String.format("Found %d configurations", numConfigs)); if (numConfigs <= 0) { throw new IllegalArgumentException("No configs match configSpec"); } EGLConfig[] configs = new EGLConfig[numConfigs]; egl.eglChooseConfig(display, s_configAttribs2, configs, numConfigs, num_config); - return configs[0]; + // printConfigs(egl, display, configs); + return chooseConfig(egl, display, configs); } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + EGLConfig closestConfig = null; + int closestDistance = 1000; + for(EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if (d >= mDepthSize && s>= mStencilSize) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + int distance = Math.abs(r - mRedSize) + + Math.abs(g - mGreenSize) + + Math.abs(b - mBlueSize) + + Math.abs(a - mAlphaSize); + if (distance < closestDistance) { + closestDistance = distance; + closestConfig = config; + } + } + } + return closestConfig; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private void printConfigs(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + int numConfigs = configs.length; + Log.w(TAG, String.format("%d configurations", numConfigs)); + for (int i = 0; i < numConfigs; i++) { + Log.w(TAG, String.format("Configuration %d:\n", i)); + printConfig(egl, display, configs[i]); + } + } + + private void printConfig(EGL10 egl, EGLDisplay display, + EGLConfig config) { + int[] attributes = { + EGL10.EGL_BUFFER_SIZE, + EGL10.EGL_ALPHA_SIZE, + EGL10.EGL_BLUE_SIZE, + EGL10.EGL_GREEN_SIZE, + EGL10.EGL_RED_SIZE, + EGL10.EGL_DEPTH_SIZE, + EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_CONFIG_CAVEAT, + EGL10.EGL_CONFIG_ID, + EGL10.EGL_LEVEL, + EGL10.EGL_MAX_PBUFFER_HEIGHT, + EGL10.EGL_MAX_PBUFFER_PIXELS, + EGL10.EGL_MAX_PBUFFER_WIDTH, + EGL10.EGL_NATIVE_RENDERABLE, + EGL10.EGL_NATIVE_VISUAL_ID, + EGL10.EGL_NATIVE_VISUAL_TYPE, + 0x3030, // EGL10.EGL_PRESERVED_RESOURCES, + EGL10.EGL_SAMPLES, + EGL10.EGL_SAMPLE_BUFFERS, + EGL10.EGL_SURFACE_TYPE, + EGL10.EGL_TRANSPARENT_TYPE, + EGL10.EGL_TRANSPARENT_RED_VALUE, + EGL10.EGL_TRANSPARENT_GREEN_VALUE, + EGL10.EGL_TRANSPARENT_BLUE_VALUE, + 0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB, + 0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA, + 0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL, + 0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL, + EGL10.EGL_LUMINANCE_SIZE, + EGL10.EGL_ALPHA_MASK_SIZE, + EGL10.EGL_COLOR_BUFFER_TYPE, + EGL10.EGL_RENDERABLE_TYPE, + 0x3042 // EGL10.EGL_CONFORMANT + }; + String[] names = { + "EGL_BUFFER_SIZE", + "EGL_ALPHA_SIZE", + "EGL_BLUE_SIZE", + "EGL_GREEN_SIZE", + "EGL_RED_SIZE", + "EGL_DEPTH_SIZE", + "EGL_STENCIL_SIZE", + "EGL_CONFIG_CAVEAT", + "EGL_CONFIG_ID", + "EGL_LEVEL", + "EGL_MAX_PBUFFER_HEIGHT", + "EGL_MAX_PBUFFER_PIXELS", + "EGL_MAX_PBUFFER_WIDTH", + "EGL_NATIVE_RENDERABLE", + "EGL_NATIVE_VISUAL_ID", + "EGL_NATIVE_VISUAL_TYPE", + "EGL_PRESERVED_RESOURCES", + "EGL_SAMPLES", + "EGL_SAMPLE_BUFFERS", + "EGL_SURFACE_TYPE", + "EGL_TRANSPARENT_TYPE", + "EGL_TRANSPARENT_RED_VALUE", + "EGL_TRANSPARENT_GREEN_VALUE", + "EGL_TRANSPARENT_BLUE_VALUE", + "EGL_BIND_TO_TEXTURE_RGB", + "EGL_BIND_TO_TEXTURE_RGBA", + "EGL_MIN_SWAP_INTERVAL", + "EGL_MAX_SWAP_INTERVAL", + "EGL_LUMINANCE_SIZE", + "EGL_ALPHA_MASK_SIZE", + "EGL_COLOR_BUFFER_TYPE", + "EGL_RENDERABLE_TYPE", + "EGL_CONFORMANT" + }; + int[] value = new int[1]; + for (int i = 0; i < attributes.length; i++) { + int attribute = attributes[i]; + String name = names[i]; + if ( egl.eglGetConfigAttrib(display, config, attribute, value)) { + Log.w(TAG, String.format(" %s: %d\n", name, value[0])); + } else { + // Log.w(TAG, String.format(" %s: failed\n", name)); + while (egl.eglGetError() != EGL10.EGL_SUCCESS); + } + } + } + + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + private int[] mValue = new int[1]; } private static class Renderer implements GLSurfaceView.Renderer { diff --git a/opengl/tests/gldual/Android.mk b/opengl/tests/gldual/Android.mk new file mode 100644 index 0000000..e73c249 --- /dev/null +++ b/opengl/tests/gldual/Android.mk @@ -0,0 +1,51 @@ +######################################################################### +# OpenGL ES JNI sample +# This makefile builds both an activity and a shared library. +######################################################################### +ifneq ($(TARGET_SIMULATOR),true) # not 64 bit clean + +TOP_LOCAL_PATH:= $(call my-dir) + +# Build activity + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := user + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := GLDual + +LOCAL_JNI_SHARED_LIBRARIES := libgldualjni + +include $(BUILD_PACKAGE) + +######################################################################### +# Build JNI Shared Library +######################################################################### + +LOCAL_PATH:= $(LOCAL_PATH)/jni + +include $(CLEAR_VARS) + +# Optional tag would mean it doesn't get installed by default +LOCAL_MODULE_TAGS := optional + +LOCAL_CFLAGS := -Werror + +LOCAL_SRC_FILES:= \ + gl_code.cpp + +LOCAL_SHARED_LIBRARIES := \ + libutils \ + libEGL \ + libGLESv2 + +LOCAL_MODULE := libgldualjni + +LOCAL_PRELINK_MODULE := false + +include $(BUILD_SHARED_LIBRARY) + +endif # TARGET_SIMULATOR diff --git a/opengl/tests/gldual/AndroidManifest.xml b/opengl/tests/gldual/AndroidManifest.xml new file mode 100644 index 0000000..06f4c4d --- /dev/null +++ b/opengl/tests/gldual/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** +** Copyright 2009, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.gldual"> + <application + android:label="@string/gldual_activity"> + <activity android:name="GLDualActivity" + android:theme="@android:style/Theme.NoTitleBar.Fullscreen" + android:launchMode="singleTask" + android:configChanges="orientation|keyboardHidden"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/opengl/tests/gldual/jni/gl_code.cpp b/opengl/tests/gldual/jni/gl_code.cpp new file mode 100644 index 0000000..f1f0a1f --- /dev/null +++ b/opengl/tests/gldual/jni/gl_code.cpp @@ -0,0 +1,165 @@ +// OpenGL ES 2.0 code + +#include <nativehelper/jni.h> +#define LOG_TAG "GL2JNI gl_code.cpp" +#include <utils/Log.h> + +#include <EGL/egl.h> +#include <GLES2/gl2.h> +#include <GLES2/gl2ext.h> + +#include <stdio.h> +#include <stdlib.h> +#include <math.h> + +static void printGLString(const char *name, GLenum s) { + const char *v = (const char *) glGetString(s); + LOGI("GL %s = %s\n", name, v); +} + +static void checkGlError(const char* op) { + for (GLint error = glGetError(); error; error + = glGetError()) { + LOGI("after %s() glError (0x%x)\n", op, error); + } +} + +static const char gVertexShader[] = "attribute vec4 vPosition;\n" + "void main() {\n" + " gl_Position = vPosition;\n" + "}\n"; + +static const char gFragmentShader[] = "precision mediump float;\n" + "void main() {\n" + " gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);\n" + "}\n"; + +GLuint loadShader(GLenum shaderType, const char* pSource) { + GLuint shader = glCreateShader(shaderType); + if (shader) { + glShaderSource(shader, 1, &pSource, NULL); + glCompileShader(shader); + GLint compiled = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (!compiled) { + GLint infoLen = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen); + if (infoLen) { + char* buf = (char*) malloc(infoLen); + if (buf) { + glGetShaderInfoLog(shader, infoLen, NULL, buf); + LOGE("Could not compile shader %d:\n%s\n", + shaderType, buf); + free(buf); + } + glDeleteShader(shader); + shader = 0; + } + } + } + return shader; +} + +GLuint createProgram(const char* pVertexSource, const char* pFragmentSource) { + GLuint vertexShader = loadShader(GL_VERTEX_SHADER, pVertexSource); + if (!vertexShader) { + return 0; + } + + GLuint pixelShader = loadShader(GL_FRAGMENT_SHADER, pFragmentSource); + if (!pixelShader) { + return 0; + } + + GLuint program = glCreateProgram(); + if (program) { + glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + glLinkProgram(program); + GLint linkStatus = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linkStatus); + if (linkStatus != GL_TRUE) { + GLint bufLength = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufLength); + if (bufLength) { + char* buf = (char*) malloc(bufLength); + if (buf) { + glGetProgramInfoLog(program, bufLength, NULL, buf); + LOGE("Could not link program:\n%s\n", buf); + free(buf); + } + } + glDeleteProgram(program); + program = 0; + } + } + return program; +} + +GLuint gProgram; +GLuint gvPositionHandle; + +bool setupGraphics(int w, int h) { + printGLString("Version", GL_VERSION); + printGLString("Vendor", GL_VENDOR); + printGLString("Renderer", GL_RENDERER); + printGLString("Extensions", GL_EXTENSIONS); + + LOGI("setupGraphics(%d, %d)", w, h); + gProgram = createProgram(gVertexShader, gFragmentShader); + if (!gProgram) { + LOGE("Could not create program."); + return false; + } + gvPositionHandle = glGetAttribLocation(gProgram, "vPosition"); + checkGlError("glGetAttribLocation"); + LOGI("glGetAttribLocation(\"vPosition\") = %d\n", + gvPositionHandle); + + glViewport(0, 0, w, h); + checkGlError("glViewport"); + return true; +} + +const GLfloat gTriangleVertices[] = { 0.0f, 0.5f, -0.5f, -0.5f, + 0.5f, -0.5f }; + +void renderFrame() { + static float grey; + grey += 0.01f; + if (grey > 1.0f) { + grey = 0.0f; + } + glClearColor(grey, grey, grey, 1.0f); + checkGlError("glClearColor"); + glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + checkGlError("glClear"); + + glUseProgram(gProgram); + checkGlError("glUseProgram"); + + glVertexAttribPointer(gvPositionHandle, 2, GL_FLOAT, GL_FALSE, 0, gTriangleVertices); + checkGlError("glVertexAttribPointer"); + glEnableVertexAttribArray(gvPositionHandle); + checkGlError("glEnableVertexAttribArray"); + glDrawArrays(GL_TRIANGLES, 0, 3); + checkGlError("glDrawArrays"); +} + +extern "C" { + JNIEXPORT void JNICALL Java_com_android_gldual_GLDualLib_init(JNIEnv * env, jobject obj, jint width, jint height); + JNIEXPORT void JNICALL Java_com_android_gldual_GLDualLib_step(JNIEnv * env, jobject obj); +}; + +JNIEXPORT void JNICALL Java_com_android_gldual_GLDualLib_init(JNIEnv * env, jobject obj, jint width, jint height)
+{ + setupGraphics(width, height); +} + +JNIEXPORT void JNICALL Java_com_android_gldual_GLDualLib_step(JNIEnv * env, jobject obj) +{ + renderFrame(); +} + diff --git a/opengl/tests/gldual/res/layout/gldual_activity.xml b/opengl/tests/gldual/res/layout/gldual_activity.xml new file mode 100644 index 0000000..f2d59c7 --- /dev/null +++ b/opengl/tests/gldual/res/layout/gldual_activity.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/text" + + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + <android.opengl.GLSurfaceView android:id="@+id/gl1" + android:layout_width="fill_parent" + android:layout_height="0dip" + android:layout_weight="1" /> + <com.android.gldual.GLDualGL2View android:id="@+id/gl2" + android:layout_width="fill_parent" + android:layout_height="0dip" + android:layout_weight="1" /> +</LinearLayout> diff --git a/opengl/tests/gldual/res/values/strings.xml b/opengl/tests/gldual/res/values/strings.xml new file mode 100644 index 0000000..4267dff --- /dev/null +++ b/opengl/tests/gldual/res/values/strings.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** +** Copyright 2006, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> + +<!-- This file contains resource definitions for displayed strings, allowing + them to be changed based on the locale and options. --> + +<resources> + <!-- Simple strings. --> + <string name="gldual_activity">GLDual</string> + +</resources> + diff --git a/opengl/tests/gldual/src/com/android/gldual/GLDualActivity.java b/opengl/tests/gldual/src/com/android/gldual/GLDualActivity.java new file mode 100644 index 0000000..9d88f64 --- /dev/null +++ b/opengl/tests/gldual/src/com/android/gldual/GLDualActivity.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gldual; + +import android.app.Activity; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.os.Bundle; +import android.view.View; +import android.widget.LinearLayout; + + +public class GLDualActivity extends Activity { + + GLSurfaceView mGLView; + GLDualGL2View mGL2View; + + @Override protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + View root = getLayoutInflater().inflate(R.layout.gldual_activity, null); + mGLView = (GLSurfaceView) root.findViewById(R.id.gl1); + mGLView.setEGLConfigChooser(5,6,5,0,0,0); + mGLView.setRenderer(new TriangleRenderer()); + mGL2View = (GLDualGL2View) root.findViewById(R.id.gl2); + setContentView(root); + } + + @Override protected void onPause() { + super.onPause(); + mGLView.onPause(); + mGL2View.onPause(); + } + + @Override protected void onResume() { + super.onResume(); + mGLView.onResume(); + mGL2View.onResume(); + } +} diff --git a/opengl/tests/gldual/src/com/android/gldual/GLDualGL2View.java b/opengl/tests/gldual/src/com/android/gldual/GLDualGL2View.java new file mode 100644 index 0000000..8f5e347 --- /dev/null +++ b/opengl/tests/gldual/src/com/android/gldual/GLDualGL2View.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gldual; +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.opengles.GL10; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import android.util.Log; + +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying an OpenGL animation. This allows the animation to run in a + * separate thread, without requiring that it be driven by the update mechanism + * of the view hierarchy. + * + * The application-specific rendering code is delegated to a GLView.Renderer + * instance. + */ +class GLDualGL2View extends GLSurfaceView { + private static String TAG = "GLDualGL2View"; + + public GLDualGL2View(Context context) { + super(context); + init(false, 0, 0); + } + + public GLDualGL2View(Context context, AttributeSet set) { + super(context, set); + init(false, 0, 0); + } + + public GLDualGL2View(Context context, boolean translucent, int depth, int stencil) { + super(context); + init(translucent, depth, stencil); + } + + private void init(boolean translucent, int depth, int stencil) { + setEGLContextFactory(new ContextFactory()); + setEGLConfigChooser( translucent ? + new ConfigChooser(8,8,8,8, depth, stencil) : + new ConfigChooser(5,6,5,0, depth, stencil)); + setRenderer(new Renderer()); + } + + private static class ContextFactory implements GLSurfaceView.EGLContextFactory { + private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + Log.w(TAG, "creating OpenGL ES 2.0 context"); + checkEglError("Before eglCreateContext", egl); + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + EGLContext context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + checkEglError("After eglCreateContext", egl); + return context; + } + + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + } + + private static void checkEglError(String prompt, EGL10 egl) { + int error; + while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) { + Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error)); + } + } + + private static class ConfigChooser implements GLSurfaceView.EGLConfigChooser { + private static int EGL_OPENGL_ES2_BIT = 4; + private static int[] s_configAttribs2 = + { + EGL10.EGL_RED_SIZE, 4, + EGL10.EGL_GREEN_SIZE, 4, + EGL10.EGL_BLUE_SIZE, 4, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + }; + + public ConfigChooser(int r, int g, int b, int a, int depth, int stencil) { + mRedSize = r; + mGreenSize = g; + mBlueSize = b; + mAlphaSize = a; + mDepthSize = depth; + mStencilSize = stencil; + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + + int[] num_config = new int[1]; + egl.eglChooseConfig(display, s_configAttribs2, null, 0, num_config); + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + EGLConfig[] configs = new EGLConfig[numConfigs]; + egl.eglChooseConfig(display, s_configAttribs2, configs, numConfigs, num_config); + // printConfigs(egl, display, configs); + return chooseConfig(egl, display, configs); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + EGLConfig closestConfig = null; + int closestDistance = 1000; + for(EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if (d >= mDepthSize && s>= mStencilSize) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + int distance = Math.abs(r - mRedSize) + + Math.abs(g - mGreenSize) + + Math.abs(b - mBlueSize) + + Math.abs(a - mAlphaSize); + if (distance < closestDistance) { + closestDistance = distance; + closestConfig = config; + } + } + } + return closestConfig; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private void printConfigs(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + int numConfigs = configs.length; + Log.w(TAG, String.format("%d configurations", numConfigs)); + for (int i = 0; i < numConfigs; i++) { + Log.w(TAG, String.format("Configuration %d:\n", i)); + printConfig(egl, display, configs[i]); + } + } + + private void printConfig(EGL10 egl, EGLDisplay display, + EGLConfig config) { + int[] attributes = { + EGL10.EGL_BUFFER_SIZE, + EGL10.EGL_ALPHA_SIZE, + EGL10.EGL_BLUE_SIZE, + EGL10.EGL_GREEN_SIZE, + EGL10.EGL_RED_SIZE, + EGL10.EGL_DEPTH_SIZE, + EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_CONFIG_CAVEAT, + EGL10.EGL_CONFIG_ID, + EGL10.EGL_LEVEL, + EGL10.EGL_MAX_PBUFFER_HEIGHT, + EGL10.EGL_MAX_PBUFFER_PIXELS, + EGL10.EGL_MAX_PBUFFER_WIDTH, + EGL10.EGL_NATIVE_RENDERABLE, + EGL10.EGL_NATIVE_VISUAL_ID, + EGL10.EGL_NATIVE_VISUAL_TYPE, + 0x3030, // EGL10.EGL_PRESERVED_RESOURCES, + EGL10.EGL_SAMPLES, + EGL10.EGL_SAMPLE_BUFFERS, + EGL10.EGL_SURFACE_TYPE, + EGL10.EGL_TRANSPARENT_TYPE, + EGL10.EGL_TRANSPARENT_RED_VALUE, + EGL10.EGL_TRANSPARENT_GREEN_VALUE, + EGL10.EGL_TRANSPARENT_BLUE_VALUE, + 0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB, + 0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA, + 0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL, + 0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL, + EGL10.EGL_LUMINANCE_SIZE, + EGL10.EGL_ALPHA_MASK_SIZE, + EGL10.EGL_COLOR_BUFFER_TYPE, + EGL10.EGL_RENDERABLE_TYPE, + 0x3042 // EGL10.EGL_CONFORMANT + }; + String[] names = { + "EGL_BUFFER_SIZE", + "EGL_ALPHA_SIZE", + "EGL_BLUE_SIZE", + "EGL_GREEN_SIZE", + "EGL_RED_SIZE", + "EGL_DEPTH_SIZE", + "EGL_STENCIL_SIZE", + "EGL_CONFIG_CAVEAT", + "EGL_CONFIG_ID", + "EGL_LEVEL", + "EGL_MAX_PBUFFER_HEIGHT", + "EGL_MAX_PBUFFER_PIXELS", + "EGL_MAX_PBUFFER_WIDTH", + "EGL_NATIVE_RENDERABLE", + "EGL_NATIVE_VISUAL_ID", + "EGL_NATIVE_VISUAL_TYPE", + "EGL_PRESERVED_RESOURCES", + "EGL_SAMPLES", + "EGL_SAMPLE_BUFFERS", + "EGL_SURFACE_TYPE", + "EGL_TRANSPARENT_TYPE", + "EGL_TRANSPARENT_RED_VALUE", + "EGL_TRANSPARENT_GREEN_VALUE", + "EGL_TRANSPARENT_BLUE_VALUE", + "EGL_BIND_TO_TEXTURE_RGB", + "EGL_BIND_TO_TEXTURE_RGBA", + "EGL_MIN_SWAP_INTERVAL", + "EGL_MAX_SWAP_INTERVAL", + "EGL_LUMINANCE_SIZE", + "EGL_ALPHA_MASK_SIZE", + "EGL_COLOR_BUFFER_TYPE", + "EGL_RENDERABLE_TYPE", + "EGL_CONFORMANT" + }; + int[] value = new int[1]; + for (int i = 0; i < attributes.length; i++) { + int attribute = attributes[i]; + String name = names[i]; + if ( egl.eglGetConfigAttrib(display, config, attribute, value)) { + Log.w(TAG, String.format(" %s: %d\n", name, value[0])); + } else { + // Log.w(TAG, String.format(" %s: failed\n", name)); + while (egl.eglGetError() != EGL10.EGL_SUCCESS); + } + } + } + + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + private int[] mValue = new int[1]; + } + + private static class Renderer implements GLSurfaceView.Renderer { + public void onDrawFrame(GL10 gl) { + GLDualLib.step(); + } + + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLDualLib.init(width, height); + } + + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + // Do nothing. + } + } +} + diff --git a/opengl/tests/gldual/src/com/android/gldual/GLDualLib.java b/opengl/tests/gldual/src/com/android/gldual/GLDualLib.java new file mode 100644 index 0000000..d8f765e --- /dev/null +++ b/opengl/tests/gldual/src/com/android/gldual/GLDualLib.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gldual; + +// Wrapper for native library + +public class GLDualLib { + + static { + System.loadLibrary("gldualjni"); + } + + /** + * @param width the current view width + * @param height the current view height + */ + public static native void init(int width, int height); + public static native void step(); +} diff --git a/opengl/tests/gldual/src/com/android/gldual/TriangleRenderer.java b/opengl/tests/gldual/src/com/android/gldual/TriangleRenderer.java new file mode 100644 index 0000000..098c4d2 --- /dev/null +++ b/opengl/tests/gldual/src/com/android/gldual/TriangleRenderer.java @@ -0,0 +1,149 @@ +package com.android.gldual; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import android.opengl.GLSurfaceView; +import android.opengl.GLU; +import android.os.SystemClock; + +public class TriangleRenderer implements GLSurfaceView.Renderer{ + + public TriangleRenderer() { + mTriangle = new Triangle(); + } + + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + /* + * By default, OpenGL enables features that improve quality + * but reduce performance. One might want to tweak that + * especially on software renderer. + */ + gl.glDisable(GL10.GL_DITHER); + + /* + * Some one-time OpenGL initialization can be made here + * probably based on features of this particular context + */ + gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, + GL10.GL_FASTEST); + + gl.glClearColor(.5f, .5f, .5f, 1); + gl.glShadeModel(GL10.GL_SMOOTH); + } + + public void onDrawFrame(GL10 gl) { + /* + * By default, OpenGL enables features that improve quality + * but reduce performance. One might want to tweak that + * especially on software renderer. + */ + gl.glDisable(GL10.GL_DITHER); + + /* + * Usually, the first thing one might want to do is to clear + * the screen. The most efficient way of doing this is to use + * glClear(). + */ + + gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); + + /* + * Now we're ready to draw some 3D objects + */ + + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glLoadIdentity(); + + GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + + long time = SystemClock.uptimeMillis() % 4000L; + float angle = 0.090f * ((int) time); + + gl.glRotatef(angle, 0, 0, 1.0f); + + mTriangle.draw(gl); + } + + public void onSurfaceChanged(GL10 gl, int w, int h) { + gl.glViewport(0, 0, w, h); + + /* + * Set our projection matrix. This doesn't have to be done + * each time we draw, but usually a new projection needs to + * be set when the viewport is resized. + */ + + float ratio = (float) w / h; + gl.glMatrixMode(GL10.GL_PROJECTION); + gl.glLoadIdentity(); + gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7); + + } + + private Triangle mTriangle; +} + +class Triangle { + public Triangle() { + + // Buffers to be passed to gl*Pointer() functions + // must be direct, i.e., they must be placed on the + // native heap where the garbage collector cannot + // move them. + // + // Buffers with multi-byte datatypes (e.g., short, int, float) + // must have their byte order set to native order + + ByteBuffer vbb = ByteBuffer.allocateDirect(VERTS * 3 * 4); + vbb.order(ByteOrder.nativeOrder()); + mFVertexBuffer = vbb.asFloatBuffer(); + + ByteBuffer tbb = ByteBuffer.allocateDirect(VERTS * 2 * 4); + tbb.order(ByteOrder.nativeOrder()); + + ByteBuffer ibb = ByteBuffer.allocateDirect(VERTS * 2); + ibb.order(ByteOrder.nativeOrder()); + mIndexBuffer = ibb.asShortBuffer(); + + // A unit-sided equalateral triangle centered on the origin. + float[] coords = { + // X, Y, Z + -0.5f, -0.25f, 0, + 0.5f, -0.25f, 0, + 0.0f, 0.559016994f, 0 + }; + + for (int i = 0; i < VERTS; i++) { + for(int j = 0; j < 3; j++) { + mFVertexBuffer.put(coords[i*3+j] * 2.0f); + } + } + + for(int i = 0; i < VERTS; i++) { + mIndexBuffer.put((short) i); + } + + mFVertexBuffer.position(0); + mIndexBuffer.position(0); + } + + public void draw(GL10 gl) { + gl.glFrontFace(GL10.GL_CCW); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mFVertexBuffer); + gl.glDrawElements(GL10.GL_TRIANGLE_STRIP, VERTS, + GL10.GL_UNSIGNED_SHORT, mIndexBuffer); + } + + private final static int VERTS = 3; + + private FloatBuffer mFVertexBuffer; + private ShortBuffer mIndexBuffer; +} diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java index 78215b0..a91635e 100644 --- a/services/java/com/android/server/ConnectivityService.java +++ b/services/java/com/android/server/ConnectivityService.java @@ -572,6 +572,8 @@ public class ConnectivityService extends IConnectivityManager.Stub { // javadoc from interface public int stopUsingNetworkFeature(int networkType, String feature) { + enforceChangePermission(); + int pid = getCallingPid(); int uid = getCallingUid(); @@ -611,7 +613,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { Log.d(TAG, "stopUsingNetworkFeature for net " + networkType + ": " + feature); } - enforceChangePermission(); + if (!ConnectivityManager.isNetworkTypeValid(networkType)) { return -1; } diff --git a/services/java/com/android/server/DropBoxService.java b/services/java/com/android/server/DropBoxService.java new file mode 100644 index 0000000..f4e5ebc --- /dev/null +++ b/services/java/com/android/server/DropBoxService.java @@ -0,0 +1,695 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.DropBox; +import android.os.ParcelFileDescriptor; +import android.os.StatFs; +import android.os.SystemClock; +import android.provider.Settings; +import android.text.format.DateFormat; +import android.util.Log; + +import com.android.internal.os.IDropBoxService; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Formatter; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.zip.GZIPOutputStream; + +/** + * Implementation of {@link IDropBoxService} using the filesystem. + * Clients use {@link DropBox} to access this service. + * + * {@hide} + */ +public final class DropBoxService extends IDropBoxService.Stub { + private static final String TAG = "DropBoxService"; + private static final int DEFAULT_RESERVE_PERCENT = 10; + private static final int DEFAULT_QUOTA_PERCENT = 10; + private static final int DEFAULT_QUOTA_KB = 5 * 1024; + private static final int DEFAULT_AGE_SECONDS = 3 * 86400; + private static final int QUOTA_RESCAN_MILLIS = 5000; + + // TODO: This implementation currently uses one file per entry, which is + // inefficient for smallish entries -- consider using a single queue file + // per tag (or even globally) instead. + + // The cached context and derived objects + + private final Context mContext; + private final ContentResolver mContentResolver; + private final File mDropBoxDir; + + // Accounting of all currently written log files (set in init()). + + private FileList mAllFiles = null; + private HashMap<String, FileList> mFilesByTag = null; + + // Various bits of disk information + + private StatFs mStatFs = null; + private int mBlockSize = 0; + private int mCachedQuotaBlocks = 0; // Space we can use: computed from free space, etc. + private long mCachedQuotaUptimeMillis = 0; + + // Ensure that all log entries have a unique timestamp + private long mLastTimestamp = 0; + + /** Receives events that might indicate a need to clean up files. */ + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mCachedQuotaUptimeMillis = 0; // Force a re-check of quota size + try { + init(); + trimToFit(); + } catch (IOException e) { + Log.e(TAG, "Can't init", e); + } + } + }; + + /** + * Creates an instance of managed drop box storage. Normally there is one of these + * run by the system, but others can be created for testing and other purposes. + * + * @param context to use for receiving free space & gservices intents + * @param path to store drop box entries in + */ + public DropBoxService(Context context, File path) { + mDropBoxDir = path; + + // Set up intent receivers + mContext = context; + mContentResolver = context.getContentResolver(); + context.registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW)); + context.registerReceiver(mReceiver, new IntentFilter(Settings.Gservices.CHANGED_ACTION)); + + // The real work gets done lazily in init() -- that way service creation always + // succeeds, and things like disk problems cause individual method failures. + } + + /** Unregisters broadcast receivers and any other hooks -- for test instances */ + public void stop() { + mContext.unregisterReceiver(mReceiver); + } + + public void add(DropBox.Entry entry) { + File temp = null; + OutputStream output = null; + final String tag = entry.getTag(); + try { + int flags = entry.getFlags(); + if ((flags & DropBox.IS_EMPTY) != 0) throw new IllegalArgumentException(); + + init(); + if (!isTagEnabled(tag)) return; + long max = trimToFit(); + long lastTrim = System.currentTimeMillis(); + + byte[] buffer = new byte[mBlockSize]; + InputStream input = entry.getInputStream(); + + // First, accumulate up to one block worth of data in memory before + // deciding whether to compress the data or not. + + int read = 0; + while (read < buffer.length) { + int n = input.read(buffer, read, buffer.length - read); + if (n <= 0) break; + read += n; + } + + // If we have at least one block, compress it -- otherwise, just write + // the data in uncompressed form. + + temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp"); + output = new FileOutputStream(temp); + if (read == buffer.length && ((flags & DropBox.IS_GZIPPED) == 0)) { + output = new GZIPOutputStream(output); + flags = flags | DropBox.IS_GZIPPED; + } + + do { + output.write(buffer, 0, read); + + long now = System.currentTimeMillis(); + if (now - lastTrim > 30 * 1000) { + max = trimToFit(); // In case data dribbles in slowly + lastTrim = now; + } + + read = input.read(buffer); + if (read <= 0) { + output.close(); // Get a final size measurement + output = null; + } else { + output.flush(); // So the size measurement is pseudo-reasonable + } + + long len = temp.length(); + if (len > max) { + Log.w(TAG, "Dropping: " + tag + " (" + temp.length() + " > " + max + " bytes)"); + temp.delete(); + temp = null; // Pass temp = null to createEntry() to leave a tombstone + break; + } + } while (read > 0); + + createEntry(temp, tag, flags); + temp = null; + } catch (IOException e) { + Log.e(TAG, "Can't write: " + tag, e); + } finally { + try { if (output != null) output.close(); } catch (IOException e) {} + entry.close(); + if (temp != null) temp.delete(); + } + } + + public boolean isTagEnabled(String tag) { + return !"disabled".equals(Settings.Gservices.getString( + mContentResolver, Settings.Gservices.DROPBOX_TAG_PREFIX + tag)); + } + + public synchronized DropBox.Entry getNextEntry(String tag, long millis) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.READ_LOGS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("READ_LOGS permission required"); + } + + try { + init(); + } catch (IOException e) { + Log.e(TAG, "Can't init", e); + return null; + } + + FileList list = tag == null ? mAllFiles : mFilesByTag.get(tag); + if (list == null) return null; + + for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) { + if (entry.tag == null) continue; + if ((entry.flags & DropBox.IS_EMPTY) != 0) { + return new DropBox.Entry(entry.tag, entry.timestampMillis); + } + try { + return new DropBox.Entry(entry.tag, entry.timestampMillis, entry.file, entry.flags); + } catch (IOException e) { + Log.e(TAG, "Can't read: " + entry.file, e); + // Continue to next file + } + } + + return null; + } + + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: Can't dump DropBoxService"); + return; + } + + try { + init(); + } catch (IOException e) { + pw.println("Can't initialize: " + e); + Log.e(TAG, "Can't init", e); + return; + } + + boolean doPrint = false, doFile = false; + ArrayList<String> searchArgs = new ArrayList<String>(); + for (int i = 0; args != null && i < args.length; i++) { + if (args[i].equals("-p") || args[i].equals("--print")) { + doPrint = true; + } else if (args[i].equals("-f") || args[i].equals("--file")) { + doFile = true; + } else if (args[i].startsWith("-")) { + pw.print("Unknown argument: "); + pw.println(args[i]); + } else { + searchArgs.add(args[i]); + } + } + + pw.format("Drop box contents: %d entries", mAllFiles.contents.size()); + pw.println(); + + if (!searchArgs.isEmpty()) { + pw.print("Searching for:"); + for (String a : searchArgs) pw.format(" %s", a); + pw.println(); + } + + int numFound = 0; + pw.println(); + for (EntryFile entry : mAllFiles.contents) { + String date = new Formatter().format("%s.%03d", + DateFormat.format("yyyy-MM-dd kk:mm:ss", entry.timestampMillis), + entry.timestampMillis % 1000).toString(); + + boolean match = true; + for (String a: searchArgs) match = match && (date.contains(a) || a.equals(entry.tag)); + if (!match) continue; + + numFound++; + pw.print(date); + pw.print(" "); + pw.print(entry.tag == null ? "(no tag)" : entry.tag); + if (entry.file == null) { + pw.println(" (no file)"); + continue; + } else if ((entry.flags & DropBox.IS_EMPTY) != 0) { + pw.println(" (contents lost)"); + continue; + } else { + pw.print((entry.flags & DropBox.IS_GZIPPED) != 0 ? " (comopressed " : " ("); + pw.print((entry.flags & DropBox.IS_TEXT) != 0 ? "text" : "data"); + pw.format(", %d bytes)", entry.file.length()); + pw.println(); + } + + if (doFile || (doPrint && (entry.flags & DropBox.IS_TEXT) == 0)) { + if (!doPrint) pw.print(" "); + pw.println(entry.file.getPath()); + } + + if ((entry.flags & DropBox.IS_TEXT) != 0 && (doPrint || !doFile)) { + DropBox.Entry dbe = null; + try { + dbe = new DropBox.Entry( + entry.tag, entry.timestampMillis, entry.file, entry.flags); + + if (doPrint) { + InputStreamReader r = new InputStreamReader(dbe.getInputStream()); + char[] buf = new char[4096]; + boolean newline = false; + for (;;) { + int n = r.read(buf); + if (n <= 0) break; + pw.write(buf, 0, n); + newline = (buf[n - 1] == '\n'); + } + if (!newline) pw.println(); + } else { + String text = dbe.getText(70); + boolean truncated = (text.length() == 70); + pw.print(" "); + pw.print(text.trim().replace('\n', '/')); + if (truncated) pw.print(" ..."); + pw.println(); + } + } catch (IOException e) { + pw.print("*** "); + pw.println(e.toString()); + Log.e(TAG, "Can't read: " + entry.file, e); + } finally { + if (dbe != null) dbe.close(); + } + } + + if (doPrint) pw.println(); + } + + if (numFound == 0) pw.println("(No entries found.)"); + + if (args == null || args.length == 0) { + if (!doPrint) pw.println(); + pw.println("Usage: dumpsys dropbox [--print|--file] [YYYY-mm-dd] [HH:MM:SS.SSS] [tag]"); + } + } + + /////////////////////////////////////////////////////////////////////////// + + /** Chronologically sorted list of {@link #EntryFile} */ + private static final class FileList implements Comparable<FileList> { + public int blocks = 0; + public final TreeSet<EntryFile> contents = new TreeSet<EntryFile>(); + + /** Sorts bigger FileList instances before smaller ones. */ + public final int compareTo(FileList o) { + if (blocks != o.blocks) return o.blocks - blocks; + if (this == o) return 0; + if (hashCode() < o.hashCode()) return -1; + if (hashCode() > o.hashCode()) return 1; + return 0; + } + } + + /** Metadata describing an on-disk log file. */ + private static final class EntryFile implements Comparable<EntryFile> { + public final String tag; + public final long timestampMillis; + public final int flags; + public final File file; + public final int blocks; + + /** Sorts earlier EntryFile instances before later ones. */ + public final int compareTo(EntryFile o) { + if (timestampMillis < o.timestampMillis) return -1; + if (timestampMillis > o.timestampMillis) return 1; + if (file != null && o.file != null) return file.compareTo(o.file); + if (o.file != null) return -1; + if (file != null) return 1; + if (this == o) return 0; + if (hashCode() < o.hashCode()) return -1; + if (hashCode() > o.hashCode()) return 1; + return 0; + } + + /** + * Moves an existing temporary file to a new log filename. + * @param temp file to rename + * @param dir to store file in + * @param tag to use for new log file name + * @param timestampMillis of log entry + * @param flags for the entry data + * @param blockSize to use for space accounting + * @throws IOException if the file can't be moved + */ + public EntryFile(File temp, File dir, String tag,long timestampMillis, + int flags, int blockSize) throws IOException { + if ((flags & DropBox.IS_EMPTY) != 0) throw new IllegalArgumentException(); + + this.tag = tag; + this.timestampMillis = timestampMillis; + this.flags = flags; + this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + + ((flags & DropBox.IS_TEXT) != 0 ? ".txt" : ".dat") + + ((flags & DropBox.IS_GZIPPED) != 0 ? ".gz" : "")); + + if (!temp.renameTo(this.file)) { + throw new IOException("Can't rename " + temp + " to " + this.file); + } + this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize); + } + + /** + * Creates a zero-length tombstone for a file whose contents were lost. + * @param dir to store file in + * @param tag to use for new log file name + * @param timestampMillis of log entry + * @throws IOException if the file can't be created. + */ + public EntryFile(File dir, String tag, long timestampMillis) throws IOException { + this.tag = tag; + this.timestampMillis = timestampMillis; + this.flags = DropBox.IS_EMPTY; + this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + ".lost"); + this.blocks = 0; + new FileOutputStream(this.file).close(); + } + + /** + * Extracts metadata from an existing on-disk log filename. + * @param file name of existing log file + * @param blockSize to use for space accounting + */ + public EntryFile(File file, int blockSize) { + this.file = file; + this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize); + + String name = file.getName(); + int at = name.lastIndexOf('@'); + if (at < 0) { + this.tag = null; + this.timestampMillis = 0; + this.flags = DropBox.IS_EMPTY; + return; + } + + int flags = 0; + this.tag = Uri.decode(name.substring(0, at)); + if (name.endsWith(".gz")) { + flags |= DropBox.IS_GZIPPED; + name = name.substring(0, name.length() - 3); + } + if (name.endsWith(".lost")) { + flags |= DropBox.IS_EMPTY; + name = name.substring(at + 1, name.length() - 5); + } else if (name.endsWith(".txt")) { + flags |= DropBox.IS_TEXT; + name = name.substring(at + 1, name.length() - 4); + } else if (name.endsWith(".dat")) { + name = name.substring(at + 1, name.length() - 4); + } else { + this.flags = DropBox.IS_EMPTY; + this.timestampMillis = 0; + return; + } + this.flags = flags; + + long millis; + try { millis = Long.valueOf(name); } catch (NumberFormatException e) { millis = 0; } + this.timestampMillis = millis; + } + + /** + * Creates a EntryFile object with only a timestamp for comparison purposes. + * @param timestampMillis to compare with. + */ + public EntryFile(long millis) { + this.tag = null; + this.timestampMillis = millis; + this.flags = DropBox.IS_EMPTY; + this.file = null; + this.blocks = 0; + } + } + + /////////////////////////////////////////////////////////////////////////// + + /** If never run before, scans disk contents to build in-memory tracking data. */ + private synchronized void init() throws IOException { + if (mStatFs == null) { + if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) { + throw new IOException("Can't mkdir: " + mDropBoxDir); + } + try { + mStatFs = new StatFs(mDropBoxDir.getPath()); + mBlockSize = mStatFs.getBlockSize(); + } catch (IllegalArgumentException e) { // StatFs throws this on error + throw new IOException("Can't statfs: " + mDropBoxDir); + } + } + + if (mAllFiles == null) { + File[] files = mDropBoxDir.listFiles(); + if (files == null) throw new IOException("Can't list files: " + mDropBoxDir); + + mAllFiles = new FileList(); + mFilesByTag = new HashMap<String, FileList>(); + + // Scan pre-existing files. + for (File file : files) { + if (file.getName().endsWith(".tmp")) { + Log.i(TAG, "Cleaning temp file: " + file); + file.delete(); + continue; + } + + EntryFile entry = new EntryFile(file, mBlockSize); + if (entry.tag == null) { + Log.w(TAG, "Unrecognized file: " + file); + continue; + } else if (entry.timestampMillis == 0) { + Log.w(TAG, "Invalid filename: " + file); + file.delete(); + continue; + } + + enrollEntry(entry); + } + } + } + + /** Adds a disk log file to in-memory tracking for accounting and enumeration. */ + private synchronized void enrollEntry(EntryFile entry) { + mAllFiles.contents.add(entry); + mAllFiles.blocks += entry.blocks; + + // mFilesByTag is used for trimming, so don't list empty files. + // (Zero-length/lost files are trimmed by date from mAllFiles.) + + if (entry.tag != null && entry.file != null && entry.blocks > 0) { + FileList tagFiles = mFilesByTag.get(entry.tag); + if (tagFiles == null) { + tagFiles = new FileList(); + mFilesByTag.put(entry.tag, tagFiles); + } + tagFiles.contents.add(entry); + tagFiles.blocks += entry.blocks; + } + } + + /** Moves a temporary file to a final log filename and enrolls it. */ + private synchronized void createEntry(File temp, String tag, int flags) throws IOException { + long t = System.currentTimeMillis(); + + // Require each entry to have a unique timestamp; if there are entries + // >10sec in the future (due to clock skew), drag them back to avoid + // keeping them around forever. + + SortedSet<EntryFile> tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000)); + EntryFile[] future = null; + if (!tail.isEmpty()) { + future = tail.toArray(new EntryFile[tail.size()]); + tail.clear(); // Remove from mAllFiles + } + + if (!mAllFiles.contents.isEmpty()) { + t = Math.max(t, mAllFiles.contents.last().timestampMillis + 1); + } + + if (future != null) { + for (EntryFile late : future) { + mAllFiles.blocks -= late.blocks; + FileList tagFiles = mFilesByTag.get(late.tag); + if (tagFiles.contents.remove(late)) tagFiles.blocks -= late.blocks; + if ((late.flags & DropBox.IS_EMPTY) == 0) { + enrollEntry(new EntryFile( + late.file, mDropBoxDir, late.tag, t++, late.flags, mBlockSize)); + } else { + enrollEntry(new EntryFile(mDropBoxDir, late.tag, t++)); + } + } + } + + if (temp == null) { + enrollEntry(new EntryFile(mDropBoxDir, tag, t)); + } else { + enrollEntry(new EntryFile(temp, mDropBoxDir, tag, t, flags, mBlockSize)); + } + } + + /** + * Trims the files on disk to make sure they aren't using too much space. + * @return the overall quota for storage (in bytes) + */ + private synchronized long trimToFit() { + // Expunge aged items (including tombstones marking deleted data). + + int ageSeconds = Settings.Gservices.getInt(mContentResolver, + Settings.Gservices.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS); + long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000; + while (!mAllFiles.contents.isEmpty()) { + EntryFile entry = mAllFiles.contents.first(); + if (entry.timestampMillis > cutoffMillis) break; + + FileList tag = mFilesByTag.get(entry.tag); + if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks; + if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks; + if (entry.file != null) entry.file.delete(); + } + + // Compute overall quota (a fraction of available free space) in blocks. + // The quota changes dynamically based on the amount of free space; + // that way when lots of data is available we can use it, but we'll get + // out of the way if storage starts getting tight. + + long uptimeMillis = SystemClock.uptimeMillis(); + if (uptimeMillis > mCachedQuotaUptimeMillis + QUOTA_RESCAN_MILLIS) { + int quotaPercent = Settings.Gservices.getInt(mContentResolver, + Settings.Gservices.DROPBOX_QUOTA_PERCENT, DEFAULT_QUOTA_PERCENT); + int reservePercent = Settings.Gservices.getInt(mContentResolver, + Settings.Gservices.DROPBOX_RESERVE_PERCENT, DEFAULT_RESERVE_PERCENT); + int quotaKb = Settings.Gservices.getInt(mContentResolver, + Settings.Gservices.DROPBOX_QUOTA_KB, DEFAULT_QUOTA_KB); + + mStatFs.restat(mDropBoxDir.getPath()); + int available = mStatFs.getAvailableBlocks(); + int nonreserved = available - mStatFs.getBlockCount() * reservePercent / 100; + int maximum = quotaKb * 1024 / mBlockSize; + mCachedQuotaBlocks = Math.min(maximum, Math.max(0, nonreserved * quotaPercent / 100)); + mCachedQuotaUptimeMillis = uptimeMillis; + } + + // If we're using too much space, delete old items to make room. + // + // We trim each tag independently (this is why we keep per-tag lists). + // Space is "fairly" shared between tags -- they are all squeezed + // equally until enough space is reclaimed. + // + // A single circular buffer (a la logcat) would be simpler, but this + // way we can handle fat/bursty data (like 1MB+ bugreports, 300KB+ + // kernel crash dumps, and 100KB+ ANR reports) without swamping small, + // well-behaved data // streams (event statistics, profile data, etc). + // + // Deleted files are replaced with zero-length tombstones to mark what + // was lost. Tombstones are expunged by age (see above). + + if (mAllFiles.blocks > mCachedQuotaBlocks) { + Log.i(TAG, "Usage (" + mAllFiles.blocks + ") > Quota (" + mCachedQuotaBlocks + ")"); + + // Find a fair share amount of space to limit each tag + int unsqueezed = mAllFiles.blocks, squeezed = 0; + TreeSet<FileList> tags = new TreeSet<FileList>(mFilesByTag.values()); + for (FileList tag : tags) { + if (squeezed > 0 && tag.blocks <= (mCachedQuotaBlocks - unsqueezed) / squeezed) { + break; + } + unsqueezed -= tag.blocks; + squeezed++; + } + int tagQuota = (mCachedQuotaBlocks - unsqueezed) / squeezed; + + // Remove old items from each tag until it meets the per-tag quota. + for (FileList tag : tags) { + if (mAllFiles.blocks < mCachedQuotaBlocks) break; + while (tag.blocks > tagQuota && !tag.contents.isEmpty()) { + EntryFile entry = tag.contents.first(); + if (tag.contents.remove(entry)) tag.blocks -= entry.blocks; + if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks; + + try { + if (entry.file != null) entry.file.delete(); + enrollEntry(new EntryFile(mDropBoxDir, entry.tag, entry.timestampMillis)); + } catch (IOException e) { + Log.e(TAG, "Can't write tombstone file", e); + } + } + } + } + + return mCachedQuotaBlocks * mBlockSize; + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b8cf844..5f30710 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -43,6 +43,7 @@ import android.util.EventLog; import android.util.Log; import android.accounts.AccountManagerService; +import java.io.File; import java.util.Timer; import java.util.TimerTask; @@ -294,6 +295,14 @@ class ServerThread extends Thread { } try { + Log.i(TAG, "DropBox Service"); + ServiceManager.addService(Context.DROPBOX_SERVICE, + new DropBoxService(context, new File("/data/system/dropbox"))); + } catch (Throwable e) { + Log.e(TAG, "Failure starting DropBox Service", e); + } + + try { Log.i(TAG, "Checkin Service"); Intent intent = new Intent().setComponent(new ComponentName( "com.google.android.server.checkin", diff --git a/services/java/com/android/server/WifiService.java b/services/java/com/android/server/WifiService.java index 32ad6c6..0ea832b 100644 --- a/services/java/com/android/server/WifiService.java +++ b/services/java/com/android/server/WifiService.java @@ -142,6 +142,7 @@ public class WifiService extends IWifiManager.Stub { private static final int MESSAGE_STOP_WIFI = 2; private static final int MESSAGE_START_WIFI = 3; private static final int MESSAGE_RELEASE_WAKELOCK = 4; + private static final int MESSAGE_UPDATE_STATE = 5; private final WifiHandler mWifiHandler; @@ -542,7 +543,7 @@ public class WifiService extends IWifiManager.Stub { value = WifiNative.getNetworkVariableCommand(netId, WifiConfiguration.ssidVarName); if (!TextUtils.isEmpty(value)) { - config.SSID = value; + config.SSID = removeDoubleQuotes(value); } else { config.SSID = null; } @@ -675,11 +676,21 @@ public class WifiService extends IWifiManager.Stub { value = WifiNative.getNetworkVariableCommand(netId, field.varName()); if (!TextUtils.isEmpty(value)) { + if (field != config.eap) value = removeDoubleQuotes(value); field.setValue(value); } } } + private static String removeDoubleQuotes(String string) { + if (string.length() <= 2) return ""; + return string.substring(1, string.length() - 1); + } + + private static String convertToQuotedString(String string) { + return "\"" + string + "\""; + } + /** * see {@link android.net.wifi.WifiManager#addOrUpdateNetwork(WifiConfiguration)} * @return the supplicant-assigned identifier for the new or updated @@ -731,7 +742,7 @@ public class WifiService extends IWifiManager.Stub { !WifiNative.setNetworkVariableCommand( netId, WifiConfiguration.ssidVarName, - config.SSID)) { + convertToQuotedString(config.SSID))) { if (DBG) { Log.d(TAG, "failed to set SSID: "+config.SSID); } @@ -894,18 +905,22 @@ public class WifiService extends IWifiManager.Stub { : config.enterpriseFields) { String varName = field.varName(); String value = field.value(); - if ((value != null) && !WifiNative.setNetworkVariableCommand( - netId, - varName, - value)) { - if (DBG) { - Log.d(TAG, config.SSID + ": failed to set " + varName + - ": " + value); + if (value != null) { + if (field != config.eap) { + value = convertToQuotedString(value); + } + if (!WifiNative.setNetworkVariableCommand( + netId, + varName, + value)) { + if (DBG) { + Log.d(TAG, config.SSID + ": failed to set " + varName + + ": " + value); + } + break setVariables; } - break setVariables; } } - return netId; } @@ -1432,6 +1447,11 @@ public class WifiService extends IWifiManager.Stub { } private void updateWifiState() { + // send a message so it's all serialized + Message.obtain(mWifiHandler, MESSAGE_UPDATE_STATE, 0, 0).sendToTarget(); + } + + private void doUpdateWifiState() { boolean wifiEnabled = getPersistedWifiEnabled(); boolean airplaneMode = isAirplaneModeOn() && !mAirplaneModeOverwridden; boolean lockHeld = mLocks.hasLocks(); @@ -1537,6 +1557,10 @@ public class WifiService extends IWifiManager.Stub { sWakeLock.release(); break; + case MESSAGE_UPDATE_STATE: + doUpdateWifiState(); + break; + case MESSAGE_DISABLE_WIFI: // a non-zero msg.arg1 value means the "enabled" setting // should be persisted @@ -1815,6 +1839,19 @@ public class WifiService extends IWifiManager.Stub { } } + public void initializeMulticastFiltering() { + enforceMulticastChangePermission(); + + synchronized (mMulticasters) { + // if anybody had requested filters be off, leave off + if (mMulticasters.size() != 0) { + return; + } else { + WifiNative.startPacketFiltering(); + } + } + } + public void acquireMulticastLock(IBinder binder, String tag) { enforceMulticastChangePermission(); diff --git a/services/java/com/android/server/WindowManagerService.java b/services/java/com/android/server/WindowManagerService.java index 69f4b89..55cf92d 100644 --- a/services/java/com/android/server/WindowManagerService.java +++ b/services/java/com/android/server/WindowManagerService.java @@ -9324,15 +9324,6 @@ public class WindowManagerService extends IWindowManager.Stub & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) { wallpaperMayChange = true; } - if (changed && !forceHiding - && (mCurrentFocus == null) - && (mFocusedApp != null)) { - // It's possible that the last focus recalculation left no - // current focused window even though the app has come to the - // foreground already. In this case, we make sure to recalculate - // focus when we show a window. - focusMayChange = true; - } } mPolicy.animatingWindowLw(w, attrs); diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java index 1aa1c76..586f63f 100644 --- a/telephony/java/android/telephony/PhoneNumberUtils.java +++ b/telephony/java/android/telephony/PhoneNumberUtils.java @@ -207,6 +207,42 @@ public class PhoneNumberUtils } /** + * Extracts the network address portion and canonicalize. + * + * This function is equivalent to extractNetworkPortion(), except + * for allowing the PLUS character to occur at arbitrary positions + * in the address portion, not just the first position. + * + * @hide + */ + public static String extractNetworkPortionAlt(String phoneNumber) { + if (phoneNumber == null) { + return null; + } + + int len = phoneNumber.length(); + StringBuilder ret = new StringBuilder(len); + boolean haveSeenPlus = false; + + for (int i = 0; i < len; i++) { + char c = phoneNumber.charAt(i); + if (c == '+') { + if (haveSeenPlus) { + continue; + } + haveSeenPlus = true; + } + if (isDialable(c)) { + ret.append(c); + } else if (isStartsPostDial (c)) { + break; + } + } + + return ret.toString(); + } + + /** * Strips separators from a phone number string. * @param phoneNumber phone number to strip. * @return phone string stripped of separators. @@ -342,6 +378,8 @@ public class PhoneNumberUtils compareLoosely(String a, String b) { int ia, ib; int matched; + int numNonDialableCharsInA = 0; + int numNonDialableCharsInB = 0; if (a == null || b == null) return a == b; @@ -362,6 +400,7 @@ public class PhoneNumberUtils if (!isDialable(ca)) { ia--; skipCmp = true; + numNonDialableCharsInA++; } cb = b.charAt(ib); @@ -369,6 +408,7 @@ public class PhoneNumberUtils if (!isDialable(cb)) { ib--; skipCmp = true; + numNonDialableCharsInB++; } if (!skipCmp) { @@ -380,13 +420,16 @@ public class PhoneNumberUtils } if (matched < MIN_MATCH) { - int aLen = a.length(); + int effectiveALen = a.length() - numNonDialableCharsInA; + int effectiveBLen = b.length() - numNonDialableCharsInB; - // if the input strings match, but their lengths < MIN_MATCH, - // treat them as equal. - if (aLen == b.length() && aLen == matched) { + + // if the number of dialable chars in a and b match, but the matched chars < MIN_MATCH, + // treat them as equal (i.e. 404-04 and 40404) + if (effectiveALen == effectiveBLen && effectiveALen == matched) { return true; } + return false; } @@ -590,7 +633,7 @@ public class PhoneNumberUtils */ public static String toCallerIDMinMatch(String phoneNumber) { - String np = extractNetworkPortion(phoneNumber); + String np = extractNetworkPortionAlt(phoneNumber); return internalGetStrippedReversed(np, MIN_MATCH); } @@ -603,7 +646,7 @@ public class PhoneNumberUtils */ public static String getStrippedReversed(String phoneNumber) { - String np = extractNetworkPortion(phoneNumber); + String np = extractNetworkPortionAlt(phoneNumber); if (np == null) return null; @@ -1017,8 +1060,8 @@ public class PhoneNumberUtils * Breaks the given number down and formats it according to the rules * for the country the number is from. * - * @param source the phone number to format - * @return a locally acceptable formatting of the input, or the raw input if + * @param source The phone number to format + * @return A locally acceptable formatting of the input, or the raw input if * formatting rules aren't known for the number */ public static String formatNumber(String source) { @@ -1028,10 +1071,27 @@ public class PhoneNumberUtils } /** + * Formats the given number with the given formatting type. Currently + * {@link #FORMAT_NANP} and {@link #FORMAT_JAPAN} are supported as a formating type. + * + * @param source the phone number to format + * @param defaultFormattingType The default formatting rules to apply if the number does + * not begin with +<country_code> + * @return The phone number formatted with the given formatting type. + * + * @hide TODO:Shuold be unhidden. + */ + public static String formatNumber(String source, int defaultFormattingType) { + SpannableStringBuilder text = new SpannableStringBuilder(source); + formatNumber(text, defaultFormattingType); + return text.toString(); + } + + /** * Returns the phone number formatting type for the given locale. * * @param locale The locale of interest, usually {@link Locale#getDefault()} - * @return the formatting type for the given locale, or FORMAT_UNKNOWN if the formatting + * @return The formatting type for the given locale, or FORMAT_UNKNOWN if the formatting * rules are not known for the given locale */ public static int getFormatTypeForLocale(Locale locale) { @@ -1041,7 +1101,8 @@ public class PhoneNumberUtils } /** - * Formats a phone number in-place. Currently only supports NANP formatting. + * Formats a phone number in-place. Currently {@link #FORMAT_JAPAN} and {@link #FORMAT_NANP} + * is supported as a second argument. * * @param text The number to be formatted, will be modified with the formatting * @param defaultFormattingType The default formatting rules to apply if the number does @@ -1247,7 +1308,7 @@ public class PhoneNumberUtils // Strip the separators from the number before comparing it // to the list. - number = extractNetworkPortion(number); + number = extractNetworkPortionAlt(number); // retrieve the list of emergency numbers String numbers = SystemProperties.get("ro.ril.ecclist"); @@ -1290,7 +1351,7 @@ public class PhoneNumberUtils // Strip the separators from the number before comparing it // to the list. - number = extractNetworkPortion(number); + number = extractNetworkPortionAlt(number); // compare tolerates null so we need to make sure that we // don't return true when both are null. diff --git a/telephony/java/com/android/internal/telephony/DriverCall.java b/telephony/java/com/android/internal/telephony/DriverCall.java index 0d9a60f..66f6b9c 100644 --- a/telephony/java/com/android/internal/telephony/DriverCall.java +++ b/telephony/java/com/android/internal/telephony/DriverCall.java @@ -73,8 +73,7 @@ public class DriverCall implements Comparable { if (p.hasMore()) { // Some lame implementations return strings // like "NOT AVAILABLE" in the CLCC line - ret.number = PhoneNumberUtils.extractNetworkPortion( - p.nextString()); + ret.number = PhoneNumberUtils.extractNetworkPortionAlt(p.nextString()); if (ret.number.length() == 0) { ret.number = null; diff --git a/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java b/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java index 3548cad..04a03b2 100755 --- a/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java +++ b/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java @@ -261,12 +261,6 @@ public class CDMAPhone extends PhoneBase { } public ServiceState getServiceState() { - int roamInd = mSST.ss.getCdmaRoamingIndicator(); - int defRoamInd = mSST.ss.getCdmaDefaultRoamingIndicator(); - - mSST.ss.setCdmaEriIconIndex(mEriManager.getCdmaEriIconIndex(roamInd, defRoamInd)); - mSST.ss.setCdmaEriIconMode(mEriManager.getCdmaEriIconMode(roamInd, defRoamInd)); - return mSST.ss; } @@ -808,10 +802,6 @@ public class CDMAPhone extends PhoneBase { } void notifyServiceStateChanged(ServiceState ss) { - // TODO this seems really inefficient. Can't we calc this when the fundamentals change and store in the - // service state? - ss.setCdmaEriIconIndex(this.getCdmaEriIconIndex()); - ss.setCdmaEriIconMode(this.getCdmaEriIconMode()); super.notifyServiceStateChangedP(ss); } @@ -1341,7 +1331,7 @@ public class CDMAPhone extends PhoneBase { @Override public boolean isOtaSpNumber(String dialStr){ boolean isOtaSpNum = false; - String dialableStr = PhoneNumberUtils.extractNetworkPortion(dialStr); + String dialableStr = PhoneNumberUtils.extractNetworkPortionAlt(dialStr); if (dialableStr != null) { isOtaSpNum = isIs683OtaSpDialStr(dialableStr); if (isOtaSpNum == false) { @@ -1354,9 +1344,7 @@ public class CDMAPhone extends PhoneBase { @Override public int getCdmaEriIconIndex() { - int roamInd = getServiceState().getCdmaRoamingIndicator(); - int defRoamInd = getServiceState().getCdmaDefaultRoamingIndicator(); - return mEriManager.getCdmaEriIconIndex(roamInd, defRoamInd); + return getServiceState().getCdmaEriIconIndex(); } /** @@ -1366,9 +1354,7 @@ public class CDMAPhone extends PhoneBase { */ @Override public int getCdmaEriIconMode() { - int roamInd = getServiceState().getCdmaRoamingIndicator(); - int defRoamInd = getServiceState().getCdmaDefaultRoamingIndicator(); - return mEriManager.getCdmaEriIconMode(roamInd, defRoamInd); + return getServiceState().getCdmaEriIconMode(); } /** diff --git a/telephony/java/com/android/internal/telephony/cdma/CdmaConnection.java b/telephony/java/com/android/internal/telephony/cdma/CdmaConnection.java index f637d33..08946d2 100755 --- a/telephony/java/com/android/internal/telephony/cdma/CdmaConnection.java +++ b/telephony/java/com/android/internal/telephony/cdma/CdmaConnection.java @@ -154,7 +154,7 @@ public class CdmaConnection extends Connection { dialString = formatDialString(dialString); Log.d(LOG_TAG, "[CDMAConn] CdmaConnection:formated dialString=" + dialString); - this.address = PhoneNumberUtils.extractNetworkPortion(dialString); + this.address = PhoneNumberUtils.extractNetworkPortionAlt(dialString); this.postDialString = PhoneNumberUtils.extractPostDialPortion(dialString); index = -1; diff --git a/telephony/java/com/android/internal/telephony/cdma/CdmaServiceStateTracker.java b/telephony/java/com/android/internal/telephony/cdma/CdmaServiceStateTracker.java index 3051e2f..03acf5c 100644 --- a/telephony/java/com/android/internal/telephony/cdma/CdmaServiceStateTracker.java +++ b/telephony/java/com/android/internal/telephony/cdma/CdmaServiceStateTracker.java @@ -811,6 +811,12 @@ final class CdmaServiceStateTracker extends ServiceStateTracker { } } + int roamingIndicator = newSS.getCdmaRoamingIndicator(); + newSS.setCdmaEriIconIndex(phone.mEriManager.getCdmaEriIconIndex(roamingIndicator, + mDefaultRoamingIndicator)); + newSS.setCdmaEriIconMode(phone.mEriManager.getCdmaEriIconMode(roamingIndicator, + mDefaultRoamingIndicator)); + // NOTE: Some operator may require overriding mCdmaRoaming // (set by the modem), depending on the mRoamingIndicator. diff --git a/telephony/java/com/android/internal/telephony/gsm/GSMPhone.java b/telephony/java/com/android/internal/telephony/gsm/GSMPhone.java index 5614c12..56499a8 100755 --- a/telephony/java/com/android/internal/telephony/gsm/GSMPhone.java +++ b/telephony/java/com/android/internal/telephony/gsm/GSMPhone.java @@ -729,7 +729,7 @@ public class GSMPhone extends PhoneBase { } // Only look at the Network portion for mmi - String networkPortion = PhoneNumberUtils.extractNetworkPortion(newDialString); + String networkPortion = PhoneNumberUtils.extractNetworkPortionAlt(newDialString); GsmMmiCode mmi = GsmMmiCode.newFromDialString(networkPortion, this); if (LOCAL_DEBUG) Log.d(LOG_TAG, "dialing w/ mmi '" + mmi + "'..."); diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmConnection.java b/telephony/java/com/android/internal/telephony/gsm/GsmConnection.java index 445be39..4788a01 100644 --- a/telephony/java/com/android/internal/telephony/gsm/GsmConnection.java +++ b/telephony/java/com/android/internal/telephony/gsm/GsmConnection.java @@ -144,7 +144,7 @@ public class GsmConnection extends Connection { this.dialString = dialString; - this.address = PhoneNumberUtils.extractNetworkPortion(dialString); + this.address = PhoneNumberUtils.extractNetworkPortionAlt(dialString); this.postDialString = PhoneNumberUtils.extractPostDialPortion(dialString); index = -1; diff --git a/test-runner/android/test/mock/MockCursor.java b/test-runner/android/test/mock/MockCursor.java new file mode 100644 index 0000000..9b1c0ef --- /dev/null +++ b/test-runner/android/test/mock/MockCursor.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.test.mock; + +import android.content.ContentResolver; +import android.database.CharArrayBuffer; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.net.Uri; +import android.os.Bundle; + +import java.util.Map; + +/** + * <P> + * A mock {@link android.database.Cursor} class that isolates the test code from real + * Cursor implementation. + * </P> + * <P> + * All methods including ones related to querying the state of the cursor are + * are non-functional and throw {@link java.lang.UnsupportedOperationException}. + * </P> + */ +public class MockCursor implements Cursor { + public int getColumnCount() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public int getColumnIndex(String columnName) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public int getColumnIndexOrThrow(String columnName) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public String getColumnName(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public String[] getColumnNames() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public int getCount() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean isNull(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public int getInt(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public long getLong(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public short getShort(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public float getFloat(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public double getDouble(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public byte[] getBlob(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public String getString(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public Bundle getExtras() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public int getPosition() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean isAfterLast() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean isBeforeFirst() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean isFirst() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean isLast() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean move(int offset) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean moveToFirst() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean moveToLast() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean moveToNext() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean moveToPrevious() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean moveToPosition(int position) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public void deactivate() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public void close() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean isClosed() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean requery() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public void registerContentObserver(ContentObserver observer) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public void registerDataSetObserver(DataSetObserver observer) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public Bundle respond(Bundle extras) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public boolean getWantsAllOnMoveCalls() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean commitUpdates() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean commitUpdates(Map<? extends Long, ? extends Map<String, Object>> values) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean hasUpdates() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public void setNotificationUri(ContentResolver cr, Uri uri) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean supportsUpdates() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean deleteRow() { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public void unregisterContentObserver(ContentObserver observer) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public void unregisterDataSetObserver(DataSetObserver observer) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateBlob(int columnIndex, byte[] value) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateDouble(int columnIndex, double value) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateFloat(int columnIndex, float value) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateInt(int columnIndex, int value) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateLong(int columnIndex, long value) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateShort(int columnIndex, short value) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateString(int columnIndex, String value) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public boolean updateToNull(int columnIndex) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("deprecation") + public void abortUpdates() { + throw new UnsupportedOperationException("unimplemented mock method"); + } +}
\ No newline at end of file diff --git a/tests/AndroidTests/Android.mk b/tests/AndroidTests/Android.mk index ced796a..757044f 100644 --- a/tests/AndroidTests/Android.mk +++ b/tests/AndroidTests/Android.mk @@ -3,7 +3,7 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := tests -LOCAL_JAVA_LIBRARIES := framework-tests android.test.runner +LOCAL_JAVA_LIBRARIES := framework-tests android.test.runner services LOCAL_STATIC_JAVA_LIBRARIES := googlelogin-client diff --git a/tests/AndroidTests/AndroidManifest.xml b/tests/AndroidTests/AndroidManifest.xml index d94327a..786178c 100644 --- a/tests/AndroidTests/AndroidManifest.xml +++ b/tests/AndroidTests/AndroidManifest.xml @@ -35,27 +35,27 @@ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BROADCAST_STICKY" /> - <uses-permission android:name="android.permission.READ_CONTACTS" /> - <uses-permission android:name="android.permission.WRITE_CONTACTS" /> - <uses-permission android:name="android.permission.WRITE_SETTINGS" /> - <uses-permission android:name="android.permission.READ_SMS"/> - <uses-permission android:name="android.permission.WRITE_SMS"/> - <uses-permission android:name="android.permission.DELETE_CACHE_FILES" /> <uses-permission android:name="android.permission.CLEAR_APP_CACHE" /> <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" /> + <uses-permission android:name="android.permission.DELETE_CACHE_FILES" /> <uses-permission android:name="android.permission.GET_PACKAGE_SIZE" /> - <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> - <uses-permission android:name="android.permission.WRITE_GSERVICES" /> + <uses-permission android:name="android.permission.READ_CONTACTS" /> + <uses-permission android:name="android.permission.READ_LOGS"/> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.READ_SMS"/> + <uses-permission android:name="android.permission.USE_CREDENTIALS" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - + <uses-permission android:name="android.permission.WRITE_GSERVICES" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + <uses-permission android:name="android.permission.WRITE_SMS"/> <uses-permission android:name="com.android.unit_tests.permission.TEST_GRANTED" /> - - <uses-permission android:name="android.permission.USE_CREDENTIALS" /> + <uses-permission android:name="com.google.android.googleapps.permission.ACCESS_GOOGLE_PASSWORD" /> <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" /> <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.ALL_SERVICES" /> - <uses-permission android:name="com.google.android.googleapps.permission.ACCESS_GOOGLE_PASSWORD" /> + <!-- InstrumentationTestRunner for AndroidTests --> <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.android.unit_tests" diff --git a/tests/AndroidTests/res/raw/v21_org_before_title.vcf b/tests/AndroidTests/res/raw/v21_org_before_title.vcf new file mode 100644 index 0000000..8ff1190 --- /dev/null +++ b/tests/AndroidTests/res/raw/v21_org_before_title.vcf @@ -0,0 +1,6 @@ +BEGIN:VCARD
+VERSION:2.1
+FN:Normal Guy
+ORG:Company;Organization;Devision;Room;Sheet No.
+TITLE:Excellent Janitor
+END:VCARD
diff --git a/tests/AndroidTests/res/raw/v21_pref_handling.vcf b/tests/AndroidTests/res/raw/v21_pref_handling.vcf new file mode 100644 index 0000000..5105310 --- /dev/null +++ b/tests/AndroidTests/res/raw/v21_pref_handling.vcf @@ -0,0 +1,15 @@ +BEGIN:VCARD +VERSION:2.1 +FN:Smith +TEL;HOME:1 +TEL;WORK;PREF:2 +TEL;ISDN:3 +EMAIL;PREF;HOME:test@example.com +EMAIL;CELL;PREF:test2@examination.com +ORG:Company +TITLE:Engineer +ORG:Mystery +TITLE:Blogger +ORG:Poetry +TITLE:Poet +END:VCARD diff --git a/tests/AndroidTests/res/raw/v21_title_before_org.vcf b/tests/AndroidTests/res/raw/v21_title_before_org.vcf new file mode 100644 index 0000000..9fdc738 --- /dev/null +++ b/tests/AndroidTests/res/raw/v21_title_before_org.vcf @@ -0,0 +1,6 @@ +BEGIN:VCARD
+VERSION:2.1
+FN:Nice Guy
+TITLE:Cool Title
+ORG:Marverous;Perfect;Great;Good;Bad;Poor
+END:VCARD
diff --git a/tests/AndroidTests/res/raw/v21_winmo_65.vcf b/tests/AndroidTests/res/raw/v21_winmo_65.vcf new file mode 100644 index 0000000..f380d0d --- /dev/null +++ b/tests/AndroidTests/res/raw/v21_winmo_65.vcf @@ -0,0 +1,10 @@ +BEGIN:VCARD
+VERSION:2.1
+N:Example;;;;
+FN:Example
+ANNIVERSARY;VALUE=DATE:20091010
+AGENT:Invalid line which must be handled correctly.
+X-CLASS:PUBLIC
+X-REDUCTION:
+X-NO:
+END:VCARD
diff --git a/tests/AndroidTests/res/raw/v30_comma_separated.vcf b/tests/AndroidTests/res/raw/v30_comma_separated.vcf new file mode 100644 index 0000000..98a7f20 --- /dev/null +++ b/tests/AndroidTests/res/raw/v30_comma_separated.vcf @@ -0,0 +1,5 @@ +BEGIN:VCARD
+VERSION:3.0
+N:F;G;M;;
+TEL;TYPE=PAGER,WORK,MSG:6101231234@pagersample.com
+END:VCARD
diff --git a/tests/AndroidTests/src/com/android/unit_tests/DropBoxTest.java b/tests/AndroidTests/src/com/android/unit_tests/DropBoxTest.java new file mode 100644 index 0000000..286f702 --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/DropBoxTest.java @@ -0,0 +1,540 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.DropBox; +import android.os.ParcelFileDescriptor; +import android.os.ServiceManager; +import android.os.StatFs; +import android.provider.Settings; +import android.test.AndroidTestCase; + +import com.android.server.DropBoxService; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.InputStream; +import java.util.Random; +import java.util.zip.GZIPOutputStream; + +/** Test {@link DropBox} functionality. */ +public class DropBoxTest extends AndroidTestCase { + public void tearDown() throws Exception { + Intent override = new Intent(Settings.Gservices.OVERRIDE_ACTION); + override.putExtra(Settings.Gservices.DROPBOX_AGE_SECONDS, ""); + override.putExtra(Settings.Gservices.DROPBOX_QUOTA_KB, ""); + override.putExtra(Settings.Gservices.DROPBOX_TAG_PREFIX + "DropBoxTest", ""); + waitForBroadcast(override); + } + + public void testAddText() throws Exception { + DropBox dropbox = (DropBox) getContext().getSystemService(Context.DROPBOX_SERVICE); + long before = System.currentTimeMillis(); + Thread.sleep(5); + dropbox.addText("DropBoxTest", "TEST0"); + Thread.sleep(5); + long between = System.currentTimeMillis(); + Thread.sleep(5); + dropbox.addText("DropBoxTest", "TEST1"); + dropbox.addText("DropBoxTest", "TEST2"); + Thread.sleep(5); + long after = System.currentTimeMillis(); + + DropBox.Entry e0 = dropbox.getNextEntry("DropBoxTest", before); + DropBox.Entry e1 = dropbox.getNextEntry("DropBoxTest", e0.getTimeMillis()); + DropBox.Entry e2 = dropbox.getNextEntry("DropBoxTest", e1.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry("DropBoxTest", e2.getTimeMillis())); + + assertTrue(e0.getTimeMillis() > before); + assertTrue(e0.getTimeMillis() < between); + assertTrue(e1.getTimeMillis() > between); + assertTrue(e1.getTimeMillis() < e2.getTimeMillis()); + assertTrue(e2.getTimeMillis() < after); + + assertEquals("TEST0", e0.getText(80)); + assertEquals("TEST1", e1.getText(80)); + assertEquals("TES", e2.getText(3)); + + e0.close(); + e1.close(); + e2.close(); + } + + public void testAddData() throws Exception { + DropBox dropbox = (DropBox) getContext().getSystemService(Context.DROPBOX_SERVICE); + long before = System.currentTimeMillis(); + dropbox.addData("DropBoxTest", "TEST".getBytes(), 0); + long after = System.currentTimeMillis(); + + DropBox.Entry e = dropbox.getNextEntry("DropBoxTest", before); + assertTrue(null == dropbox.getNextEntry("DropBoxTest", e.getTimeMillis())); + + assertEquals("DropBoxTest", e.getTag()); + assertTrue(e.getTimeMillis() >= before); + assertEquals(0, e.getFlags()); + assertTrue(null == e.getText(80)); + + byte[] buf = new byte[80]; + assertEquals("TEST", new String(buf, 0, e.getInputStream().read(buf))); + + e.close(); + } + + public void testAddFile() throws Exception { + File dir = getEmptyDir("testAddFile"); + long before = System.currentTimeMillis(); + + File f0 = new File(dir, "f0.txt"); + File f1 = new File(dir, "f1.txt.gz"); + File f2 = new File(dir, "f2.dat"); + File f3 = new File(dir, "f2.dat.gz"); + + FileWriter w0 = new FileWriter(f0); + GZIPOutputStream gz1 = new GZIPOutputStream(new FileOutputStream(f1)); + FileOutputStream os2 = new FileOutputStream(f2); + GZIPOutputStream gz3 = new GZIPOutputStream(new FileOutputStream(f3)); + + w0.write("FILE0"); + gz1.write("FILE1".getBytes()); + os2.write("DATA2".getBytes()); + gz3.write("DATA3".getBytes()); + + w0.close(); + gz1.close(); + os2.close(); + gz3.close(); + + DropBox dropbox = (DropBox) getContext().getSystemService(Context.DROPBOX_SERVICE); + int mode = ParcelFileDescriptor.MODE_READ_ONLY; + + ParcelFileDescriptor pfd0 = ParcelFileDescriptor.open(f0, mode); + ParcelFileDescriptor pfd1 = ParcelFileDescriptor.open(f1, mode); + ParcelFileDescriptor pfd2 = ParcelFileDescriptor.open(f2, mode); + ParcelFileDescriptor pfd3 = ParcelFileDescriptor.open(f3, mode); + + dropbox.addFile("DropBoxTest", pfd0, DropBox.IS_TEXT); + dropbox.addFile("DropBoxTest", pfd1, DropBox.IS_TEXT | DropBox.IS_GZIPPED); + dropbox.addFile("DropBoxTest", pfd2, 0); + dropbox.addFile("DropBoxTest", pfd3, DropBox.IS_GZIPPED); + + pfd0.close(); + pfd1.close(); + pfd2.close(); + pfd3.close(); + + DropBox.Entry e0 = dropbox.getNextEntry("DropBoxTest", before); + DropBox.Entry e1 = dropbox.getNextEntry("DropBoxTest", e0.getTimeMillis()); + DropBox.Entry e2 = dropbox.getNextEntry("DropBoxTest", e1.getTimeMillis()); + DropBox.Entry e3 = dropbox.getNextEntry("DropBoxTest", e2.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry("DropBoxTest", e3.getTimeMillis())); + + assertTrue(e0.getTimeMillis() > before); + assertTrue(e1.getTimeMillis() > e0.getTimeMillis()); + assertTrue(e2.getTimeMillis() > e1.getTimeMillis()); + assertTrue(e3.getTimeMillis() > e2.getTimeMillis()); + + assertEquals(DropBox.IS_TEXT, e0.getFlags()); + assertEquals(DropBox.IS_TEXT, e1.getFlags()); + assertEquals(0, e2.getFlags()); + assertEquals(0, e3.getFlags()); + + assertEquals("FILE0", e0.getText(80)); + + byte[] buf1 = new byte[80]; + assertEquals("FILE1", new String(buf1, 0, e1.getInputStream().read(buf1))); + + assertTrue(null == e2.getText(80)); + byte[] buf2 = new byte[80]; + assertEquals("DATA2", new String(buf2, 0, e2.getInputStream().read(buf2))); + + assertTrue(null == e3.getText(80)); + byte[] buf3 = new byte[80]; + assertEquals("DATA3", new String(buf3, 0, e3.getInputStream().read(buf3))); + + e0.close(); + e1.close(); + e2.close(); + e3.close(); + } + + public void testAddEntriesInTheFuture() throws Exception { + File dir = getEmptyDir("testAddEntriesInTheFuture"); + long before = System.currentTimeMillis(); + + // Near future: should be allowed to persist + FileWriter w0 = new FileWriter(new File(dir, "DropBoxTest@" + (before + 5000) + ".txt")); + w0.write("FUTURE0"); + w0.close(); + + // Far future: should be collapsed + FileWriter w1 = new FileWriter(new File(dir, "DropBoxTest@" + (before + 100000) + ".txt")); + w1.write("FUTURE1"); + w1.close(); + + // Another far future item, this one gzipped + File f2 = new File(dir, "DropBoxTest@" + (before + 100001) + ".txt.gz"); + GZIPOutputStream gz2 = new GZIPOutputStream(new FileOutputStream(f2)); + gz2.write("FUTURE2".getBytes()); + gz2.close(); + + // Tombstone in the far future + new FileOutputStream(new File(dir, "DropBoxTest@" + (before + 100002) + ".lost")).close(); + + DropBoxService service = new DropBoxService(getContext(), dir); + DropBox dropbox = new DropBox(service); + + // Until a write, the timestamps are taken at face value + DropBox.Entry e0 = dropbox.getNextEntry(null, before); + DropBox.Entry e1 = dropbox.getNextEntry(null, e0.getTimeMillis()); + DropBox.Entry e2 = dropbox.getNextEntry(null, e1.getTimeMillis()); + DropBox.Entry e3 = dropbox.getNextEntry(null, e2.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry(null, e3.getTimeMillis())); + + assertEquals("FUTURE0", e0.getText(80)); + assertEquals("FUTURE1", e1.getText(80)); + assertEquals("FUTURE2", e2.getText(80)); + assertEquals(null, e3.getText(80)); + + assertEquals(before + 5000, e0.getTimeMillis()); + assertEquals(before + 100000, e1.getTimeMillis()); + assertEquals(before + 100001, e2.getTimeMillis()); + assertEquals(before + 100002, e3.getTimeMillis()); + + e0.close(); + e1.close(); + e2.close(); + e3.close(); + + // Write something to force a collapse + dropbox.addText("NotDropBoxTest", "FUTURE"); + e0 = dropbox.getNextEntry(null, before); + e1 = dropbox.getNextEntry(null, e0.getTimeMillis()); + e2 = dropbox.getNextEntry(null, e1.getTimeMillis()); + e3 = dropbox.getNextEntry(null, e2.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry("DropBoxTest", e3.getTimeMillis())); + + assertEquals("FUTURE0", e0.getText(80)); + assertEquals("FUTURE1", e1.getText(80)); + assertEquals("FUTURE2", e2.getText(80)); + assertEquals(null, e3.getText(80)); + + assertEquals(before + 5000, e0.getTimeMillis()); + assertEquals(before + 5001, e1.getTimeMillis()); + assertEquals(before + 5002, e2.getTimeMillis()); + assertEquals(before + 5003, e3.getTimeMillis()); + + e0.close(); + e1.close(); + e2.close(); + e3.close(); + service.stop(); + } + + public void testIsTagEnabled() throws Exception { + DropBox dropbox = (DropBox) getContext().getSystemService(Context.DROPBOX_SERVICE); + long before = System.currentTimeMillis(); + dropbox.addText("DropBoxTest", "TEST-ENABLED"); + assertTrue(dropbox.isTagEnabled("DropBoxTest")); + + Intent override = new Intent(Settings.Gservices.OVERRIDE_ACTION); + override.putExtra(Settings.Gservices.DROPBOX_TAG_PREFIX + "DropBoxTest", "disabled"); + waitForBroadcast(override); + + dropbox.addText("DropBoxTest", "TEST-DISABLED"); + assertFalse(dropbox.isTagEnabled("DropBoxTest")); + + override = new Intent(Settings.Gservices.OVERRIDE_ACTION); + override.putExtra(Settings.Gservices.DROPBOX_TAG_PREFIX + "DropBoxTest", ""); + waitForBroadcast(override); + + dropbox.addText("DropBoxTest", "TEST-ENABLED-AGAIN"); + assertTrue(dropbox.isTagEnabled("DropBoxTest")); + + DropBox.Entry e0 = dropbox.getNextEntry("DropBoxTest", before); + DropBox.Entry e1 = dropbox.getNextEntry("DropBoxTest", e0.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry("DropBoxTest", e1.getTimeMillis())); + + assertEquals("TEST-ENABLED", e0.getText(80)); + assertEquals("TEST-ENABLED-AGAIN", e1.getText(80)); + + e0.close(); + e1.close(); + } + + public void testGetNextEntry() throws Exception { + File dir = getEmptyDir("testGetNextEntry"); + DropBoxService service = new DropBoxService(getContext(), dir); + DropBox dropbox = new DropBox(service); + + long before = System.currentTimeMillis(); + dropbox.addText("DropBoxTest.A", "A0"); + dropbox.addText("DropBoxTest.B", "B0"); + dropbox.addText("DropBoxTest.A", "A1"); + + DropBox.Entry a0 = dropbox.getNextEntry("DropBoxTest.A", before); + DropBox.Entry a1 = dropbox.getNextEntry("DropBoxTest.A", a0.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry("DropBoxTest.A", a1.getTimeMillis())); + + DropBox.Entry b0 = dropbox.getNextEntry("DropBoxTest.B", before); + assertTrue(null == dropbox.getNextEntry("DropBoxTest.B", b0.getTimeMillis())); + + DropBox.Entry x0 = dropbox.getNextEntry(null, before); + DropBox.Entry x1 = dropbox.getNextEntry(null, x0.getTimeMillis()); + DropBox.Entry x2 = dropbox.getNextEntry(null, x1.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry(null, x2.getTimeMillis())); + + assertEquals("DropBoxTest.A", a0.getTag()); + assertEquals("DropBoxTest.A", a1.getTag()); + assertEquals("A0", a0.getText(80)); + assertEquals("A1", a1.getText(80)); + + assertEquals("DropBoxTest.B", b0.getTag()); + assertEquals("B0", b0.getText(80)); + + assertEquals("DropBoxTest.A", x0.getTag()); + assertEquals("DropBoxTest.B", x1.getTag()); + assertEquals("DropBoxTest.A", x2.getTag()); + assertEquals("A0", x0.getText(80)); + assertEquals("B0", x1.getText(80)); + assertEquals("A1", x2.getText(80)); + + a0.close(); + a1.close(); + b0.close(); + x0.close(); + x1.close(); + x2.close(); + service.stop(); + } + + public void testSizeLimits() throws Exception { + File dir = getEmptyDir("testSizeLimits"); + int blockSize = new StatFs(dir.getPath()).getBlockSize(); + + // Limit storage to 10 blocks + int kb = blockSize * 10 / 1024; + Intent override = new Intent(Settings.Gservices.OVERRIDE_ACTION); + override.putExtra(Settings.Gservices.DROPBOX_QUOTA_KB, Integer.toString(kb)); + waitForBroadcast(override); + + // Three tags using a total of 12 blocks: + // DropBoxTest0 [ ][ ] + // DropBoxTest1 [x][ ][ ][ ][xxx(20 blocks)xxx] + // DropBoxTest2 [xxxxxxxxxx][ ][ ] + // + // The blocks marked "x" will be removed due to storage restrictions. + // Use random fill (so it doesn't compress), subtract a little for gzip overhead + + final int overhead = 64; + long before = System.currentTimeMillis(); + DropBoxService service = new DropBoxService(getContext(), dir); + DropBox dropbox = new DropBox(service); + + addRandomEntry(dropbox, "DropBoxTest0", blockSize - overhead); + addRandomEntry(dropbox, "DropBoxTest0", blockSize - overhead); + + addRandomEntry(dropbox, "DropBoxTest1", blockSize - overhead); + addRandomEntry(dropbox, "DropBoxTest1", blockSize - overhead); + addRandomEntry(dropbox, "DropBoxTest1", blockSize * 2 - overhead); + addRandomEntry(dropbox, "DropBoxTest1", blockSize - overhead); + addRandomEntry(dropbox, "DropBoxTest1", blockSize * 20 - overhead); + + addRandomEntry(dropbox, "DropBoxTest2", blockSize * 4 - overhead); + addRandomEntry(dropbox, "DropBoxTest2", blockSize - overhead); + addRandomEntry(dropbox, "DropBoxTest2", blockSize - overhead); + + DropBox.Entry e0 = dropbox.getNextEntry(null, before); + DropBox.Entry e1 = dropbox.getNextEntry(null, e0.getTimeMillis()); + DropBox.Entry e2 = dropbox.getNextEntry(null, e1.getTimeMillis()); + DropBox.Entry e3 = dropbox.getNextEntry(null, e2.getTimeMillis()); + DropBox.Entry e4 = dropbox.getNextEntry(null, e3.getTimeMillis()); + DropBox.Entry e5 = dropbox.getNextEntry(null, e4.getTimeMillis()); + DropBox.Entry e6 = dropbox.getNextEntry(null, e5.getTimeMillis()); + DropBox.Entry e7 = dropbox.getNextEntry(null, e6.getTimeMillis()); + DropBox.Entry e8 = dropbox.getNextEntry(null, e7.getTimeMillis()); + DropBox.Entry e9 = dropbox.getNextEntry(null, e8.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry(null, e9.getTimeMillis())); + + assertEquals("DropBoxTest0", e0.getTag()); + assertEquals("DropBoxTest0", e1.getTag()); + assertEquals(blockSize - overhead, getEntrySize(e0)); + assertEquals(blockSize - overhead, getEntrySize(e1)); + + assertEquals("DropBoxTest1", e2.getTag()); + assertEquals("DropBoxTest1", e3.getTag()); + assertEquals("DropBoxTest1", e4.getTag()); + assertEquals("DropBoxTest1", e5.getTag()); + assertEquals("DropBoxTest1", e6.getTag()); + assertEquals(-1, getEntrySize(e2)); // Tombstone + assertEquals(blockSize - overhead, getEntrySize(e3)); + assertEquals(blockSize * 2 - overhead, getEntrySize(e4)); + assertEquals(blockSize - overhead, getEntrySize(e5)); + assertEquals(-1, getEntrySize(e6)); + + assertEquals("DropBoxTest2", e7.getTag()); + assertEquals("DropBoxTest2", e8.getTag()); + assertEquals("DropBoxTest2", e9.getTag()); + assertEquals(-1, getEntrySize(e7)); // Tombstone + assertEquals(blockSize - overhead, getEntrySize(e8)); + assertEquals(blockSize - overhead, getEntrySize(e9)); + + e0.close(); + e1.close(); + e2.close(); + e3.close(); + e4.close(); + e5.close(); + e6.close(); + e7.close(); + e8.close(); + e9.close(); + + // Specifying a tag name skips tombstone records. + + DropBox.Entry t0 = dropbox.getNextEntry("DropBoxTest1", before); + DropBox.Entry t1 = dropbox.getNextEntry("DropBoxTest1", t0.getTimeMillis()); + DropBox.Entry t2 = dropbox.getNextEntry("DropBoxTest1", t1.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry("DropBoxTest1", t2.getTimeMillis())); + + assertEquals("DropBoxTest1", t0.getTag()); + assertEquals("DropBoxTest1", t1.getTag()); + assertEquals("DropBoxTest1", t2.getTag()); + + assertEquals(blockSize - overhead, getEntrySize(t0)); + assertEquals(blockSize * 2 - overhead, getEntrySize(t1)); + assertEquals(blockSize - overhead, getEntrySize(t2)); + + t0.close(); + t1.close(); + t2.close(); + service.stop(); + } + + public void testAgeLimits() throws Exception { + File dir = getEmptyDir("testAgeLimits"); + int blockSize = new StatFs(dir.getPath()).getBlockSize(); + + // Limit storage to 10 blocks with an expiration of 1 second + int kb = blockSize * 10 / 1024; + Intent override = new Intent(Settings.Gservices.OVERRIDE_ACTION); + override.putExtra(Settings.Gservices.DROPBOX_AGE_SECONDS, "1"); + override.putExtra(Settings.Gservices.DROPBOX_QUOTA_KB, Integer.toString(kb)); + waitForBroadcast(override); + + // Write one normal entry and another so big that it is instantly tombstoned + long before = System.currentTimeMillis(); + DropBoxService service = new DropBoxService(getContext(), dir); + DropBox dropbox = new DropBox(service); + + dropbox.addText("DropBoxTest", "TEST"); + addRandomEntry(dropbox, "DropBoxTest", blockSize * 20); + + // Verify that things are as expected + DropBox.Entry e0 = dropbox.getNextEntry(null, before); + DropBox.Entry e1 = dropbox.getNextEntry(null, e0.getTimeMillis()); + assertTrue(null == dropbox.getNextEntry(null, e1.getTimeMillis())); + + assertEquals("TEST", e0.getText(80)); + assertEquals(null, e1.getText(80)); + assertEquals(-1, getEntrySize(e1)); + + e0.close(); + e1.close(); + + // Wait a second and write another entry -- old ones should be expunged + Thread.sleep(2000); + dropbox.addText("DropBoxTest", "TEST1"); + + e0 = dropbox.getNextEntry(null, before); + assertTrue(null == dropbox.getNextEntry(null, e0.getTimeMillis())); + assertEquals("TEST1", e0.getText(80)); + e0.close(); + } + + public void testCreateDropBoxWithInvalidDirectory() throws Exception { + // If created with an invalid directory, the DropBox should suffer quietly + // and fail all operations (this is how it survives a full disk). + // Once the directory becomes possible to create, it will start working. + + File dir = new File(getEmptyDir("testCreateDropBoxWith"), "InvalidDirectory"); + new FileOutputStream(dir).close(); // Create an empty file + DropBoxService service = new DropBoxService(getContext(), dir); + DropBox dropbox = new DropBox(service); + + dropbox.addText("DropBoxTest", "should be ignored"); + dropbox.addData("DropBoxTest", "should be ignored".getBytes(), 0); + assertTrue(null == dropbox.getNextEntry("DropBoxTest", 0)); + + dir.delete(); // Remove the file so a directory can be created + dropbox.addText("DropBoxTest", "TEST"); + DropBox.Entry e = dropbox.getNextEntry("DropBoxTest", 0); + assertTrue(null == dropbox.getNextEntry("DropBoxTest", e.getTimeMillis())); + assertEquals("DropBoxTest", e.getTag()); + assertEquals("TEST", e.getText(80)); + e.close(); + service.stop(); + } + + private void addRandomEntry(DropBox dropbox, String tag, int size) throws Exception { + byte[] bytes = new byte[size]; + new Random(System.currentTimeMillis()).nextBytes(bytes); + + File f = new File(getEmptyDir("addRandomEntry"), "random.dat"); + FileOutputStream os = new FileOutputStream(f); + os.write(bytes); + os.close(); + + ParcelFileDescriptor fd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); + dropbox.addFile(tag, fd, 0); + fd.close(); + } + + private int getEntrySize(DropBox.Entry e) throws Exception { + InputStream is = e.getInputStream(); + if (is == null) return -1; + int length = 0; + while (is.read() != -1) length++; + return length; + } + + private void waitForBroadcast(Intent intent) throws InterruptedException { + BroadcastReceiver receiver = new BroadcastReceiver() { + public synchronized void onReceive(Context context, Intent intent) { notify(); } + }; + + getContext().sendOrderedBroadcast(intent, null, receiver, null, 0, null, null); + synchronized (receiver) { receiver.wait(); } + } + + private void recursiveDelete(File file) { + if (!file.delete() && file.isDirectory()) { + for (File f : file.listFiles()) recursiveDelete(f); + file.delete(); + } + } + + private File getEmptyDir(String name) { + File dir = getContext().getDir("DropBoxTest." + name, 0); + for (File f : dir.listFiles()) recursiveDelete(f); + assertTrue(dir.listFiles().length == 0); + return dir; + } +} diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/ContentValuesBuilder.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/ContentValuesBuilder.java new file mode 100644 index 0000000..e99e4cb --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/ContentValuesBuilder.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests.vcard; + +import android.content.ContentValues; + +/** + * ContentValues-like class which enables users to chain put() methods and restricts + * the other methods. + */ +/* package */ class ContentValuesBuilder { + private final ContentValues mContentValues; + + public ContentValuesBuilder(final ContentValues contentValues) { + mContentValues = contentValues; + } + + public ContentValuesBuilder put(String key, String value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Byte value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Short value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Integer value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Long value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Float value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Double value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Boolean value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, byte[] value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder putNull(String key) { + mContentValues.putNull(key); + return this; + } +} diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/PropertyNode.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/PropertyNode.java index 0ee74df..d93a41b 100644 --- a/tests/AndroidTests/src/com/android/unit_tests/vcard/PropertyNode.java +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/PropertyNode.java @@ -16,19 +16,21 @@ package com.android.unit_tests.vcard; import android.content.ContentValues; - -import org.apache.commons.codec.binary.Base64; +import android.pim.vcard.ContactStruct; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.Map.Entry; -import java.util.regex.Pattern; /** - * @hide old class just for test + * Previously used in main vCard handling code but now exists only for testing. + * + * Especially useful for testing parser code (VCardParser), since all properties can be + * checked via this class unlike {@link ContactStruct}, which only emits the result of + * interpretation of the content of each vCard. We cannot know whether vCard parser or + * ContactStruct is wrong withouth this class. */ public class PropertyNode { public String propName; @@ -101,6 +103,15 @@ public class PropertyNode { } @Override + public int hashCode() { + // vCard may contain more than one same line in one entry, while HashSet or any other + // library which utilize hashCode() does not honor that, so intentionally throw an + // Exception. + throw new UnsupportedOperationException( + "PropertyNode does not provide hashCode() implementation intentionally."); + } + + @Override public boolean equals(Object obj) { if (!(obj instanceof PropertyNode)) { return false; @@ -159,164 +170,4 @@ public class PropertyNode { builder.append(propValue); return builder.toString(); } - - /** - * Encode this object into a string which can be decoded. - */ - public String encode() { - // PropertyNode#toString() is for reading, not for parsing in the future. - // We construct appropriate String here. - StringBuilder builder = new StringBuilder(); - if (propName.length() > 0) { - builder.append("propName:["); - builder.append(propName); - builder.append("],"); - } - int size = propGroupSet.size(); - if (size > 0) { - Set<String> set = propGroupSet; - builder.append("propGroup:["); - int i = 0; - for (String group : set) { - // We do not need to double quote groups. - // group = 1*(ALPHA / DIGIT / "-") - builder.append(group); - if (i < size - 1) { - builder.append(","); - } - i++; - } - builder.append("],"); - } - - if (paramMap.size() > 0 || paramMap_TYPE.size() > 0) { - ContentValues values = paramMap; - builder.append("paramMap:["); - size = paramMap.size(); - int i = 0; - for (Entry<String, Object> entry : values.valueSet()) { - // Assuming param-key does not contain NON-ASCII nor symbols. - // - // According to vCard 3.0: - // param-name = iana-token / x-name - builder.append(entry.getKey()); - - // param-value may contain any value including NON-ASCIIs. - // We use the following replacing rule. - // \ -> \\ - // , -> \, - // In String#replaceAll(), "\\\\" means a single backslash. - builder.append("="); - builder.append(entry.getValue().toString() - .replaceAll("\\\\", "\\\\\\\\") - .replaceAll(",", "\\\\,")); - if (i < size -1) { - builder.append(","); - } - i++; - } - - Set<String> set = paramMap_TYPE; - size = paramMap_TYPE.size(); - if (i > 0 && size > 0) { - builder.append(","); - } - i = 0; - for (String type : set) { - builder.append("TYPE="); - builder.append(type - .replaceAll("\\\\", "\\\\\\\\") - .replaceAll(",", "\\\\,")); - if (i < size - 1) { - builder.append(","); - } - i++; - } - builder.append("],"); - } - - size = propValue_vector.size(); - if (size > 0) { - builder.append("propValue:["); - List<String> list = propValue_vector; - for (int i = 0; i < size; i++) { - builder.append(list.get(i) - .replaceAll("\\\\", "\\\\\\\\") - .replaceAll(",", "\\\\,")); - if (i < size -1) { - builder.append(","); - } - } - builder.append("],"); - } - - return builder.toString(); - } - - public static PropertyNode decode(String encodedString) { - PropertyNode propertyNode = new PropertyNode(); - String trimed = encodedString.trim(); - if (trimed.length() == 0) { - return propertyNode; - } - String[] elems = trimed.split("],"); - - for (String elem : elems) { - int index = elem.indexOf('['); - String name = elem.substring(0, index - 1); - Pattern pattern = Pattern.compile("(?<!\\\\),"); - String[] values = pattern.split(elem.substring(index + 1), -1); - if (name.equals("propName")) { - propertyNode.propName = values[0]; - } else if (name.equals("propGroupSet")) { - for (String value : values) { - propertyNode.propGroupSet.add(value); - } - } else if (name.equals("paramMap")) { - ContentValues paramMap = propertyNode.paramMap; - Set<String> paramMap_TYPE = propertyNode.paramMap_TYPE; - for (String value : values) { - String[] tmp = value.split("=", 2); - String mapKey = tmp[0]; - // \, -> , - // \\ -> \ - // In String#replaceAll(), "\\\\" means a single backslash. - String mapValue = - tmp[1].replaceAll("\\\\,", ",").replaceAll("\\\\\\\\", "\\\\"); - if (mapKey.equalsIgnoreCase("TYPE")) { - paramMap_TYPE.add(mapValue); - } else { - paramMap.put(mapKey, mapValue); - } - } - } else if (name.equals("propValue")) { - StringBuilder builder = new StringBuilder(); - List<String> list = propertyNode.propValue_vector; - int length = values.length; - for (int i = 0; i < length; i++) { - String normValue = values[i] - .replaceAll("\\\\,", ",") - .replaceAll("\\\\\\\\", "\\\\"); - list.add(normValue); - builder.append(normValue); - if (i < length - 1) { - builder.append(";"); - } - } - propertyNode.propValue = builder.toString(); - } - } - - // At this time, QUOTED-PRINTABLE is already decoded to Java String. - // We just need to decode BASE64 String to binary. - String encoding = propertyNode.paramMap.getAsString("ENCODING"); - if (encoding != null && - (encoding.equalsIgnoreCase("BASE64") || - encoding.equalsIgnoreCase("B"))) { - propertyNode.propValue_bytes = - Base64.decodeBase64(propertyNode.propValue_vector.get(0).getBytes()); - } - - return propertyNode; - } } diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/PropertyNodesVerifier.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/PropertyNodesVerifier.java new file mode 100644 index 0000000..a9775fa --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/PropertyNodesVerifier.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests.vcard; + +import android.content.ContentValues; +import android.pim.vcard.VCardConfig; +import android.pim.vcard.VCardParser; +import android.pim.vcard.VCardParser_V21; +import android.pim.vcard.VCardParser_V30; +import android.pim.vcard.exception.VCardException; +import android.test.AndroidTestCase; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +public class PropertyNodesVerifier extends VNodeBuilder { + private final List<PropertyNodesVerifierElem> mPropertyNodesVerifierElemList; + private final AndroidTestCase mAndroidTestCase; + private int mIndex; + + public PropertyNodesVerifier(AndroidTestCase testCase) { + mPropertyNodesVerifierElemList = new ArrayList<PropertyNodesVerifierElem>(); + mAndroidTestCase = testCase; + } + + public PropertyNodesVerifierElem addPropertyNodesVerifierElem() { + PropertyNodesVerifierElem elem = new PropertyNodesVerifierElem(mAndroidTestCase); + mPropertyNodesVerifierElemList.add(elem); + return elem; + } + + public void verify(int resId, int vCardType) + throws IOException, VCardException { + verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), vCardType); + } + + public void verify(int resId, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), + vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType) throws IOException, VCardException { + final VCardParser vCardParser; + if (VCardConfig.isV30(vCardType)) { + vCardParser = new VCardParser_V30(true); // use StrictParsing + } else { + vCardParser = new VCardParser_V21(); + } + verify(is, vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + try { + vCardParser.parse(is, this); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void endRecord() { + super.endRecord(); + mAndroidTestCase.assertTrue(mIndex < mPropertyNodesVerifierElemList.size()); + mAndroidTestCase.assertTrue(mIndex < vNodeList.size()); + mPropertyNodesVerifierElemList.get(mIndex).verify(vNodeList.get(mIndex)); + mIndex++; + } +} + +/** + * Utility class which verifies input VNode. + * + * This class first checks whether each propertyNode in the VNode is in the + * "ordered expected property list". + * If the node does not exist in the "ordered list", the class refers to + * "unorderd expected property set" and checks the node is expected somewhere. + */ +class PropertyNodesVerifierElem { + public static class TypeSet extends HashSet<String> { + public TypeSet(String ... array) { + super(Arrays.asList(array)); + } + } + + public static class GroupSet extends HashSet<String> { + public GroupSet(String ... array) { + super(Arrays.asList(array)); + } + } + + private final HashMap<String, List<PropertyNode>> mOrderedNodeMap; + // Intentionally use ArrayList instead of Set, assuming there may be more than one + // exactly same objects. + private final ArrayList<PropertyNode> mUnorderedNodeList; + private final TestCase mTestCase; + + public PropertyNodesVerifierElem(TestCase testCase) { + mOrderedNodeMap = new HashMap<String, List<PropertyNode>>(); + mUnorderedNodeList = new ArrayList<PropertyNode>(); + mTestCase = testCase; + } + + // WithOrder + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, String propValue) { + return addNodeWithOrder(propName, propValue, null, null, null, null, null); + } + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, String propValue, + ContentValues contentValues) { + return addNodeWithOrder(propName, propValue, null, null, contentValues, null, null); + } + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, String propValue, + List<String> propValueList) { + return addNodeWithOrder(propName, propValue, propValueList, null, null, null, null); + } + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, List<String> propValueList) { + final String propValue = concatinateListWithSemiColon(propValueList); + return addNodeWithOrder(propName, propValue.toString(), propValueList, + null, null, null, null); + } + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, String propValue, + TypeSet paramMap_TYPE) { + return addNodeWithOrder(propName, propValue, null, null, null, paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, + List<String> propValueList, TypeSet paramMap_TYPE) { + final String propValue = concatinateListWithSemiColon(propValueList); + return addNodeWithOrder(propName, propValue, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, String propValue, + List<String> propValueList, TypeSet paramMap_TYPE) { + return addNodeWithOrder(propName, propValue, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addNodeWithOrder(String propName, String propValue, + List<String> propValueList, byte[] propValue_bytes, + ContentValues paramMap, TypeSet paramMap_TYPE, GroupSet propGroupSet) { + PropertyNode propertyNode = new PropertyNode(propName, + propValue, propValueList, propValue_bytes, + paramMap, paramMap_TYPE, propGroupSet); + List<PropertyNode> expectedNodeList = mOrderedNodeMap.get(propName); + if (expectedNodeList == null) { + expectedNodeList = new ArrayList<PropertyNode>(); + mOrderedNodeMap.put(propName, expectedNodeList); + } + expectedNodeList.add(propertyNode); + return this; + } + + // WithoutOrder + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, String propValue) { + return addNodeWithoutOrder(propName, propValue, null, null, null, null, null); + } + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, String propValue, + ContentValues contentValues) { + return addNodeWithoutOrder(propName, propValue, null, null, contentValues, null, null); + } + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, String propValue, + List<String> propValueList) { + return addNodeWithoutOrder(propName, propValue, propValueList, null, null, null, null); + } + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, List<String> propValueList) { + final String propValue = concatinateListWithSemiColon(propValueList); + return addNodeWithoutOrder(propName, propValue, propValueList, + null, null, null, null); + } + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, String propValue, + TypeSet paramMap_TYPE) { + return addNodeWithoutOrder(propName, propValue, null, null, null, paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, + List<String> propValueList, TypeSet paramMap_TYPE) { + final String propValue = concatinateListWithSemiColon(propValueList); + return addNodeWithoutOrder(propName, propValue, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, String propValue, + List<String> propValueList, TypeSet paramMap_TYPE) { + return addNodeWithoutOrder(propName, propValue, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addNodeWithoutOrder(String propName, String propValue, + List<String> propValueList, byte[] propValue_bytes, + ContentValues paramMap, TypeSet paramMap_TYPE, GroupSet propGroupSet) { + mUnorderedNodeList.add(new PropertyNode(propName, propValue, + propValueList, propValue_bytes, paramMap, paramMap_TYPE, propGroupSet)); + return this; + } + + public void verify(VNode vnode) { + for (PropertyNode actualNode : vnode.propList) { + verifyNode(actualNode.propName, actualNode); + } + if (!mOrderedNodeMap.isEmpty() || !mUnorderedNodeList.isEmpty()) { + List<String> expectedProps = new ArrayList<String>(); + for (List<PropertyNode> nodes : mOrderedNodeMap.values()) { + for (PropertyNode node : nodes) { + if (!expectedProps.contains(node.propName)) { + expectedProps.add(node.propName); + } + } + } + for (PropertyNode node : mUnorderedNodeList) { + if (!expectedProps.contains(node.propName)) { + expectedProps.add(node.propName); + } + } + mTestCase.fail("Expected property " + Arrays.toString(expectedProps.toArray()) + + " was not found."); + } + } + + private void verifyNode(final String propName, final PropertyNode actualNode) { + List<PropertyNode> expectedNodeList = mOrderedNodeMap.get(propName); + final int size = (expectedNodeList != null ? expectedNodeList.size() : 0); + if (size > 0) { + for (int i = 0; i < size; i++) { + PropertyNode expectedNode = expectedNodeList.get(i); + List<PropertyNode> expectedButDifferentValueList = new ArrayList<PropertyNode>(); + if (expectedNode.propName.equals(propName)) { + if (expectedNode.equals(actualNode)) { + expectedNodeList.remove(i); + if (expectedNodeList.size() == 0) { + mOrderedNodeMap.remove(propName); + } + return; + } else { + expectedButDifferentValueList.add(expectedNode); + } + } + + // "actualNode" is not in ordered expected list. + // Try looking over unordered expected list. + if (tryFoundExpectedNodeFromUnorderedList(actualNode, + expectedButDifferentValueList)) { + return; + } + + if (!expectedButDifferentValueList.isEmpty()) { + // Same propName exists but with different value(s). + failWithExpectedNodeList(propName, actualNode, + expectedButDifferentValueList); + } else { + // There's no expected node with same propName. + mTestCase.fail("Unexpected property \"" + propName + "\" exists."); + } + } + } else { + List<PropertyNode> expectedButDifferentValueList = + new ArrayList<PropertyNode>(); + if (tryFoundExpectedNodeFromUnorderedList(actualNode, expectedButDifferentValueList)) { + return; + } else { + if (!expectedButDifferentValueList.isEmpty()) { + // Same propName exists but with different value(s). + failWithExpectedNodeList(propName, actualNode, + expectedButDifferentValueList); + } else { + // There's no expected node with same propName. + mTestCase.fail("Unexpected property \"" + propName + "\" exists."); + } + } + } + } + + private String concatinateListWithSemiColon(List<String> array) { + StringBuffer buffer = new StringBuffer(); + boolean first = true; + for (String propValueElem : array) { + if (first) { + first = false; + } else { + buffer.append(';'); + } + buffer.append(propValueElem); + } + + return buffer.toString(); + } + + private boolean tryFoundExpectedNodeFromUnorderedList(PropertyNode actualNode, + List<PropertyNode> expectedButDifferentValueList) { + final String propName = actualNode.propName; + int unorderedListSize = mUnorderedNodeList.size(); + for (int i = 0; i < unorderedListSize; i++) { + PropertyNode unorderedExpectedNode = mUnorderedNodeList.get(i); + if (unorderedExpectedNode.propName.equals(propName)) { + if (unorderedExpectedNode.equals(actualNode)) { + mUnorderedNodeList.remove(i); + return true; + } + expectedButDifferentValueList.add(unorderedExpectedNode); + } + } + return false; + } + + private void failWithExpectedNodeList(String propName, PropertyNode actualNode, + List<PropertyNode> expectedNodeList) { + StringBuilder builder = new StringBuilder(); + for (PropertyNode expectedNode : expectedNodeList) { + builder.append("expected: "); + builder.append(expectedNode.toString()); + builder.append("\n"); + } + mTestCase.fail("Property \"" + propName + "\" has wrong value.\n" + + builder.toString() + + " actual: " + actualNode.toString()); + } +} diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardExporterTests.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardExporterTests.java new file mode 100644 index 0000000..cdecd3b --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardExporterTests.java @@ -0,0 +1,906 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests.vcard; + +import android.content.ContentValues; +import android.pim.vcard.VCardConfig; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; + +import com.android.unit_tests.vcard.PropertyNodesVerifierElem.TypeSet; + +import java.util.Arrays; + +/** + * Tests for the code related to vCard exporter, inculding vCard composer. + * This test class depends on vCard importer code, so if tests for vCard importer fail, + * the result of this class will not be reliable. + */ +public class VCardExporterTests extends VCardTestsBase { + private static final byte[] sPhotoByteArray = + VCardImporterTests.sPhotoByteArrayForComplicatedCase; + + public void testSimpleV21() { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid"); + + VCardVerifier verifier = new VCardVerifier(resolver, V21); + verifier.addPropertyNodesVerifierElem() + .addNodeWithoutOrder("FN", "Roid Ando") + .addNodeWithoutOrder("N", "Ando;Roid;;;", Arrays.asList("Ando", "Roid", "", "", "")); + verifier.verify(); + } + + private void testStructuredNameBasic(int version) { + final boolean isV30 = VCardConfig.isV30(version); + ExportTestResolver resolver = new ExportTestResolver(); + + resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") + .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") + .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") + .put(StructuredName.PREFIX, "AppropriatePrefix") + .put(StructuredName.SUFFIX, "AppropriateSuffix") + .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") + .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle"); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + PropertyNodesVerifierElem elem = verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("N", + "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" + + "AppropriatePrefix;AppropriateSuffix", + Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", + "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) + .addNodeWithOrder("FN", + "AppropriatePrefix AppropriateGivenName " + + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") + .addNodeWithoutOrder("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") + .addNodeWithoutOrder("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") + .addNodeWithoutOrder("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); + + if (isV30) { + elem.addNodeWithoutOrder("SORT-STRING", + "AppropriatePhoneticGiven AppropriatePhoneticMiddle " + + "AppropriatePhoneticFamily"); + } + + verifier.verify(); + } + + public void testStructuredNameBasicV21() { + testStructuredNameBasic(V21); + } + + public void testStructuredNameBasicV30() { + testStructuredNameBasic(V30); + } + + /** + * Test that only "primary" StructuredName is emitted, so that our vCard file + * will not confuse the external importer, assuming there may be some importer + * which presume that there's only one property toward each of "N", "FN", etc. + * Note that more than one "N", "FN", etc. properties are acceptable in vCard spec. + */ + private void testStructuredNameUsePrimaryCommon(int version) { + final boolean isV30 = (version == V30); + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1") + .put(StructuredName.PREFIX, "DoNotEmitPrefix1") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix1") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily1") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven1") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle1"); + + // With "IS_PRIMARY=1". This is what we should use. + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") + .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") + .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") + .put(StructuredName.PREFIX, "AppropriatePrefix") + .put(StructuredName.SUFFIX, "AppropriateSuffix") + .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") + .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle") + .put(StructuredName.IS_PRIMARY, 1); + + // With "IS_PRIMARY=1", but we should ignore this time, since this is second, not first. + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2") + .put(StructuredName.PREFIX, "DoNotEmitPrefix2") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix2") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily2") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven2") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2") + .put(StructuredName.IS_PRIMARY, 1); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + PropertyNodesVerifierElem elem = verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("N", + "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" + + "AppropriatePrefix;AppropriateSuffix", + Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", + "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) + .addNodeWithOrder("FN", + "AppropriatePrefix AppropriateGivenName " + + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") + .addNodeWithoutOrder("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") + .addNodeWithoutOrder("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") + .addNodeWithoutOrder("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); + + if (isV30) { + elem.addNodeWithoutOrder("SORT-STRING", + "AppropriatePhoneticGiven AppropriatePhoneticMiddle " + + "AppropriatePhoneticFamily"); + } + + verifier.verify(); + } + + public void testStructuredNameUsePrimaryV21() { + testStructuredNameUsePrimaryCommon(V21); + } + + public void testStructuredNameUsePrimaryV30() { + testStructuredNameUsePrimaryCommon(V30); + } + + /** + * Tests that only "super primary" StructuredName is emitted. + * See also the comment in {@link #testStructuredNameUsePrimaryCommon(int)}. + */ + private void testStructuredNameUseSuperPrimaryCommon(int version) { + final boolean isV30 = (version == V30); + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1") + .put(StructuredName.PREFIX, "DoNotEmitPrefix1") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix1") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily1") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven1") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle1"); + + // With "IS_PRIMARY=1", but we should ignore this time. + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2") + .put(StructuredName.PREFIX, "DoNotEmitPrefix2") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix2") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily2") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven2") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2") + .put(StructuredName.IS_PRIMARY, 1); + + // With "IS_SUPER_PRIMARY=1". This is what we should use. + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") + .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") + .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") + .put(StructuredName.PREFIX, "AppropriatePrefix") + .put(StructuredName.SUFFIX, "AppropriateSuffix") + .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") + .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle") + .put(StructuredName.IS_SUPER_PRIMARY, 1); + + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName3") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName3") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName3") + .put(StructuredName.PREFIX, "DoNotEmitPrefix3") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix3") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily3") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven3") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle3") + .put(StructuredName.IS_PRIMARY, 1); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + PropertyNodesVerifierElem elem = verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("N", + "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" + + "AppropriatePrefix;AppropriateSuffix", + Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", + "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) + .addNodeWithOrder("FN", + "AppropriatePrefix AppropriateGivenName " + + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") + .addNodeWithoutOrder("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") + .addNodeWithoutOrder("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") + .addNodeWithoutOrder("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); + + if (isV30) { + elem.addNodeWithoutOrder("SORT-STRING", + "AppropriatePhoneticGiven AppropriatePhoneticMiddle" + + " AppropriatePhoneticFamily"); + } + + verifier.verify(); + } + + public void testStructuredNameUseSuperPrimaryV21() { + testStructuredNameUseSuperPrimaryCommon(V21); + } + + public void testStructuredNameUseSuperPrimaryV30() { + testStructuredNameUseSuperPrimaryCommon(V30); + } + + public void testNickNameV30() { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(Nickname.CONTENT_ITEM_TYPE) + .put(Nickname.NAME, "Nicky"); + + VCardVerifier verifier = new VCardVerifier(resolver, V30); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithOrder("NICKNAME", "Nicky"); + + verifier.verify(); + } + + private void testPhoneBasicCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + resolver.buildContactEntry().buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1") + .put(Phone.TYPE, Phone.TYPE_HOME); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("TEL", "1", new TypeSet("HOME")); + + verifier.verify(); + } + + public void testPhoneBasicV21() { + testPhoneBasicCommon(V21); + } + + public void testPhoneBasicV30() { + testPhoneBasicCommon(V30); + } + + /** + * Tests that vCard composer emits corresponding type param which we expect. + */ + private void testPhoneVariousTypeSupport(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "10") + .put(Phone.TYPE, Phone.TYPE_HOME); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "20") + .put(Phone.TYPE, Phone.TYPE_WORK); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "30") + .put(Phone.TYPE, Phone.TYPE_FAX_HOME); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "40") + .put(Phone.TYPE, Phone.TYPE_FAX_WORK); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "50") + .put(Phone.TYPE, Phone.TYPE_MOBILE); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "60") + .put(Phone.TYPE, Phone.TYPE_PAGER); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "70") + .put(Phone.TYPE, Phone.TYPE_OTHER); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "80") + .put(Phone.TYPE, Phone.TYPE_CAR); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "90") + .put(Phone.TYPE, Phone.TYPE_COMPANY_MAIN); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "100") + .put(Phone.TYPE, Phone.TYPE_ISDN); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "110") + .put(Phone.TYPE, Phone.TYPE_MAIN); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "120") + .put(Phone.TYPE, Phone.TYPE_OTHER_FAX); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "130") + .put(Phone.TYPE, Phone.TYPE_TELEX); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "140") + .put(Phone.TYPE, Phone.TYPE_WORK_MOBILE); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "150") + .put(Phone.TYPE, Phone.TYPE_WORK_PAGER); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "160") + .put(Phone.TYPE, Phone.TYPE_MMS); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("TEL", "10", new TypeSet("HOME")) + .addNodeWithoutOrder("TEL", "20", new TypeSet("WORK")) + .addNodeWithoutOrder("TEL", "30", new TypeSet("HOME", "FAX")) + .addNodeWithoutOrder("TEL", "40", new TypeSet("WORK", "FAX")) + .addNodeWithoutOrder("TEL", "50", new TypeSet("CELL")) + .addNodeWithoutOrder("TEL", "60", new TypeSet("PAGER")) + .addNodeWithoutOrder("TEL", "70", new TypeSet("VOICE")) + .addNodeWithoutOrder("TEL", "80", new TypeSet("CAR")) + .addNodeWithoutOrder("TEL", "90", new TypeSet("WORK", "PREF")) + .addNodeWithoutOrder("TEL", "100", new TypeSet("ISDN")) + .addNodeWithoutOrder("TEL", "110", new TypeSet("PREF")) + .addNodeWithoutOrder("TEL", "120", new TypeSet("FAX")) + .addNodeWithoutOrder("TEL", "130", new TypeSet("TLX")) + .addNodeWithoutOrder("TEL", "140", new TypeSet("WORK", "CELL")) + .addNodeWithoutOrder("TEL", "150", new TypeSet("WORK", "PAGER")) + .addNodeWithoutOrder("TEL", "160", new TypeSet("MSG")); + verifier.verify(); + } + + public void testPhoneVariousTypeSupportV21() { + testPhoneVariousTypeSupport(V21); + } + + public void testPhoneVariousTypeSupportV30() { + testPhoneVariousTypeSupport(V30); + } + + /** + * Tests that "PREF"s are emitted appropriately. + */ + private void testPhonePrefHandlingCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1") + .put(Phone.TYPE, Phone.TYPE_HOME); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "2") + .put(Phone.TYPE, Phone.TYPE_WORK) + .put(Phone.IS_PRIMARY, 1); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "3") + .put(Phone.TYPE, Phone.TYPE_FAX_HOME) + .put(Phone.IS_PRIMARY, 1); + entry.buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "4") + .put(Phone.TYPE, Phone.TYPE_FAX_WORK); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("TEL", "4", new TypeSet("WORK", "FAX")) + .addNodeWithoutOrder("TEL", "3", new TypeSet("HOME", "FAX", "PREF")) + .addNodeWithoutOrder("TEL", "2", new TypeSet("WORK", "PREF")) + .addNodeWithoutOrder("TEL", "1", new TypeSet("HOME")); + verifier.verify(); + } + + public void testPhonePrefHandlingV21() { + testPhonePrefHandlingCommon(V21); + } + + public void testPhonePrefHandlingV30() { + testPhonePrefHandlingCommon(V30); + } + + private void testEmailBasicCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "sample@example.com"); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("EMAIL", "sample@example.com"); + + verifier.verify(); + } + + public void testEmailBasicV21() { + testEmailBasicCommon(V21); + } + + public void testEmailBasicV30() { + testEmailBasicCommon(V30); + } + + private void testEmailVariousTypeSupportCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_home@example.com") + .put(Email.TYPE, Email.TYPE_HOME); + entry.buildData(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_work@example.com") + .put(Email.TYPE, Email.TYPE_WORK); + entry.buildData(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_mobile@example.com") + .put(Email.TYPE, Email.TYPE_MOBILE); + entry.buildData(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_other@example.com") + .put(Email.TYPE, Email.TYPE_OTHER); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("EMAIL", "type_home@example.com", new TypeSet("HOME")) + .addNodeWithoutOrder("EMAIL", "type_work@example.com", new TypeSet("WORK")) + .addNodeWithoutOrder("EMAIL", "type_mobile@example.com", new TypeSet("CELL")) + .addNodeWithoutOrder("EMAIL", "type_other@example.com"); + + verifier.verify(); + } + + public void testEmailVariousTypeSupportV21() { + testEmailVariousTypeSupportCommon(V21); + } + + public void testEmailVariousTypeSupportV30() { + testEmailVariousTypeSupportCommon(V30); + } + + private void testEmailPrefHandlingCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_home@example.com") + .put(Email.TYPE, Email.TYPE_HOME) + .put(Email.IS_PRIMARY, 1); + entry.buildData(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_notype@example.com") + .put(Email.IS_PRIMARY, 1); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("EMAIL", "type_notype@example.com", new TypeSet("PREF")) + .addNodeWithoutOrder("EMAIL", "type_home@example.com", new TypeSet("HOME", "PREF")); + + verifier.verify(); + } + + public void testEmailPrefHandlingV21() { + testEmailPrefHandlingCommon(V21); + } + + public void testEmailPrefHandlingV30() { + testEmailPrefHandlingCommon(V30); + } + + private void testPostalOnlyWithStructuredDataCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal Code, + // ; Country Name + resolver.buildContactEntry().buildData(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "Pobox") + .put(StructuredPostal.NEIGHBORHOOD, "Neighborhood") + .put(StructuredPostal.STREET, "Street") + .put(StructuredPostal.CITY, "City") + .put(StructuredPostal.REGION, "Region") + .put(StructuredPostal.POSTCODE, "100") + .put(StructuredPostal.COUNTRY, "Country"); + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("ADR", "Pobox;Neighborhood;Street;City;Region;100;Country", + Arrays.asList("Pobox", "Neighborhood", "Street", "City", + "Region", "100", "Country"), new TypeSet("HOME")); + + verifier.verify(); + } + + public void testPostalOnlyWithStructuredDataV21() { + testPostalOnlyWithStructuredDataCommon(V21); + } + + public void testPostalOnlyWithStructuredDataV30() { + testPostalOnlyWithStructuredDataCommon(V30); + } + + private void testPostalOnlyWithFormattedAddressCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + resolver.buildContactEntry().buildData(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.FORMATTED_ADDRESS, + "Formatted address CA 123-334 United Statue"); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithOrder("ADR", ";Formatted address CA 123-334 United Statue;;;;;", + Arrays.asList("", "Formatted address CA 123-334 United Statue", + "", "", "", "", ""), new TypeSet("HOME")); + + verifier.verify(); + } + + public void testPostalOnlyWithFormattedAddressV21() { + testPostalOnlyWithFormattedAddressCommon(V21); + } + + public void testPostalOnlyWithFormattedAddressV30() { + testPostalOnlyWithFormattedAddressCommon(V30); + } + + /** + * Tests that the vCard composer honors formatted data when it is available + * even when it is partial. + */ + private void testPostalWithBothStructuredAndFormattedCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + resolver.buildContactEntry().buildData(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "Pobox") + .put(StructuredPostal.COUNTRY, "Country") + .put(StructuredPostal.FORMATTED_ADDRESS, + "Formatted address CA 123-334 United Statue"); // Should be ignored + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("ADR", "Pobox;;;;;;Country", + Arrays.asList("Pobox", "", "", "", "", "", "Country"), + new TypeSet("HOME")); + + verifier.verify(); + } + + public void testPostalWithBothStructuredAndFormattedV21() { + testPostalWithBothStructuredAndFormattedCommon(V21); + } + + public void testPostalWithBothStructuredAndFormattedV30() { + testPostalWithBothStructuredAndFormattedCommon(V30); + } + + private void testOrganizationCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "CompanyX") + .put(Organization.DEPARTMENT, "DepartmentY") + .put(Organization.TITLE, "TitleZ") + .put(Organization.JOB_DESCRIPTION, "Description Rambda") // Ignored. + .put(Organization.OFFICE_LOCATION, "Mountain View") // Ignored. + .put(Organization.PHONETIC_NAME, "PhoneticName!") // Ignored + .put(Organization.SYMBOL, "(^o^)/~~"); // Ignore him (her). + + entry.buildData(Organization.CONTENT_ITEM_TYPE) + .putNull(Organization.COMPANY) + .put(Organization.DEPARTMENT, "DepartmentXX") + .putNull(Organization.TITLE); + + entry.buildData(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "CompanyXYZ") + .putNull(Organization.DEPARTMENT) + .put(Organization.TITLE, "TitleXYZYX"); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + + // Currently we do not use group but depend on the order. + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithOrder("ORG", "CompanyX;DepartmentY", + Arrays.asList("CompanyX", "DepartmentY")) + .addNodeWithOrder("TITLE", "TitleZ") + .addNodeWithOrder("ORG", "DepartmentXX") + .addNodeWithOrder("ORG", "CompanyXYZ") + .addNodeWithOrder("TITLE", "TitleXYZYX"); + + verifier.verify(); + } + + public void testOrganizationV21() { + testOrganizationCommon(V21); + } + + public void testOrganizationV30() { + testOrganizationCommon(V30); + } + + private void testImVariousTypeSupportCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_AIM) + .put(Im.DATA, "aim"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_MSN) + .put(Im.DATA, "msn"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_YAHOO) + .put(Im.DATA, "yahoo"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_SKYPE) + .put(Im.DATA, "skype"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_QQ) + .put(Im.DATA, "qq"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK) + .put(Im.DATA, "google talk"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_ICQ) + .put(Im.DATA, "icq"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_JABBER) + .put(Im.DATA, "jabber"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_NETMEETING) + .put(Im.DATA, "netmeeting"); + + // No determined way to express unknown type... + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("X-JABBER", "jabber") + .addNodeWithoutOrder("X-ICQ", "icq") + .addNodeWithoutOrder("X-GOOGLE-TALK", "google talk") + .addNodeWithoutOrder("X-QQ", "qq") + .addNodeWithoutOrder("X-SKYPE-USERNAME", "skype") + .addNodeWithoutOrder("X-YAHOO", "yahoo") + .addNodeWithoutOrder("X-MSN", "msn") + .addNodeWithoutOrder("X-NETMEETING", "netmeeting") + .addNodeWithoutOrder("X-AIM", "aim"); + + verifier.verify(); + } + + public void testImBasiV21() { + testImVariousTypeSupportCommon(V21); + } + + public void testImBasicV30() { + testImVariousTypeSupportCommon(V30); + } + + private void testImPrefHandlingCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_AIM) + .put(Im.DATA, "aim1"); + + entry.buildData(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_AIM) + .put(Im.DATA, "aim2") + .put(Im.TYPE, Im.TYPE_HOME) + .put(Im.IS_PRIMARY, 1); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("X-AIM", "aim1") + .addNodeWithoutOrder("X-AIM", "aim2", new TypeSet("HOME", "PREF")); + + verifier.verify(); + } + + public void testImPrefHandlingV21() { + testImPrefHandlingCommon(V21); + } + + public void testImPrefHandlingV30() { + testImPrefHandlingCommon(V30); + } + + private void testWebsiteCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Website.CONTENT_ITEM_TYPE) + .put(Website.URL, "http://website.example.android.com/index.html") + .put(Website.TYPE, Website.TYPE_BLOG); + + entry.buildData(Website.CONTENT_ITEM_TYPE) + .put(Website.URL, "ftp://ftp.example.android.com/index.html") + .put(Website.TYPE, Website.TYPE_FTP); + + // We drop TYPE information since vCard (especially 3.0) does not allow us to emit it. + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("URL", "ftp://ftp.example.android.com/index.html") + .addNodeWithoutOrder("URL", "http://website.example.android.com/index.html"); + verifier.verify(); + } + + public void testWebsiteV21() { + testWebsiteCommon(V21); + } + + public void testWebsiteV30() { + testWebsiteCommon(V30); + } + + private void testEventCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_ANNIVERSARY) + .put(Event.START_DATE, "1982-06-16"); + entry.buildData(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_BIRTHDAY) + .put(Event.START_DATE, "2008-10-22"); + entry.buildData(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_OTHER) + .put(Event.START_DATE, "2018-03-12"); + entry.buildData(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_CUSTOM) + .put(Event.LABEL, "The last day") + .put(Event.START_DATE, "When the Tower of Hanoi with 64 rings is completed."); + entry.buildData(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_BIRTHDAY) + .put(Event.START_DATE, "2009-05-19"); + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("BDAY", "2008-10-22"); + + verifier.verify(); + } + + public void testEventV21() { + testEventCommon(V21); + } + + public void testEventV30() { + testEventCommon(V30); + } + + private void testNoteCommon(int version) { + ExportTestResolver resolver = new ExportTestResolver(); + + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "note1"); + entry.buildData(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "note2") + .put(Note.IS_PRIMARY, 1); // Just ignored. + + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithOrder("NOTE", "note1") + .addNodeWithOrder("NOTE", "note2"); + + verifier.verify(); + } + + public void testNoteV21() { + testNoteCommon(V21); + } + + public void testNoteV30() { + testNoteCommon(V30); + } + + private void testPhotoCommon(int version) { + final boolean isV30 = version == V30; + ExportTestResolver resolver = new ExportTestResolver(); + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "PhotoTest"); + entry.buildData(Photo.CONTENT_ITEM_TYPE) + .put(Photo.PHOTO, sPhotoByteArray); + + ContentValues contentValuesForPhoto = new ContentValues(); + contentValuesForPhoto.put("ENCODING", (isV30 ? "b" : "BASE64")); + VCardVerifier verifier = new VCardVerifier(resolver, version); + verifier.addPropertyNodesVerifierElem() + .addNodeWithoutOrder("FN", "PhotoTest") + .addNodeWithoutOrder("N", "PhotoTest;;;;", + Arrays.asList("PhotoTest", "", "", "", "")) + .addNodeWithOrder("PHOTO", null, null, sPhotoByteArray, + contentValuesForPhoto, new TypeSet("JPEG"), null); + + verifier.verify(); + } + + public void testPhotoV21() { + testPhotoCommon(V21); + } + + public void testPhotoV30() { + testPhotoCommon(V30); + } + + public void testV30HandleEscape() { + final int version = V30; + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\\") + .put(StructuredName.GIVEN_NAME, ";") + .put(StructuredName.MIDDLE_NAME, ",") + .put(StructuredName.PREFIX, "\n") + .put(StructuredName.DISPLAY_NAME, "[<{Unescaped:Asciis}>]"); + VCardVerifier verifier = new VCardVerifier(resolver, version); + // Verifies the vCard String correctly escapes each character which must be escaped. + verifier.addLineVerifier() + .addExpected("N:\\\\;\\;;\\,;\\n;") + .addExpected("FN:[<{Unescaped:Asciis}>]"); + verifier.addPropertyNodesVerifierElem() + .addNodeWithoutOrder("FN", "[<{Unescaped:Asciis}>]") + .addNodeWithoutOrder("N", Arrays.asList("\\", ";", ",", "\n", "")); + + verifier.verify(); + } + + /** + * There's no "NICKNAME" property in vCard 2.1, while there is in vCard 3.0. + * We use Android-specific "X-ANDROID-CUSTOM" property. + * This test verifies the functionality. + */ + public void testNickNameV21() { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(Nickname.CONTENT_ITEM_TYPE) + .put(Nickname.NAME, "Nicky"); + + VCardVerifier verifier = new VCardVerifier(resolver, V21); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithOrder("X-ANDROID-CUSTOM", + Nickname.CONTENT_ITEM_TYPE + ";Nicky;;;;;;;;;;;;;;"); + verifier.addImportVerifier() + .addExpected(Nickname.CONTENT_ITEM_TYPE) + .put(Nickname.NAME, "Nicky"); + verifier.verify(); + } + + public void testTolerateBrokenPhoneNumberEntryV21() { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_HOME) + .put(Phone.NUMBER, "111-222-3333 (Miami)\n444-5555-666 (Tokyo);" + + "777-888-9999 (Chicago);111-222-3333 (Miami)"); + VCardVerifier verifier = new VCardVerifier(resolver, V21); + verifier.addPropertyNodesVerifierWithEmptyName() + .addNodeWithoutOrder("TEL", "111-222-3333", new TypeSet("HOME")) + .addNodeWithoutOrder("TEL", "444-555-5666", new TypeSet("HOME")) + .addNodeWithoutOrder("TEL", "777-888-9999", new TypeSet("HOME")); + verifier.verify(); + } +} diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardImporterTests.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardImporterTests.java new file mode 100644 index 0000000..b1fccaa --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardImporterTests.java @@ -0,0 +1,1057 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests.vcard; + +import android.content.ContentValues; +import android.pim.vcard.VCardConfig; +import android.pim.vcard.VCardParser; +import android.pim.vcard.VCardParser_V21; +import android.pim.vcard.exception.VCardException; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; + +import com.android.unit_tests.R; +import com.android.unit_tests.vcard.PropertyNodesVerifierElem.TypeSet; + +import java.io.IOException; +import java.util.Arrays; + +public class VCardImporterTests extends VCardTestsBase { + // Push data into int array at first since values like 0x80 are + // interpreted as int by the compiler and casting all of them is + // cumbersome... + private static final int[] sPhotoIntArrayForComplicatedCase = { + 0xff, 0xd8, 0xff, 0xe1, 0x0a, 0x0f, 0x45, 0x78, 0x69, 0x66, 0x00, + 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0d, + 0x01, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0xaa, 0x01, 0x0f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0xba, 0x01, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0xc2, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0xc8, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x28, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x31, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xd8, 0x01, 0x32, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x13, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x82, + 0x98, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xfa, + 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x84, 0xc4, 0xa5, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, + 0x01, 0x08, 0x00, 0x00, 0x04, 0x1e, 0x32, 0x30, 0x30, 0x38, 0x31, + 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, 0x00, 0x00, + 0x44, 0x6f, 0x43, 0x6f, 0x4d, 0x6f, 0x00, 0x00, 0x44, 0x39, 0x30, + 0x35, 0x69, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x44, 0x39, 0x30, + 0x35, 0x69, 0x20, 0x56, 0x65, 0x72, 0x31, 0x2e, 0x30, 0x30, 0x00, + 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, + 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x00, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x49, 0x4d, 0x00, 0x30, 0x33, + 0x30, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x14, 0x00, + 0x14, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x34, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x10, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x11, 0x09, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x0f, 0x0b, 0x00, + 0x00, 0x27, 0x10, 0x00, 0x00, 0x05, 0x97, 0x00, 0x00, 0x27, 0x10, + 0x00, 0x00, 0x08, 0xb0, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1c, + 0x01, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x02, 0x5e, 0x00, 0x00, + 0x27, 0x10, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x27, 0x10, 0x00, + 0x00, 0x03, 0xcb, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1b, 0xe5, + 0x00, 0x00, 0x27, 0x10, 0x00, 0x28, 0x82, 0x9a, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x6a, 0x82, 0x9d, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x72, 0x88, 0x22, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x90, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x32, 0x32, 0x30, 0x90, + 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, 0x7a, + 0x90, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, + 0x8e, 0x91, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, + 0x03, 0x00, 0x91, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x03, 0xa2, 0x92, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x03, 0xaa, 0x92, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x03, 0xb2, 0x92, 0x04, 0x00, 0x0a, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x03, 0xba, 0x92, 0x05, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xc2, 0x92, 0x07, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x92, 0x08, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, 0x09, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, + 0x0a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xca, + 0x92, 0x7c, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x92, 0x86, 0x00, 0x07, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, + 0x03, 0xd2, 0xa0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, + 0x31, 0x30, 0x30, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x60, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x48, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0xa2, 0x0e, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xe8, 0xa2, 0x0f, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xf0, 0xa2, 0x10, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0xa2, + 0x17, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, + 0xa3, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, + 0x00, 0xa3, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, + 0x00, 0x00, 0xa4, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x04, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x03, 0xf8, 0xa4, 0x05, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x1d, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x07, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x08, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, + 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0xa4, 0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0xa4, 0x0c, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, + 0x00, 0x27, 0x10, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, + 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, + 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x33, 0x31, 0x00, 0x32, 0x30, + 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, 0x31, 0x33, + 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x00, 0x00, 0x29, 0x88, + 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x02, 0xb2, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x25, 0x00, + 0x00, 0x00, 0x0a, 0x00, 0x00, 0x0e, 0x92, 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x30, 0x30, + 0x38, 0x31, 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, + 0x00, 0x00, 0x20, 0x2a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x2a, + 0xe2, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x04, 0x52, 0x39, 0x38, 0x00, 0x00, 0x02, 0x00, 0x07, 0x00, 0x00, + 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, + 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x04, 0x6c, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x04, 0x74, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x04, 0x7c, 0x02, 0x02, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0x8b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, + 0x00, 0x20, 0x16, 0x18, 0x1c, 0x18, 0x14, 0x20, 0x1c, 0x1a, 0x1c, + 0x24, 0x22, 0x20, 0x26, 0x30, 0x50, 0x34, 0x30, 0x2c, 0x2c, 0x30, + 0x62, 0x46, 0x4a, 0x3a, 0x50, 0x74, 0x66, 0x7a, 0x78, 0x72, 0x66, + 0x70, 0x6e, 0x80, 0x90, 0xb8, 0x9c, 0x80, 0x88, 0xae, 0x8a, 0x6e, + 0x70, 0xa0, 0xda, 0xa2, 0xae, 0xbe, 0xc4, 0xce, 0xd0, 0xce, 0x7c, + 0x9a, 0xe2, 0xf2, 0xe0, 0xc8, 0xf0, 0xb8, 0xca, 0xce, 0xc6, 0x01, + 0x22, 0x24, 0x24, 0x30, 0x2a, 0x30, 0x5e, 0x34, 0x34, 0x5e, 0xc6, + 0x84, 0x70, 0x84, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xff, 0xc0, + 0x00, 0x11, 0x08, 0x00, 0x78, 0x00, 0xa0, 0x03, 0x01, 0x21, 0x00, + 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, + 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, + 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, + 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, + 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, + 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, + 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, + 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, + 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, + 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, + 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, + 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, + 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, + 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, + 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, + 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, + 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, + 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, + 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, + 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, + 0x14, 0x54, 0xaa, 0x2a, 0x46, 0x48, 0xa2, 0xa4, 0x55, 0xa6, 0x04, + 0x8a, 0x29, 0xe0, 0x53, 0x10, 0xe0, 0x29, 0xc0, 0x50, 0x03, 0xb1, + 0x46, 0x29, 0x80, 0x84, 0x52, 0x11, 0x40, 0x0d, 0x22, 0x9a, 0x45, + 0x20, 0x23, 0x61, 0x51, 0x30, 0xa0, 0x08, 0xc8, 0xa8, 0xd8, 0x52, + 0x02, 0x26, 0x15, 0x0b, 0x0a, 0x00, 0xb4, 0xa2, 0xa5, 0x5a, 0x00, + 0x91, 0x45, 0x4a, 0xa2, 0x81, 0x92, 0x01, 0x4e, 0x02, 0x98, 0x87, + 0x0a, 0x70, 0xa0, 0x07, 0x62, 0x8c, 0x50, 0x21, 0x0d, 0x25, 0x00, + 0x34, 0x8a, 0x61, 0x14, 0x0c, 0x63, 0x0a, 0x89, 0x85, 0x00, 0x46, + 0xd5, 0x1b, 0x52, 0x02, 0x16, 0xa8, 0x98, 0x50, 0x05, 0x94, 0xa9, + 0x16, 0x80, 0x25, 0x5a, 0x95, 0x68, 0x18, 0xf1, 0x4f, 0x14, 0xc4, + 0x3b, 0xb5, 0x22, 0xb6, 0x38, 0x34, 0x00, 0xe3, 0x22, 0x8e, 0xf4, + 0x79, 0x8a, 0x7b, 0xd1, 0x71, 0x03, 0x30, 0xc7, 0x14, 0x83, 0xa5, + 0x00, 0x06, 0x98, 0x68, 0x01, 0x8d, 0x51, 0x35, 0x03, 0x22, 0x6a, + 0x8d, 0xa9, 0x01, 0x13, 0x54, 0x4d, 0x40, 0x13, 0xa5, 0x4a, 0x28, + 0x02, 0x45, 0x35, 0x2a, 0x9a, 0x00, 0x78, 0x34, 0xf0, 0x69, 0x80, + 0x34, 0x81, 0x45, 0x40, 0xce, 0x58, 0xe6, 0xa2, 0x4c, 0x06, 0xe4, + 0xfa, 0xd1, 0x93, 0x50, 0x21, 0xca, 0xe4, 0x55, 0x84, 0x90, 0x30, + 0xab, 0x8b, 0x18, 0xa6, 0x9a, 0x6a, 0xc4, 0x31, 0xaa, 0x26, 0xa0, + 0x64, 0x4d, 0x51, 0xb5, 0x20, 0x23, 0x6a, 0x89, 0xa8, 0x02, 0x44, + 0x35, 0x2a, 0x9a, 0x00, 0x95, 0x4d, 0x48, 0xa6, 0x80, 0x24, 0x53, + 0x4e, 0xce, 0x05, 0x30, 0x2b, 0x3b, 0xee, 0x6a, 0x91, 0x5d, 0x76, + 0x63, 0xbd, 0x65, 0x7d, 0x40, 0x66, 0x68, 0xa9, 0x02, 0x45, 0x2b, + 0xb3, 0x9e, 0xb4, 0xc5, 0x6d, 0xad, 0x9a, 0xa0, 0x2c, 0x06, 0xc8, + 0xcd, 0x04, 0xd6, 0xa2, 0x23, 0x63, 0x51, 0xb1, 0xa0, 0x64, 0x4d, + 0x51, 0x93, 0x48, 0x08, 0xda, 0xa2, 0x6a, 0x00, 0x72, 0x1a, 0x99, + 0x4d, 0x00, 0x48, 0xa6, 0xa4, 0x53, 0x4c, 0x07, 0x86, 0x03, 0xbd, + 0x2b, 0x9c, 0xa7, 0x14, 0x98, 0x10, 0x85, 0x34, 0xe0, 0xa6, 0xb3, + 0xb0, 0x0b, 0xb5, 0xa8, 0x0a, 0xd4, 0x58, 0x42, 0xed, 0x3e, 0x94, + 0xd2, 0xa6, 0x8b, 0x01, 0x34, 0x44, 0xed, 0xe6, 0x9c, 0x4d, 0x6a, + 0x80, 0x8d, 0x8d, 0x46, 0xc6, 0x80, 0x23, 0x63, 0x51, 0x9a, 0x06, + 0x46, 0xd5, 0x13, 0x52, 0x01, 0x54, 0xd4, 0xaa, 0x68, 0x02, 0x40, + 0x6a, 0x40, 0x78, 0xa0, 0x08, 0x59, 0xce, 0xee, 0xb5, 0x2a, 0x39, + 0xd9, 0x59, 0xa7, 0xa8, 0x00, 0x73, 0xeb, 0x4e, 0x0e, 0x7d, 0x69, + 0x5c, 0x05, 0xf3, 0x0f, 0xad, 0x1e, 0x61, 0xf5, 0xa7, 0x71, 0x0b, + 0xe6, 0x35, 0x21, 0x90, 0xd3, 0xb8, 0x0e, 0x32, 0x10, 0x95, 0x10, + 0x91, 0xb3, 0xd6, 0x9b, 0x60, 0x4b, 0x9c, 0x8a, 0x63, 0x1a, 0xb0, + 0x18, 0x4d, 0x46, 0xc6, 0x80, 0x22, 0x6a, 0x61, 0xa4, 0x31, 0xaa, + 0x6a, 0x55, 0x34, 0x01, 0x2a, 0x9a, 0x7e, 0x78, 0xa0, 0x08, 0x09, + 0xf9, 0xaa, 0x58, 0xcf, 0xca, 0x6b, 0x3e, 0xa0, 0x00, 0xd3, 0x81, + 0xa9, 0x01, 0x73, 0x46, 0x69, 0x80, 0xb9, 0xa4, 0xcd, 0x00, 0x2b, + 0x1f, 0x92, 0xa3, 0x07, 0x9a, 0x6f, 0x70, 0x26, 0xcf, 0x14, 0xd2, + 0x6b, 0x51, 0x0c, 0x63, 0x51, 0xb1, 0xa0, 0x08, 0xda, 0x98, 0x69, + 0x0c, 0x8d, 0x4d, 0x4a, 0xa6, 0x80, 0x24, 0x53, 0x52, 0x03, 0xc5, + 0x02, 0x21, 0x27, 0xe6, 0xa9, 0x23, 0x3f, 0x29, 0xac, 0xfa, 0x8c, + 0x01, 0xe6, 0x9c, 0x0d, 0x48, 0x0a, 0x0d, 0x2e, 0x68, 0x01, 0x73, + 0x49, 0x9a, 0x60, 0x2b, 0x1f, 0x92, 0x98, 0x3a, 0xd3, 0x7b, 0x81, + 0x36, 0x78, 0xa6, 0x93, 0x5a, 0x88, 0x8c, 0x9a, 0x63, 0x1a, 0x00, + 0x8c, 0xd3, 0x0d, 0x21, 0x91, 0x29, 0xa9, 0x14, 0xd0, 0x04, 0x8a, + 0x69, 0xe0, 0xd3, 0x11, 0x1b, 0x1e, 0x6a, 0x48, 0xcf, 0xca, 0x6b, + 0x3e, 0xa3, 0x10, 0x1a, 0x70, 0x35, 0x20, 0x38, 0x1a, 0x5c, 0xd2, + 0x01, 0x73, 0x49, 0x9a, 0x60, 0x39, 0x8f, 0xca, 0x29, 0x8b, 0xf7, + 0xaa, 0xba, 0x88, 0x96, 0x9a, 0x6b, 0x40, 0x18, 0xc6, 0xa3, 0x26, + 0x80, 0x18, 0x69, 0xa6, 0x90, 0xc8, 0x14, 0xd4, 0x8a, 0x69, 0x80, + 0xf0, 0x6a, 0x40, 0x68, 0x10, 0xbb, 0x41, 0xa7, 0xe3, 0x0b, 0xc5, + 0x2b, 0x01, 0x10, 0xa7, 0x03, 0x59, 0x0c, 0x76, 0x69, 0x73, 0x40, + 0x0b, 0x9a, 0x28, 0x11, 0x28, 0x19, 0x5e, 0x69, 0x02, 0x81, 0x5a, + 0xd8, 0x00, 0xd3, 0x4d, 0x50, 0x0c, 0x6a, 0x8c, 0xd2, 0x01, 0xa6, + 0x98, 0x69, 0x0c, 0xae, 0xa6, 0xa4, 0x06, 0x80, 0x1e, 0xa6, 0x9e, + 0x0d, 0x31, 0x12, 0x03, 0x4f, 0x06, 0x80, 0x13, 0x60, 0x34, 0xd3, + 0xc1, 0xa8, 0x92, 0x01, 0xf1, 0x8d, 0xdd, 0x69, 0xcc, 0xa1, 0x69, + 0x5b, 0x4b, 0x80, 0x83, 0x93, 0x52, 0x04, 0x14, 0xe2, 0xae, 0x03, + 0xa9, 0x0d, 0x68, 0x03, 0x4d, 0x34, 0xd0, 0x03, 0x0d, 0x30, 0xd2, + 0x01, 0x86, 0x9a, 0x68, 0x19, 0x58, 0x1a, 0x78, 0xa4, 0x04, 0x8a, + 0x69, 0xe0, 0xd3, 0x10, 0xe0, 0x69, 0xe0, 0xd0, 0x03, 0xc1, 0xa8, + 0xdb, 0xad, 0x4c, 0x81, 0x12, 0x45, 0xd6, 0x9d, 0x25, 0x1d, 0x00, + 0x6a, 0xf5, 0xa9, 0xe8, 0x80, 0x31, 0x29, 0x0d, 0x58, 0x08, 0x69, + 0x86, 0x80, 0x1a, 0x69, 0x86, 0x90, 0x0c, 0x34, 0xd3, 0x48, 0x65, + 0x51, 0x4f, 0x06, 0x98, 0x0f, 0x14, 0xf0, 0x68, 0x10, 0xf0, 0x69, + 0xe0, 0xd0, 0x03, 0x81, 0xa5, 0x2b, 0x9a, 0x1a, 0xb8, 0x87, 0xa8, + 0xdb, 0x4a, 0x46, 0x68, 0xb6, 0x80, 0x2a, 0xa8, 0x14, 0xea, 0x12, + 0xb0, 0x05, 0x21, 0xa6, 0x02, 0x1a, 0x61, 0xa0, 0x06, 0x9a, 0x61, + 0xa4, 0x31, 0x86, 0x9a, 0x69, 0x0c, 0xa8, 0x0d, 0x3c, 0x53, 0x01, + 0xe2, 0x9e, 0x28, 0x10, 0xf1, 0x4e, 0x06, 0x98, 0x0f, 0x06, 0x9e, + 0x0d, 0x02, 0x1c, 0x29, 0xc2, 0x80, 0x16, 0x96, 0x80, 0x0a, 0x4a, + 0x00, 0x43, 0x4d, 0x34, 0x0c, 0x61, 0xa6, 0x1a, 0x40, 0x34, 0xd3, + 0x4d, 0x21, 0x80, 0xff, 0xd9, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0a, + 0x07, 0x07, 0x08, 0x07, 0x06, 0x0a, 0x08, 0x08, 0x08, 0x0b, 0x0a, + 0x0a, 0x0b, 0x0e, 0x18, 0x10, 0x0e, 0x0d, 0x0d, 0x0e, 0x1d, 0x15, + 0x16, 0x11, 0x18, 0x23, 0x1f, 0x25, 0x24, 0x22, 0x1f, 0x22, 0x21, + 0x26, 0x2b, 0x37, 0x2f, 0x26, 0x29, 0x34, 0x29, 0x21, 0x22, 0x30, + 0x41, 0x31, 0x34, 0x39, 0x3b, 0x3e, 0x3e, 0x3e, 0x25, 0x2e, 0x44, + 0x49, 0x43, 0x3c, 0x48, 0x37, 0x3d, 0x3e, 0x3b, 0x01, 0x0a, 0x0b, + 0x0b, 0x0e, 0x0d, 0x0e, 0x1c, 0x10, 0x10, 0x1c, 0x3b, 0x28, 0x22, + 0x28, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0xff, 0xc0, 0x00, 0x11, + 0x08, 0x00, 0x48, 0x00, 0x60, 0x03, 0x01, 0x21, 0x00, 0x02, 0x11, + 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, 0x00, 0x01, + 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, + 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, + 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, + 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, + 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, + 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, + 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, + 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, + 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, + 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, 0x03, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, + 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, + 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, + 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, + 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, + 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, + 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, + 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, + 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, + 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, + 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, + 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, + 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x9e, 0xd2, + 0x2e, 0x07, 0x15, 0xaf, 0x6d, 0x08, 0xe2, 0xb3, 0x45, 0x1a, 0xf6, + 0xd0, 0x00, 0x01, 0xc5, 0x68, 0x45, 0x17, 0x4a, 0xb4, 0x22, 0xe4, + 0x70, 0x8c, 0x74, 0xa9, 0x3c, 0xa1, 0x8e, 0x95, 0x48, 0x96, 0x31, + 0xe2, 0x18, 0xe9, 0x55, 0xa5, 0x8c, 0x7a, 0x50, 0x05, 0x0b, 0x88, + 0x86, 0x0f, 0x15, 0x8f, 0x75, 0x1f, 0x26, 0x93, 0x19, 0x91, 0x77, + 0x18, 0xc1, 0xac, 0x4b, 0xc8, 0xfa, 0xd6, 0x63, 0x37, 0x6d, 0x31, + 0xb4, 0x73, 0x5b, 0x36, 0xa0, 0x1c, 0x50, 0x80, 0xd7, 0x83, 0xa0, + 0xab, 0xd1, 0x62, 0xad, 0x09, 0x8f, 0x17, 0x29, 0x03, 0xb2, 0xcc, + 0xe0, 0x77, 0x14, 0xa3, 0x56, 0xb3, 0x27, 0x1e, 0x67, 0xe9, 0x52, + 0xea, 0xc6, 0x3a, 0x36, 0x48, 0xef, 0x3d, 0x27, 0x70, 0x22, 0x60, + 0x47, 0x52, 0x69, 0xb2, 0xe2, 0xad, 0x3b, 0xea, 0x80, 0xa3, 0x38, + 0xe0, 0xd6, 0x3d, 0xd8, 0x1c, 0xd0, 0xca, 0x46, 0x3d, 0xd0, 0x18, + 0x35, 0x89, 0x78, 0xa3, 0x9a, 0xcd, 0x8c, 0xd2, 0xb3, 0x93, 0x2a, + 0x2b, 0x66, 0xd5, 0xf1, 0x8a, 0x10, 0x1a, 0xd6, 0xf2, 0x03, 0x8a, + 0x9e, 0xe6, 0xf4, 0x5a, 0xdb, 0xef, 0xfe, 0x23, 0xc0, 0xa7, 0x27, + 0xcb, 0x16, 0xc4, 0xcc, 0xdd, 0xe2, 0x78, 0x9a, 0x69, 0x66, 0xcc, + 0x99, 0xe1, 0x4d, 0x47, 0xba, 0xbc, 0xd9, 0x6a, 0xee, 0x26, 0x59, + 0x59, 0x4d, 0xac, 0x69, 0x34, 0x52, 0xe5, 0x8f, 0x55, 0xad, 0x58, + 0xae, 0x85, 0xc4, 0x22, 0x41, 0xdf, 0xad, 0x76, 0x61, 0xe5, 0x6f, + 0x74, 0x45, 0x69, 0xdc, 0x00, 0x79, 0xac, 0x8b, 0xa6, 0xc9, 0x35, + 0xd4, 0x34, 0x64, 0xdc, 0x37, 0x06, 0xb1, 0xae, 0x88, 0xc1, 0xac, + 0xd8, 0xc9, 0x2c, 0xa6, 0xe0, 0x73, 0x5b, 0x36, 0xf3, 0x74, 0xe6, + 0x84, 0x05, 0xe3, 0xa9, 0x47, 0x6a, 0x14, 0xb6, 0x49, 0x3d, 0x85, + 0x3a, 0xee, 0xee, 0x2b, 0xa8, 0xe2, 0x6f, 0x30, 0x81, 0xe9, 0x8a, + 0xca, 0xa4, 0xe2, 0xd3, 0x8b, 0x01, 0xb1, 0xf9, 0x04, 0x7f, 0xaf, + 0x23, 0xf0, 0xa9, 0x54, 0x41, 0x9c, 0xfd, 0xa3, 0xf4, 0xae, 0x65, + 0x18, 0xf7, 0x25, 0x8a, 0xe2, 0x02, 0x38, 0xb8, 0xfd, 0x2a, 0x7b, + 0x5b, 0xa8, 0x6d, 0x6d, 0x5d, 0x9a, 0x5d, 0xcb, 0xbb, 0xd2, 0xb6, + 0xa6, 0xa3, 0x19, 0x5e, 0xe2, 0x03, 0x7b, 0x1d, 0xc2, 0x17, 0x8d, + 0xb8, 0xac, 0xfb, 0x89, 0x39, 0x35, 0xd6, 0x9a, 0x6a, 0xe8, 0x66, + 0x55, 0xcb, 0xf5, 0xac, 0x7b, 0x96, 0xeb, 0x50, 0xc6, 0x88, 0x6d, + 0x66, 0xe9, 0xcd, 0x6c, 0xdb, 0x4f, 0xd3, 0x9a, 0x00, 0x2f, 0xe6, + 0xf9, 0xa3, 0xe7, 0xb5, 0x4a, 0x93, 0x7f, 0xa2, 0xc6, 0x73, 0xdc, + 0xd7, 0x15, 0x55, 0xef, 0x48, 0x7d, 0x09, 0x52, 0x6e, 0x3a, 0xd4, + 0xab, 0x2f, 0xbd, 0x61, 0x16, 0x0c, 0x73, 0x49, 0xc5, 0x24, 0x92, + 0x7f, 0xa2, 0x63, 0xfd, 0xaa, 0xd6, 0x2f, 0x71, 0x0e, 0xb1, 0x93, + 0xf7, 0x2d, 0xf5, 0xa4, 0x9e, 0x4e, 0xb5, 0xdd, 0x4b, 0xf8, 0x68, + 0x4c, 0xcb, 0xb9, 0x93, 0xad, 0x65, 0xce, 0xd9, 0x26, 0xa9, 0x8d, + 0x19, 0xf6, 0xf2, 0xf4, 0xe6, 0xb5, 0xad, 0xe7, 0xc6, 0x39, 0xa0, + 0x18, 0xeb, 0xc9, 0x77, 0x6c, 0x35, 0x2a, 0x4b, 0xfe, 0x8a, 0x9c, + 0xff, 0x00, 0x11, 0xae, 0x3a, 0x8b, 0xde, 0x61, 0xd0, 0x9e, 0x39, + 0xb8, 0xeb, 0x53, 0xac, 0xb9, 0xae, 0x5b, 0x00, 0xf3, 0x27, 0x14, + 0x92, 0xc9, 0xfe, 0x8a, 0x3f, 0xde, 0x35, 0xac, 0x3a, 0x88, 0x92, + 0xcd, 0xb1, 0x6e, 0x7d, 0xcd, 0x32, 0x67, 0xeb, 0xcd, 0x7a, 0x14, + 0xfe, 0x04, 0x26, 0x66, 0xce, 0xf9, 0x26, 0xb3, 0xe6, 0x6e, 0xb4, + 0xd9, 0x48, 0xc8, 0x82, 0x4e, 0x07, 0x35, 0xa7, 0x6f, 0x2f, 0x02, + 0x9a, 0x06, 0x5f, 0x8c, 0xa4, 0x83, 0x0e, 0x32, 0x2a, 0x69, 0xe3, + 0xdd, 0x12, 0x08, 0x97, 0x85, 0xec, 0x2a, 0x2a, 0x42, 0xf1, 0x76, + 0x26, 0xe4, 0x6a, 0x59, 0x0e, 0x18, 0x10, 0x6a, 0xd2, 0x89, 0x02, + 0x6e, 0x2a, 0x71, 0xeb, 0x5c, 0x1c, 0x8c, 0xa6, 0x48, 0xbb, 0xdc, + 0x61, 0x41, 0x35, 0x72, 0x28, 0x87, 0xd9, 0xf6, 0x4a, 0xb9, 0xe7, + 0x38, 0xae, 0x8c, 0x3d, 0x36, 0xdd, 0xde, 0xc4, 0xb0, 0x21, 0x51, + 0x76, 0xa8, 0xc0, 0xaa, 0x93, 0x31, 0xe6, 0xbb, 0x2d, 0x65, 0x61, + 0x19, 0xd3, 0x1e, 0xb5, 0x46, 0x5a, 0x96, 0x5a, 0x30, 0xa0, 0x7e, + 0x05, 0x69, 0x5b, 0xc9, 0xc6, 0x28, 0x40, 0xcd, 0x08, 0x64, 0x3c, + 0x73, 0x57, 0xe1, 0x94, 0xf1, 0xcd, 0x5a, 0x21, 0x8c, 0xb9, 0x63, + 0xe7, 0x67, 0x1d, 0xab, 0x40, 0xb1, 0xfb, 0x00, 0x1d, 0xf0, 0x2b, + 0x99, 0x2d, 0x66, 0x3e, 0x88, 0x75, 0x81, 0x3f, 0x31, 0xf6, 0xab, + 0x64, 0xd6, 0xb4, 0x17, 0xee, 0xd0, 0x9e, 0xe4, 0x32, 0x1a, 0xa7, + 0x31, 0xad, 0x18, 0x14, 0x26, 0xef, 0x54, 0xa5, 0xa8, 0x65, 0xa3, + 0x9c, 0x81, 0xfa, 0x56, 0x8c, 0x2d, 0xce, 0x68, 0x40, 0xcb, 0xf1, + 0x37, 0xbd, 0x5e, 0x85, 0xea, 0xd1, 0x0c, 0xbb, 0x19, 0x56, 0x23, + 0x20, 0x1f, 0xad, 0x5c, 0x42, 0x08, 0x03, 0xb5, 0x55, 0x91, 0x04, + 0xc9, 0x80, 0x38, 0x00, 0x0a, 0x71, 0x34, 0x6c, 0x32, 0x27, 0xe9, + 0x55, 0x25, 0x15, 0x2c, 0x68, 0xa3, 0x30, 0xeb, 0x54, 0xa5, 0x15, + 0x0c, 0xd1, 0x00, 0xff, 0xd9}; + + /* package */ static final byte[] sPhotoByteArrayForComplicatedCase; + + static { + final int length = sPhotoIntArrayForComplicatedCase.length; + sPhotoByteArrayForComplicatedCase = new byte[length]; + for (int i = 0; i < length; i++) { + sPhotoByteArrayForComplicatedCase[i] = (byte)sPhotoIntArrayForComplicatedCase[i]; + } + } + + public void testV21SimpleCase1_Parsing() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("N", "Ando;Roid;", Arrays.asList("Ando", "Roid", "")); + verifier.verify(R.raw.v21_simple_1, V21); + } + + public void testV21SimpleCase1_Type_Generic() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + verifier.addImportVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid") + .put(StructuredName.DISPLAY_NAME, "Roid Ando"); + verifier.verify(R.raw.v21_simple_1, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + public void testV21SimpleCase1_Type_Japanese() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + verifier.addImportVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid") + // If name-related strings only contains printable Ascii, + // the order is remained to be US's: + // "Prefix Given Middle Family Suffix" + .put(StructuredName.DISPLAY_NAME, "Roid Ando"); + verifier.verify(R.raw.v21_simple_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS); + } + + public void testV21SimpleCase2() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + verifier.addImportVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Ando Roid"); + verifier.verify(R.raw.v21_simple_2, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + public void testV21SimpleCase3() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + verifier.addImportVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid") + // "FN" field should be prefered since it should contain the original + // order intended by the author of the file. + .put(StructuredName.DISPLAY_NAME, "Ando Roid"); + verifier.verify(R.raw.v21_simple_3, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + /** + * Tests ';' is properly handled by VCardParser implementation. + */ + public void testV21BackslashCase_Parsing() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1") + .addNodeWithOrder("N", ";A;B\\;C\\;;D;:E;\\\\;", + Arrays.asList("", "A;B\\", "C\\;", "D", ":E", "\\\\", "")) + .addNodeWithOrder("FN", "A;B\\C\\;D:E\\\\"); + verifier.verify(R.raw.v21_backslash, V21); + } + + /** + * Tests ContactStruct correctly ignores redundant fields in "N" property values and + * inserts name related data. + */ + public void testV21BackslashCase() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + verifier.addImportVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + // FAMILY_NAME is empty and removed in this test... + .put(StructuredName.GIVEN_NAME, "A;B\\") + .put(StructuredName.MIDDLE_NAME, "C\\;") + .put(StructuredName.PREFIX, "D") + .put(StructuredName.SUFFIX, ":E") + .put(StructuredName.DISPLAY_NAME, "A;B\\C\\;D:E\\\\"); + verifier.verify(R.raw.v21_backslash, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + public void testOrgBeforTitle() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Normal Guy"); + + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Company") + .put(Organization.DEPARTMENT, "Organization Devision Room Sheet No.") + .put(Organization.TITLE, "Excellent Janitor") + .put(Organization.TYPE, Organization.TYPE_WORK); + verifier.verify(R.raw.v21_org_before_title, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + public void testTitleBeforOrg() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Nice Guy"); + + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Marverous") + .put(Organization.DEPARTMENT, "Perfect Great Good Bad Poor") + .put(Organization.TITLE, "Cool Title") + .put(Organization.TYPE, Organization.TYPE_WORK); + verifier.verify(R.raw.v21_title_before_org, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + /** + * Verifies that vCard importer correctly interpret "PREF" attribute to IS_PRIMARY. + * The data contain three cases: one "PREF", no "PREF" and multiple "PREF", in each type. + */ + public void testV21PrefToIsPrimary() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Smith"); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1") + .put(Phone.TYPE, Phone.TYPE_HOME); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "2") + .put(Phone.TYPE, Phone.TYPE_WORK) + .put(Phone.IS_PRIMARY, 1); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "3") + .put(Phone.TYPE, Phone.TYPE_ISDN); + + elem.addExpected(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "test@example.com") + .put(Email.TYPE, Email.TYPE_HOME) + .put(Email.IS_PRIMARY, 1); + + elem.addExpected(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "test2@examination.com") + .put(Email.TYPE, Email.TYPE_MOBILE) + .put(Email.IS_PRIMARY, 1); + + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Company") + .put(Organization.TITLE, "Engineer") + .put(Organization.TYPE, Organization.TYPE_WORK); + + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Mystery") + .put(Organization.TITLE, "Blogger") + .put(Organization.TYPE, Organization.TYPE_WORK); + + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Poetry") + .put(Organization.TITLE, "Poet") + .put(Organization.TYPE, Organization.TYPE_WORK); + verifier.verify(R.raw.v21_pref_handling, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + /** + * Tests all the properties in a complicated vCard are correctly parsed by the VCardParser. + */ + public void testV21ComplicatedCase_Parsing() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1") + .addNodeWithOrder("N", "Gump;Forrest;Hoge;Pos;Tao", + Arrays.asList("Gump", "Forrest", "Hoge", "Pos", "Tao")) + .addNodeWithOrder("FN", "Joe Due") + .addNodeWithOrder("ORG", "Gump Shrimp Co.;Sales Dept.;Manager;Fish keeper", + Arrays.asList("Gump Shrimp Co.", "Sales Dept.;Manager", "Fish keeper")) + .addNodeWithOrder("ROLE", "Fish Cake Keeper!") + .addNodeWithOrder("TITLE", "Shrimp Man") + .addNodeWithOrder("X-CLASS", "PUBLIC") + .addNodeWithOrder("TEL", "(111) 555-1212", new TypeSet("WORK", "VOICE")) + .addNodeWithOrder("TEL", "(404) 555-1212", new TypeSet("HOME", "VOICE")) + .addNodeWithOrder("TEL", "0311111111", new TypeSet("CELL")) + .addNodeWithOrder("TEL", "0322222222", new TypeSet("VIDEO")) + .addNodeWithOrder("TEL", "0333333333", new TypeSet("VOICE")) + .addNodeWithOrder("ADR", + ";;100 Waters Edge;Baytown;LA;30314;United States of America", + Arrays.asList("", "", "100 Waters Edge", "Baytown", + "LA", "30314", "United States of America"), + null, null, new TypeSet("WORK"), null) + .addNodeWithOrder("LABEL", + "100 Waters Edge\r\nBaytown, LA 30314\r\nUnited States of America", + null, null, mContentValuesForQP, new TypeSet("WORK"), null) + .addNodeWithOrder("ADR", + ";;42 Plantation St.;Baytown;LA;30314;United States of America", + Arrays.asList("", "", "42 Plantation St.", "Baytown", + "LA", "30314", "United States of America"), null, null, + new TypeSet("HOME"), null) + .addNodeWithOrder("LABEL", + "42 Plantation St.\r\nBaytown, LA 30314\r\nUnited States of America", + null, null, mContentValuesForQP, + new TypeSet("HOME"), null) + .addNodeWithOrder("EMAIL", "forrestgump@walladalla.com", + new TypeSet("PREF", "INTERNET")) + .addNodeWithOrder("EMAIL", "cell@example.com", new TypeSet("CELL")) + .addNodeWithOrder("NOTE", "The following note is the example from RFC 2045.") + .addNodeWithOrder("NOTE", + "Now's the time for all folk to come to the aid of their country.", + null, null, mContentValuesForQP, null, null) + .addNodeWithOrder("PHOTO", null, + null, sPhotoByteArrayForComplicatedCase, mContentValuesForBase64V21, + new TypeSet("JPEG"), null) + .addNodeWithOrder("X-ATTRIBUTE", "Some String") + .addNodeWithOrder("BDAY", "19800101") + .addNodeWithOrder("GEO", "35.6563854,139.6994233") + .addNodeWithOrder("URL", "http://www.example.com/") + .addNodeWithOrder("REV", "20080424T195243Z"); + verifier.verify(R.raw.v21_complicated, V21); + } + + /** + * Checks ContactStruct correctly inserts values in a complicated vCard + * into ContentResolver. + */ + public void testV21ComplicatedCase() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Gump") + .put(StructuredName.GIVEN_NAME, "Forrest") + .put(StructuredName.MIDDLE_NAME, "Hoge") + .put(StructuredName.PREFIX, "Pos") + .put(StructuredName.SUFFIX, "Tao") + .put(StructuredName.DISPLAY_NAME, "Joe Due"); + + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.TYPE, Organization.TYPE_WORK) + .put(Organization.COMPANY, "Gump Shrimp Co.") + .put(Organization.DEPARTMENT, "Sales Dept.;Manager Fish keeper") + .put(Organization.TITLE, "Shrimp Man"); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_WORK) + // Phone number is expected to be formated with NAMP format in default. + .put(Phone.NUMBER, "111-555-1212"); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_HOME) + .put(Phone.NUMBER, "404-555-1212"); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_MOBILE) + .put(Phone.NUMBER, "031-111-1111"); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VIDEO") + .put(Phone.NUMBER, "032-222-2222"); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VOICE") + .put(Phone.NUMBER, "033-333-3333"); + + elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) + .put(StructuredPostal.COUNTRY, "United States of America") + .put(StructuredPostal.POSTCODE, "30314") + .put(StructuredPostal.REGION, "LA") + .put(StructuredPostal.CITY, "Baytown") + .put(StructuredPostal.STREET, "100 Waters Edge") + .put(StructuredPostal.FORMATTED_ADDRESS, + "100 Waters Edge Baytown LA 30314 United States of America"); + + elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME) + .put(StructuredPostal.COUNTRY, "United States of America") + .put(StructuredPostal.POSTCODE, "30314") + .put(StructuredPostal.REGION, "LA") + .put(StructuredPostal.CITY, "Baytown") + .put(StructuredPostal.STREET, "42 Plantation St.") + .put(StructuredPostal.FORMATTED_ADDRESS, + "42 Plantation St. Baytown LA 30314 United States of America"); + + elem.addExpected(Email.CONTENT_ITEM_TYPE) + // "TYPE=INTERNET" -> TYPE_CUSTOM + the label "INTERNET" + .put(Email.TYPE, Email.TYPE_CUSTOM) + .put(Email.LABEL, "INTERNET") + .put(Email.DATA, "forrestgump@walladalla.com") + .put(Email.IS_PRIMARY, 1); + + elem.addExpected(Email.CONTENT_ITEM_TYPE) + .put(Email.TYPE, Email.TYPE_MOBILE) + .put(Email.DATA, "cell@example.com"); + + elem.addExpected(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "The following note is the example from RFC 2045."); + + elem.addExpected(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, + "Now's the time for all folk to come to the aid of their country."); + + elem.addExpected(Photo.CONTENT_ITEM_TYPE) + // No information about its image format can be inserted. + .put(Photo.PHOTO, sPhotoByteArrayForComplicatedCase); + + elem.addExpected(Event.CONTENT_ITEM_TYPE) + .put(Event.START_DATE, "19800101") + .put(Event.TYPE, Event.TYPE_BIRTHDAY); + + elem.addExpected(Website.CONTENT_ITEM_TYPE) + .put(Website.URL, "http://www.example.com/") + .put(Website.TYPE, Website.TYPE_HOMEPAGE); + verifier.verify(R.raw.v21_complicated, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + public void testV30Simple_Parsing() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "3.0") + .addNodeWithOrder("FN", "And Roid") + .addNodeWithOrder("N", "And;Roid;;;", Arrays.asList("And", "Roid", "", "", "")) + .addNodeWithOrder("ORG", "Open;Handset; Alliance", + Arrays.asList("Open", "Handset", " Alliance")) + .addNodeWithOrder("SORT-STRING", "android") + .addNodeWithOrder("TEL", "0300000000", new TypeSet("PREF", "VOICE")) + .addNodeWithOrder("CLASS", "PUBLIC") + .addNodeWithOrder("X-GNO", "0") + .addNodeWithOrder("X-GN", "group0") + .addNodeWithOrder("X-REDUCTION", "0") + .addNodeWithOrder("REV", "20081031T065854Z"); + verifier.verify(R.raw.v30_simple, V30); + } + + public void testV30Simple() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "And") + .put(StructuredName.GIVEN_NAME, "Roid") + .put(StructuredName.DISPLAY_NAME, "And Roid") + .put(StructuredName.PHONETIC_GIVEN_NAME, "android"); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Open") + .put(Organization.DEPARTMENT, "Handset Alliance") + .put(Organization.TYPE, Organization.TYPE_WORK); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VOICE") + .put(Phone.NUMBER, "030-000-0000") + .put(Phone.IS_PRIMARY, 1); + verifier.verify(R.raw.v30_simple, VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8); + } + + public void testV21Japanese1_Parsing() throws IOException, VCardException { + // Though Japanese careers append ";;;;" at the end of the value of "SOUND", + // vCard 2.1/3.0 specification does not allow multiple values. + // Do not need to handle it as multiple values. + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1", null, null, null, null, null) + .addNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addNodeWithOrder("TEL", "0300000000", null, null, null, + new TypeSet("VOICE", "PREF"), null); + verifier.verify(R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS); + } + + private void testV21Japanese1Common(int resId, int vcardType, boolean japanese) + throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9") + // While vCard parser does not split "SOUND" property values, + // ContactStruct care it. + .put(StructuredName.PHONETIC_FAMILY_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E"); + + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + // Phone number formatting is different. + .put(Phone.NUMBER, (japanese ? "03-0000-0000" : "030-000-0000")) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VOICE") + .put(Phone.IS_PRIMARY, 1); + verifier.verify(resId, vcardType); + } + + /** + * Verifies vCard with Japanese can be parsed correctly with + * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_GENERIC_UTF8}. + */ + public void testV21Japanese1_Type_Generic_Utf8() throws IOException, VCardException { + testV21Japanese1Common( + R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, false); + } + + /** + * Verifies vCard with Japanese can be parsed correctly with + * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE_SJIS}. + */ + public void testV21Japanese1_Type_Japanese_Sjis() throws IOException, VCardException { + testV21Japanese1Common( + R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, true); + } + + /** + * Verifies vCard with Japanese can be parsed correctly with + * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE_UTF8}. + * since vCard 2.1 specifies the charset of each line if it contains non-Ascii. + */ + public void testV21Japanese1_Type_Japanese_Utf8() throws IOException, VCardException { + testV21Japanese1Common( + R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8, true); + } + + public void testV21Japanese2_Parsing() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1") + .addNodeWithOrder("N", "\u5B89\u85E4;\u30ED\u30A4\u30C9\u0031;;;", + Arrays.asList("\u5B89\u85E4", "\u30ED\u30A4\u30C9\u0031", + "", "", ""), + null, mContentValuesForSJis, null, null) + .addNodeWithOrder("FN", "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9\u0020\u0031", + null, null, mContentValuesForSJis, null, null) + .addNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73;\uFF9B\uFF72\uFF84\uFF9E\u0031;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addNodeWithOrder("ADR", + ";\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC\u0036" + + "\u968E;;;;150-8512;", + Arrays.asList("", + "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + + "\u0036\u968E", "", "", "", "150-8512", ""), + null, mContentValuesForQPAndSJis, new TypeSet("HOME"), null) + .addNodeWithOrder("NOTE", "\u30E1\u30E2", null, null, + mContentValuesForQPAndSJis, null, null); + verifier.verify(R.raw.v21_japanese_2, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS); + } + + public void testV21Japanese2_Type_Generic_Utf8() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4") + .put(StructuredName.GIVEN_NAME, "\u30ED\u30A4\u30C9\u0031") + .put(StructuredName.DISPLAY_NAME, + "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9\u0020\u0031") + // ContactStruct should correctly split "SOUND" property into several elements, + // even though VCardParser side does not care it. + .put(StructuredName.PHONETIC_FAMILY_NAME, "\uFF71\uFF9D\uFF84\uFF9E\uFF73") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\uFF9B\uFF72\uFF84\uFF9E\u0031"); + + elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POSTCODE, "150-8512") + .put(StructuredPostal.NEIGHBORHOOD, + "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + + "\u0036\u968E") + .put(StructuredPostal.FORMATTED_ADDRESS, + "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + + "\u0036\u968E 150-8512") + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME); + elem.addExpected(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "\u30E1\u30E2"); + verifier.verify(R.raw.v21_japanese_2, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + } + + public void testV21MultipleEntryCase_Parse() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1") + .addNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0033", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0033;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addNodeWithOrder("TEL", "9", new TypeSet("X-NEC-SECRET")) + .addNodeWithOrder("TEL", "10", new TypeSet("X-NEC-HOTEL")) + .addNodeWithOrder("TEL", "11", new TypeSet("X-NEC-SCHOOL")) + .addNodeWithOrder("TEL", "12", new TypeSet("FAX", "HOME")); + + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1") + .addNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0034", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0034;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addNodeWithOrder("TEL", "13", new TypeSet("MODEM")) + .addNodeWithOrder("TEL", "14", new TypeSet("PAGER")) + .addNodeWithOrder("TEL", "15", new TypeSet("X-NEC-FAMILY")) + .addNodeWithOrder("TEL", "16", new TypeSet("X-NEC-GIRL")); + + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1") + .addNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0035", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0035;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addNodeWithOrder("TEL", "17", new TypeSet("X-NEC-BOY")) + .addNodeWithOrder("TEL", "18", new TypeSet("X-NEC-FRIEND")) + .addNodeWithOrder("TEL", "19", new TypeSet("X-NEC-PHS")) + .addNodeWithOrder("TEL", "20", new TypeSet("X-NEC-RESTAURANT")); + verifier.verify(R.raw.v21_multiple_entry, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS); + } + + public void testV21MultipleEntryCase() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033") + .put(StructuredName.PHONETIC_FAMILY_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0033"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-SECRET") + .put(Phone.NUMBER, "9"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-HOTEL") + .put(Phone.NUMBER, "10"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-SCHOOL") + .put(Phone.NUMBER, "11"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_FAX_HOME) + .put(Phone.NUMBER, "12"); + + elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034") + .put(StructuredName.PHONETIC_FAMILY_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0034"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "MODEM") + .put(Phone.NUMBER, "13"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_PAGER) + .put(Phone.NUMBER, "14"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-FAMILY") + .put(Phone.NUMBER, "15"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-GIRL") + .put(Phone.NUMBER, "16"); + + elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035") + .put(StructuredName.PHONETIC_FAMILY_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0035"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-BOY") + .put(Phone.NUMBER, "17"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-FRIEND") + .put(Phone.NUMBER, "18"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-PHS") + .put(Phone.NUMBER, "19"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-RESTAURANT") + .put(Phone.NUMBER, "20"); + verifier.verify(R.raw.v21_multiple_entry, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS); + } + + public void testIgnoreAgentV21_Parse() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + ContentValues contentValuesForValue = new ContentValues(); + contentValuesForValue.put("VALUE", "DATE"); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "2.1") + .addNodeWithOrder("N", Arrays.asList("Example", "", "", "", "")) + .addNodeWithOrder("FN", "Example") + .addNodeWithOrder("ANNIVERSARY", "20091010", contentValuesForValue) + .addNodeWithOrder("AGENT", "") + .addNodeWithOrder("X-CLASS", "PUBLIC") + .addNodeWithOrder("X-REDUCTION", "") + .addNodeWithOrder("X-NO", ""); + + // Only scan mode lets vCard parser accepts invalid AGENT lines like above. + verifier.verify(R.raw.v21_winmo_65, V21, + new VCardParser_V21(VCardParser.PARSER_MODE_SCAN)); + } + + public void testIgnoreAgentV21() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Example") + .put(StructuredName.DISPLAY_NAME, "Example"); + verifier.verify(R.raw.v21_winmo_65, V21, + new VCardParser_V21(VCardParser.PARSER_MODE_SCAN)); + } + + public void testPagerV30_Parse() throws IOException, VCardException { + PropertyNodesVerifier verifier = new PropertyNodesVerifier(this); + verifier.addPropertyNodesVerifierElem() + .addNodeWithOrder("VERSION", "3.0") + .addNodeWithOrder("N", Arrays.asList("F", "G", "M", "", "")) + .addNodeWithOrder("TEL", "6101231234@pagersample.com", + new TypeSet("WORK", "MSG", "PAGER")); + verifier.verify(R.raw.v30_comma_separated, VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8); + } + + public void testPagerV30() throws IOException, VCardException { + ImportVerifier verifier = new ImportVerifier(); + ImportVerifierElem elem = verifier.addImportVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "F") + .put(StructuredName.MIDDLE_NAME, "M") + .put(StructuredName.GIVEN_NAME, "G") + .put(StructuredName.DISPLAY_NAME, "G M F"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_PAGER) + .put(Phone.NUMBER, "6101231234@pagersample.com"); + verifier.verify(R.raw.v30_comma_separated, VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8); + } +} diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardJapanizationTests.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardJapanizationTests.java new file mode 100644 index 0000000..4b65008 --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardJapanizationTests.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests.vcard; + +import android.content.ContentValues; +import android.pim.vcard.VCardConfig; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; + +import com.android.unit_tests.vcard.PropertyNodesVerifierElem.TypeSet; + +import java.util.Arrays; + +public class VCardJapanizationTests extends VCardTestsBase { + private void testNameUtf8Common(int vcardType) { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") + .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") + .put(StructuredName.MIDDLE_NAME, "B") + .put(StructuredName.PREFIX, "Dr.") + .put(StructuredName.SUFFIX, "Ph.D"); + + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); + ContentValues contentValues = + (VCardConfig.isV30(vcardType) ? null : mContentValuesForQPAndUtf8); + verifier.addPropertyNodesVerifierElem() + .addNodeWithoutOrder("FN", "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D", + contentValues) + .addNodeWithoutOrder("N", "\u3075\u308B\u3069;\u3091\u308A\u304B;B;Dr.;Ph.D", + Arrays.asList( + "\u3075\u308B\u3069", "\u3091\u308A\u304B", "B", "Dr.", "Ph.D"), + null, contentValues, null, null); + verifier.verify(); + } + + public void testNameUtf8V21() { + testNameUtf8Common(VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8); + } + + public void testNameUtf8V30() { + testNameUtf8Common(VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8); + } + + public void testNameShiftJis() { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") + .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") + .put(StructuredName.MIDDLE_NAME, "B") + .put(StructuredName.PREFIX, "Dr.") + .put(StructuredName.SUFFIX, "Ph.D"); + + VCardVerifier verifier = new VCardVerifier(resolver, + VCardConfig.VCARD_TYPE_V30_JAPANESE_SJIS); + verifier.addPropertyNodesVerifierElem() + .addNodeWithoutOrder("FN", "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D", + mContentValuesForSJis) + .addNodeWithoutOrder("N", "\u3075\u308B\u3069;\u3091\u308A\u304B;B;Dr.;Ph.D", + Arrays.asList( + "\u3075\u308B\u3069", "\u3091\u308A\u304B", "B", "Dr.", "Ph.D"), + null, mContentValuesForSJis, null, null); + verifier.verify(); + } + + /** + * DoCoMo phones require all name elements should be in "family name" field. + */ + public void testNameDoCoMo() { + ExportTestResolver resolver = new ExportTestResolver(); + resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") + .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") + .put(StructuredName.MIDDLE_NAME, "B") + .put(StructuredName.PREFIX, "Dr.") + .put(StructuredName.SUFFIX, "Ph.D"); + + VCardVerifier verifier = new VCardVerifier(resolver, + VCardConfig.VCARD_TYPE_DOCOMO); + final String fullName = "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D"; + verifier.addPropertyNodesVerifierElem() + .addNodeWithoutOrder("N", fullName + ";;;;", + Arrays.asList(fullName, "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addNodeWithoutOrder("FN", fullName, mContentValuesForSJis) + .addNodeWithoutOrder("SOUND", ";;;;", new TypeSet("X-IRMC-N")) + .addNodeWithoutOrder("TEL", "", new TypeSet("HOME")) + .addNodeWithoutOrder("EMAIL", "", new TypeSet("HOME")) + .addNodeWithoutOrder("ADR", "", new TypeSet("HOME")) + .addNodeWithoutOrder("X-CLASS", "PUBLIC") + .addNodeWithoutOrder("X-REDUCTION", "") + .addNodeWithoutOrder("X-NO", "") + .addNodeWithoutOrder("X-DCM-HMN-MODE", ""); + verifier.verify(); + } +}
\ No newline at end of file diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTestSuite.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTestSuite.java new file mode 100644 index 0000000..f3d1c5e --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTestSuite.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests.vcard; + +import com.android.unit_tests.AndroidTests; + +import android.test.suitebuilder.TestSuiteBuilder; + +import junit.framework.TestSuite; + +public class VCardTestSuite extends TestSuite { + public static TestSuite suite() { + TestSuiteBuilder suiteBuilder = new TestSuiteBuilder(AndroidTests.class); + suiteBuilder.includeAllPackagesUnderHere(); + return suiteBuilder.build(); + } +}
\ No newline at end of file diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTests.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTests.java deleted file mode 100644 index 7589ba8..0000000 --- a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTests.java +++ /dev/null @@ -1,923 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.unit_tests.vcard; - -import android.content.ContentValues; -import android.pim.vcard.ContactStruct; -import android.pim.vcard.EntryHandler; -import android.pim.vcard.VCardParser_V21; -import android.pim.vcard.VCardParser_V30; -import android.pim.vcard.exception.VCardException; -import android.test.AndroidTestCase; - -import com.android.unit_tests.R; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; - -public class VCardTests extends AndroidTestCase { - - // TODO: Use EntityIterator, which is added in Eclair. - private static class EntryHolder implements EntryHandler { - public List<ContactStruct> contacts = new ArrayList<ContactStruct>(); - public void onParsingStart() { - } - public void onEntryCreated(ContactStruct contactStruct) { - contacts.add(contactStruct); - } - public void onParsingEnd() { - } - } - /* - static void verify(ContactStruct expected, ContactStruct actual) { - if (!equalsString(expected.getName(), actual.getName())) { - fail(String.format("Names do not equal: \"%s\" != \"%s\"", - expected.getName(), actual.getName())); - } - if (!equalsString( - expected.getPhoneticName(), actual.getPhoneticName())) { - fail(String.format("Phonetic names do not equal: \"%s\" != \"%s\"", - expected.getPhoneticName(), actual.getPhoneticName())); - } - { - final byte[] expectedPhotoBytes = expected.getPhotoBytes(); - final byte[] actualPhotoBytes = actual.getPhotoBytes(); - if (!((expectedPhotoBytes == null && actualPhotoBytes == null) || - Arrays.equals(expectedPhotoBytes, actualPhotoBytes))) { - fail("photoBytes is not equal."); - } - } - verifyInternal(expected.getNotes(), actual.getNotes(), "notes"); - verifyInternal(expected.getPhoneList(), actual.getPhoneList(), "phones"); - verifyInternal(expected.getContactMethodList(), actual.getContactMethodList(), - "contact lists"); - verifyInternal(expected.getOrganizationList(), actual.getOrganizationList(), - "organizations"); - { - final Map<String, List<String>> expectedMap = - expected.getExtensionMap(); - final Map<String, List<String>> actualMap = - actual.getExtensionMap(); - if (verifySize((expectedMap == null ? 0 : expectedMap.size()), - (actualMap == null ? 0 : actualMap.size()), "extensions") > 0) { - for (String key : expectedMap.keySet()) { - if (!actualMap.containsKey(key)) { - fail(String.format( - "Actual does not have %s extension while expected has", - key)); - } - final List<String> expectedList = expectedMap.get(key); - final List<String> actualList = actualMap.get(key); - verifyInternal(expectedList, actualList, - String.format("extension \"%s\"", key)); - } - } - } - } - - private static boolean equalsString(String a, String b) { - if (a == null || a.length() == 0) { - return b == null || b.length() == 0; - } else { - return a.equals(b); - } - } - - private static int verifySize(int expectedSize, int actualSize, String name) { - if (expectedSize != actualSize) { - fail(String.format("Size of %s is different: %d != %d", - name, expectedSize, actualSize)); - } - return expectedSize; - } - - private static <T> void verifyInternal(final List<T> expected, final List<T> actual, - String name) { - if(verifySize((expected == null ? 0 : expected.size()), - (actual == null ? 0 : actual.size()), name) > 0) { - int size = expected.size(); - for (int i = 0; i < size; i++) { - final T expectedObj = expected.get(i); - final T actualObj = actual.get(i); - if (!expected.equals(actual)) { - fail(String.format("The %i %s are different: %s != %s", - i, name, expectedObj, actualObj)); - } - } - } - }*/ - - private class PropertyNodesVerifier { - private HashMap<String, ArrayList<PropertyNode>> mPropertyNodeMap; - public PropertyNodesVerifier(PropertyNode... nodes) { - mPropertyNodeMap = new HashMap<String, ArrayList<PropertyNode>>(); - for (PropertyNode propertyNode : nodes) { - String propName = propertyNode.propName; - ArrayList<PropertyNode> expectedNodes = - mPropertyNodeMap.get(propName); - if (expectedNodes == null) { - expectedNodes = new ArrayList<PropertyNode>(); - mPropertyNodeMap.put(propName, expectedNodes); - } - expectedNodes.add(propertyNode); - } - } - - public void verify(VNode vnode) { - for (PropertyNode propertyNode : vnode.propList) { - String propName = propertyNode.propName; - ArrayList<PropertyNode> nodes = mPropertyNodeMap.get(propName); - if (nodes == null) { - fail("Unexpected propName \"" + propName + "\" exists."); - } - boolean successful = false; - int size = nodes.size(); - for (int i = 0; i < size; i++) { - PropertyNode expectedNode = nodes.get(i); - if (expectedNode.propName.equals(propName)) { - if (expectedNode.equals(propertyNode)) { - successful = true; - nodes.remove(i); - if (nodes.size() == 0) { - mPropertyNodeMap.remove(propName); - } - break; - } else { - fail("Property \"" + propName + "\" has wrong value.\n" - + "expected: " + expectedNode.toString() - + "\n actual: " + propertyNode.toString()); - } - } - } - if (!successful) { - fail("Unexpected property \"" + propName + "\" exists."); - } - } - if (mPropertyNodeMap.size() != 0) { - ArrayList<String> expectedProps = new ArrayList<String>(); - for (ArrayList<PropertyNode> nodes : mPropertyNodeMap.values()) { - for (PropertyNode node : nodes) { - expectedProps.add(node.propName); - } - } - fail("expected props " + Arrays.toString(expectedProps.toArray()) + - " was not found"); - } - } - } - - /* - public void testV21SimpleCase1_1() throws IOException, VCardException { - VCardParser parser = new VCardParser_V21(); - VCardDataBuilder builder = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_ENGLISH); - EntryHolder holder = new EntryHolder(); - builder.addEntryHandler(holder); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_1); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, holder.contacts.size()); - verify(new ContactStruct("Roid Ando", null, - null, null, null, null, null, null), - holder.contacts.get(0)); - } - - public void testV21SimpleCase1_2() throws IOException, VCardException { - VCardParser parser = new VCardParser_V21(); - VCardDataBuilder builder = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_JAPANESE); - EntryHolder holder = new EntryHolder(); - builder.addEntryHandler(holder); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_1); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, holder.contacts.size()); - verify(new ContactStruct("Ando Roid", null, - null, null, null, null, null, null), - holder.contacts.get(0)); - } - - public void testV21SimpleCase2() throws IOException, VCardException { - VCardParser parser = new VCardParser_V21(); - VCardDataBuilder builder = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_ENGLISH); - EntryHolder holder = new EntryHolder(); - builder.addEntryHandler(holder); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_2); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, holder.contacts.size()); - verify(new ContactStruct("Ando Roid", null, - null, null, null, null, null, null), - holder.contacts.get(0)); - } - - public void testV21SimpleCase3() throws IOException, VCardException { - VCardParser parser = new VCardParser_V21(); - VCardDataBuilder builder1 = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_ENGLISH); - EntryHolder holder = new EntryHolder(); - builder1.addEntryHandler(holder); - VNodeBuilder builder2 = new VNodeBuilder(); - VCardBuilderCollection collection = - new VCardBuilderCollection( - new ArrayList<VCardBuilder>(Arrays.asList(builder1, builder2))); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_3); - assertEquals(true, parser.parse(is,"ISO-8859-1", collection)); - is.close(); - - assertEquals(1, builder2.vNodeList.size()); - VNode vnode = builder2.vNodeList.get(0); - PropertyNodesVerifier verifier = new PropertyNodesVerifier( - new PropertyNode("N", "Ando;Roid;", - Arrays.asList("Ando", "Roid", ""), - null, null, null, null), - new PropertyNode("FN", "Ando Roid", - null, null, null, null, null)); - verifier.verify(vnode); - - // FN is prefered. - assertEquals(1, holder.contacts.size()); - ContactStruct actual = holder.contacts.get(0); - verify(new ContactStruct("Ando Roid", null, - null, null, null, null, null, null), - actual); - }*/ - - public void testV21BackslashCase() throws IOException, VCardException { - VCardParser_V21 parser = new VCardParser_V21(); - VNodeBuilder builder = new VNodeBuilder(); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_backslash); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, builder.vNodeList.size()); - PropertyNodesVerifier verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "2.1", - null, null, null, null, null), - new PropertyNode("N", ";A;B\\;C\\;;D;:E;\\\\;", - Arrays.asList("", "A;B\\", "C\\;", "D", ":E", "\\\\", ""), - null, null, null, null), - new PropertyNode("FN", "A;B\\C\\;D:E\\\\", - null, null, null, null, null)); - verifier.verify(builder.vNodeList.get(0)); - } - - public void testV21ComplicatedCase() throws IOException, VCardException { - VCardParser_V21 parser = new VCardParser_V21(); - VNodeBuilder builder = new VNodeBuilder(); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_complicated); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, builder.vNodeList.size()); - ContentValues contentValuesForQP = new ContentValues(); - contentValuesForQP.put("ENCODING", "QUOTED-PRINTABLE"); - ContentValues contentValuesForPhoto = new ContentValues(); - contentValuesForPhoto.put("ENCODING", "BASE64"); - // Push data into int array at first since values like 0x80 are - // interpreted as int by the compiler and casting all of them is - // cumbersome... - int[] photoIntArray = { - 0xff, 0xd8, 0xff, 0xe1, 0x0a, 0x0f, 0x45, 0x78, 0x69, 0x66, 0x00, - 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0d, - 0x01, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, - 0xaa, 0x01, 0x0f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0x00, 0xba, 0x01, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0xc2, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0xc8, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x28, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x31, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xd8, 0x01, 0x32, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x13, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x82, - 0x98, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xfa, - 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, - 0x84, 0xc4, 0xa5, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, - 0x01, 0x08, 0x00, 0x00, 0x04, 0x1e, 0x32, 0x30, 0x30, 0x38, 0x31, - 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, 0x00, 0x00, - 0x44, 0x6f, 0x43, 0x6f, 0x4d, 0x6f, 0x00, 0x00, 0x44, 0x39, 0x30, - 0x35, 0x69, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x44, 0x39, 0x30, - 0x35, 0x69, 0x20, 0x56, 0x65, 0x72, 0x31, 0x2e, 0x30, 0x30, 0x00, - 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, - 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x00, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x49, 0x4d, 0x00, 0x30, 0x33, - 0x30, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x14, 0x00, - 0x14, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x34, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x10, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x11, 0x09, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x0f, 0x0b, 0x00, - 0x00, 0x27, 0x10, 0x00, 0x00, 0x05, 0x97, 0x00, 0x00, 0x27, 0x10, - 0x00, 0x00, 0x08, 0xb0, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1c, - 0x01, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x02, 0x5e, 0x00, 0x00, - 0x27, 0x10, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x27, 0x10, 0x00, - 0x00, 0x03, 0xcb, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1b, 0xe5, - 0x00, 0x00, 0x27, 0x10, 0x00, 0x28, 0x82, 0x9a, 0x00, 0x05, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x6a, 0x82, 0x9d, 0x00, 0x05, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x72, 0x88, 0x22, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x90, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x32, 0x32, 0x30, 0x90, - 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, 0x7a, - 0x90, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, - 0x8e, 0x91, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, - 0x03, 0x00, 0x91, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x03, 0xa2, 0x92, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x03, 0xaa, 0x92, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x03, 0xb2, 0x92, 0x04, 0x00, 0x0a, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x03, 0xba, 0x92, 0x05, 0x00, 0x05, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xc2, 0x92, 0x07, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x92, 0x08, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, 0x09, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, - 0x0a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xca, - 0x92, 0x7c, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x92, 0x86, 0x00, 0x07, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, - 0x03, 0xd2, 0xa0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, - 0x31, 0x30, 0x30, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x60, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x48, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0xa2, 0x0e, 0x00, 0x05, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xe8, 0xa2, 0x0f, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xf0, 0xa2, 0x10, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0xa2, - 0x17, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, - 0xa3, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, - 0x00, 0xa3, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, - 0x00, 0x00, 0xa4, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x04, 0x00, 0x05, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x03, 0xf8, 0xa4, 0x05, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x1d, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x07, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x08, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, - 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0xa4, 0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0xa4, 0x0c, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, - 0x00, 0x27, 0x10, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, - 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, - 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x33, 0x31, 0x00, 0x32, 0x30, - 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, 0x31, 0x33, - 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x00, 0x00, 0x29, 0x88, - 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x02, 0xb2, 0x00, 0x00, 0x00, - 0x64, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x25, 0x00, - 0x00, 0x00, 0x0a, 0x00, 0x00, 0x0e, 0x92, 0x00, 0x00, 0x03, 0xe8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x30, 0x30, - 0x38, 0x31, 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, - 0x00, 0x00, 0x20, 0x2a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x2a, - 0xe2, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x04, 0x52, 0x39, 0x38, 0x00, 0x00, 0x02, 0x00, 0x07, 0x00, 0x00, - 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, - 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x04, 0x6c, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x04, 0x74, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x04, 0x7c, 0x02, 0x02, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0x8b, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x00, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, - 0x00, 0x20, 0x16, 0x18, 0x1c, 0x18, 0x14, 0x20, 0x1c, 0x1a, 0x1c, - 0x24, 0x22, 0x20, 0x26, 0x30, 0x50, 0x34, 0x30, 0x2c, 0x2c, 0x30, - 0x62, 0x46, 0x4a, 0x3a, 0x50, 0x74, 0x66, 0x7a, 0x78, 0x72, 0x66, - 0x70, 0x6e, 0x80, 0x90, 0xb8, 0x9c, 0x80, 0x88, 0xae, 0x8a, 0x6e, - 0x70, 0xa0, 0xda, 0xa2, 0xae, 0xbe, 0xc4, 0xce, 0xd0, 0xce, 0x7c, - 0x9a, 0xe2, 0xf2, 0xe0, 0xc8, 0xf0, 0xb8, 0xca, 0xce, 0xc6, 0x01, - 0x22, 0x24, 0x24, 0x30, 0x2a, 0x30, 0x5e, 0x34, 0x34, 0x5e, 0xc6, - 0x84, 0x70, 0x84, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xff, 0xc0, - 0x00, 0x11, 0x08, 0x00, 0x78, 0x00, 0xa0, 0x03, 0x01, 0x21, 0x00, - 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, - 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, - 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, - 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, - 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, - 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, - 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, - 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, - 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, - 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, - 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, - 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, - 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, - 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, - 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, - 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, - 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, - 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, - 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, - 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, - 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, - 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, - 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, - 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, - 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, - 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, - 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, - 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, - 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, - 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, - 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, - 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, - 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, - 0x14, 0x54, 0xaa, 0x2a, 0x46, 0x48, 0xa2, 0xa4, 0x55, 0xa6, 0x04, - 0x8a, 0x29, 0xe0, 0x53, 0x10, 0xe0, 0x29, 0xc0, 0x50, 0x03, 0xb1, - 0x46, 0x29, 0x80, 0x84, 0x52, 0x11, 0x40, 0x0d, 0x22, 0x9a, 0x45, - 0x20, 0x23, 0x61, 0x51, 0x30, 0xa0, 0x08, 0xc8, 0xa8, 0xd8, 0x52, - 0x02, 0x26, 0x15, 0x0b, 0x0a, 0x00, 0xb4, 0xa2, 0xa5, 0x5a, 0x00, - 0x91, 0x45, 0x4a, 0xa2, 0x81, 0x92, 0x01, 0x4e, 0x02, 0x98, 0x87, - 0x0a, 0x70, 0xa0, 0x07, 0x62, 0x8c, 0x50, 0x21, 0x0d, 0x25, 0x00, - 0x34, 0x8a, 0x61, 0x14, 0x0c, 0x63, 0x0a, 0x89, 0x85, 0x00, 0x46, - 0xd5, 0x1b, 0x52, 0x02, 0x16, 0xa8, 0x98, 0x50, 0x05, 0x94, 0xa9, - 0x16, 0x80, 0x25, 0x5a, 0x95, 0x68, 0x18, 0xf1, 0x4f, 0x14, 0xc4, - 0x3b, 0xb5, 0x22, 0xb6, 0x38, 0x34, 0x00, 0xe3, 0x22, 0x8e, 0xf4, - 0x79, 0x8a, 0x7b, 0xd1, 0x71, 0x03, 0x30, 0xc7, 0x14, 0x83, 0xa5, - 0x00, 0x06, 0x98, 0x68, 0x01, 0x8d, 0x51, 0x35, 0x03, 0x22, 0x6a, - 0x8d, 0xa9, 0x01, 0x13, 0x54, 0x4d, 0x40, 0x13, 0xa5, 0x4a, 0x28, - 0x02, 0x45, 0x35, 0x2a, 0x9a, 0x00, 0x78, 0x34, 0xf0, 0x69, 0x80, - 0x34, 0x81, 0x45, 0x40, 0xce, 0x58, 0xe6, 0xa2, 0x4c, 0x06, 0xe4, - 0xfa, 0xd1, 0x93, 0x50, 0x21, 0xca, 0xe4, 0x55, 0x84, 0x90, 0x30, - 0xab, 0x8b, 0x18, 0xa6, 0x9a, 0x6a, 0xc4, 0x31, 0xaa, 0x26, 0xa0, - 0x64, 0x4d, 0x51, 0xb5, 0x20, 0x23, 0x6a, 0x89, 0xa8, 0x02, 0x44, - 0x35, 0x2a, 0x9a, 0x00, 0x95, 0x4d, 0x48, 0xa6, 0x80, 0x24, 0x53, - 0x4e, 0xce, 0x05, 0x30, 0x2b, 0x3b, 0xee, 0x6a, 0x91, 0x5d, 0x76, - 0x63, 0xbd, 0x65, 0x7d, 0x40, 0x66, 0x68, 0xa9, 0x02, 0x45, 0x2b, - 0xb3, 0x9e, 0xb4, 0xc5, 0x6d, 0xad, 0x9a, 0xa0, 0x2c, 0x06, 0xc8, - 0xcd, 0x04, 0xd6, 0xa2, 0x23, 0x63, 0x51, 0xb1, 0xa0, 0x64, 0x4d, - 0x51, 0x93, 0x48, 0x08, 0xda, 0xa2, 0x6a, 0x00, 0x72, 0x1a, 0x99, - 0x4d, 0x00, 0x48, 0xa6, 0xa4, 0x53, 0x4c, 0x07, 0x86, 0x03, 0xbd, - 0x2b, 0x9c, 0xa7, 0x14, 0x98, 0x10, 0x85, 0x34, 0xe0, 0xa6, 0xb3, - 0xb0, 0x0b, 0xb5, 0xa8, 0x0a, 0xd4, 0x58, 0x42, 0xed, 0x3e, 0x94, - 0xd2, 0xa6, 0x8b, 0x01, 0x34, 0x44, 0xed, 0xe6, 0x9c, 0x4d, 0x6a, - 0x80, 0x8d, 0x8d, 0x46, 0xc6, 0x80, 0x23, 0x63, 0x51, 0x9a, 0x06, - 0x46, 0xd5, 0x13, 0x52, 0x01, 0x54, 0xd4, 0xaa, 0x68, 0x02, 0x40, - 0x6a, 0x40, 0x78, 0xa0, 0x08, 0x59, 0xce, 0xee, 0xb5, 0x2a, 0x39, - 0xd9, 0x59, 0xa7, 0xa8, 0x00, 0x73, 0xeb, 0x4e, 0x0e, 0x7d, 0x69, - 0x5c, 0x05, 0xf3, 0x0f, 0xad, 0x1e, 0x61, 0xf5, 0xa7, 0x71, 0x0b, - 0xe6, 0x35, 0x21, 0x90, 0xd3, 0xb8, 0x0e, 0x32, 0x10, 0x95, 0x10, - 0x91, 0xb3, 0xd6, 0x9b, 0x60, 0x4b, 0x9c, 0x8a, 0x63, 0x1a, 0xb0, - 0x18, 0x4d, 0x46, 0xc6, 0x80, 0x22, 0x6a, 0x61, 0xa4, 0x31, 0xaa, - 0x6a, 0x55, 0x34, 0x01, 0x2a, 0x9a, 0x7e, 0x78, 0xa0, 0x08, 0x09, - 0xf9, 0xaa, 0x58, 0xcf, 0xca, 0x6b, 0x3e, 0xa0, 0x00, 0xd3, 0x81, - 0xa9, 0x01, 0x73, 0x46, 0x69, 0x80, 0xb9, 0xa4, 0xcd, 0x00, 0x2b, - 0x1f, 0x92, 0xa3, 0x07, 0x9a, 0x6f, 0x70, 0x26, 0xcf, 0x14, 0xd2, - 0x6b, 0x51, 0x0c, 0x63, 0x51, 0xb1, 0xa0, 0x08, 0xda, 0x98, 0x69, - 0x0c, 0x8d, 0x4d, 0x4a, 0xa6, 0x80, 0x24, 0x53, 0x52, 0x03, 0xc5, - 0x02, 0x21, 0x27, 0xe6, 0xa9, 0x23, 0x3f, 0x29, 0xac, 0xfa, 0x8c, - 0x01, 0xe6, 0x9c, 0x0d, 0x48, 0x0a, 0x0d, 0x2e, 0x68, 0x01, 0x73, - 0x49, 0x9a, 0x60, 0x2b, 0x1f, 0x92, 0x98, 0x3a, 0xd3, 0x7b, 0x81, - 0x36, 0x78, 0xa6, 0x93, 0x5a, 0x88, 0x8c, 0x9a, 0x63, 0x1a, 0x00, - 0x8c, 0xd3, 0x0d, 0x21, 0x91, 0x29, 0xa9, 0x14, 0xd0, 0x04, 0x8a, - 0x69, 0xe0, 0xd3, 0x11, 0x1b, 0x1e, 0x6a, 0x48, 0xcf, 0xca, 0x6b, - 0x3e, 0xa3, 0x10, 0x1a, 0x70, 0x35, 0x20, 0x38, 0x1a, 0x5c, 0xd2, - 0x01, 0x73, 0x49, 0x9a, 0x60, 0x39, 0x8f, 0xca, 0x29, 0x8b, 0xf7, - 0xaa, 0xba, 0x88, 0x96, 0x9a, 0x6b, 0x40, 0x18, 0xc6, 0xa3, 0x26, - 0x80, 0x18, 0x69, 0xa6, 0x90, 0xc8, 0x14, 0xd4, 0x8a, 0x69, 0x80, - 0xf0, 0x6a, 0x40, 0x68, 0x10, 0xbb, 0x41, 0xa7, 0xe3, 0x0b, 0xc5, - 0x2b, 0x01, 0x10, 0xa7, 0x03, 0x59, 0x0c, 0x76, 0x69, 0x73, 0x40, - 0x0b, 0x9a, 0x28, 0x11, 0x28, 0x19, 0x5e, 0x69, 0x02, 0x81, 0x5a, - 0xd8, 0x00, 0xd3, 0x4d, 0x50, 0x0c, 0x6a, 0x8c, 0xd2, 0x01, 0xa6, - 0x98, 0x69, 0x0c, 0xae, 0xa6, 0xa4, 0x06, 0x80, 0x1e, 0xa6, 0x9e, - 0x0d, 0x31, 0x12, 0x03, 0x4f, 0x06, 0x80, 0x13, 0x60, 0x34, 0xd3, - 0xc1, 0xa8, 0x92, 0x01, 0xf1, 0x8d, 0xdd, 0x69, 0xcc, 0xa1, 0x69, - 0x5b, 0x4b, 0x80, 0x83, 0x93, 0x52, 0x04, 0x14, 0xe2, 0xae, 0x03, - 0xa9, 0x0d, 0x68, 0x03, 0x4d, 0x34, 0xd0, 0x03, 0x0d, 0x30, 0xd2, - 0x01, 0x86, 0x9a, 0x68, 0x19, 0x58, 0x1a, 0x78, 0xa4, 0x04, 0x8a, - 0x69, 0xe0, 0xd3, 0x10, 0xe0, 0x69, 0xe0, 0xd0, 0x03, 0xc1, 0xa8, - 0xdb, 0xad, 0x4c, 0x81, 0x12, 0x45, 0xd6, 0x9d, 0x25, 0x1d, 0x00, - 0x6a, 0xf5, 0xa9, 0xe8, 0x80, 0x31, 0x29, 0x0d, 0x58, 0x08, 0x69, - 0x86, 0x80, 0x1a, 0x69, 0x86, 0x90, 0x0c, 0x34, 0xd3, 0x48, 0x65, - 0x51, 0x4f, 0x06, 0x98, 0x0f, 0x14, 0xf0, 0x68, 0x10, 0xf0, 0x69, - 0xe0, 0xd0, 0x03, 0x81, 0xa5, 0x2b, 0x9a, 0x1a, 0xb8, 0x87, 0xa8, - 0xdb, 0x4a, 0x46, 0x68, 0xb6, 0x80, 0x2a, 0xa8, 0x14, 0xea, 0x12, - 0xb0, 0x05, 0x21, 0xa6, 0x02, 0x1a, 0x61, 0xa0, 0x06, 0x9a, 0x61, - 0xa4, 0x31, 0x86, 0x9a, 0x69, 0x0c, 0xa8, 0x0d, 0x3c, 0x53, 0x01, - 0xe2, 0x9e, 0x28, 0x10, 0xf1, 0x4e, 0x06, 0x98, 0x0f, 0x06, 0x9e, - 0x0d, 0x02, 0x1c, 0x29, 0xc2, 0x80, 0x16, 0x96, 0x80, 0x0a, 0x4a, - 0x00, 0x43, 0x4d, 0x34, 0x0c, 0x61, 0xa6, 0x1a, 0x40, 0x34, 0xd3, - 0x4d, 0x21, 0x80, 0xff, 0xd9, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0a, - 0x07, 0x07, 0x08, 0x07, 0x06, 0x0a, 0x08, 0x08, 0x08, 0x0b, 0x0a, - 0x0a, 0x0b, 0x0e, 0x18, 0x10, 0x0e, 0x0d, 0x0d, 0x0e, 0x1d, 0x15, - 0x16, 0x11, 0x18, 0x23, 0x1f, 0x25, 0x24, 0x22, 0x1f, 0x22, 0x21, - 0x26, 0x2b, 0x37, 0x2f, 0x26, 0x29, 0x34, 0x29, 0x21, 0x22, 0x30, - 0x41, 0x31, 0x34, 0x39, 0x3b, 0x3e, 0x3e, 0x3e, 0x25, 0x2e, 0x44, - 0x49, 0x43, 0x3c, 0x48, 0x37, 0x3d, 0x3e, 0x3b, 0x01, 0x0a, 0x0b, - 0x0b, 0x0e, 0x0d, 0x0e, 0x1c, 0x10, 0x10, 0x1c, 0x3b, 0x28, 0x22, - 0x28, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0xff, 0xc0, 0x00, 0x11, - 0x08, 0x00, 0x48, 0x00, 0x60, 0x03, 0x01, 0x21, 0x00, 0x02, 0x11, - 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, 0x00, 0x01, - 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, - 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, - 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, - 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, - 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, - 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, - 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, - 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, - 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, - 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, - 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, - 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, - 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, - 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, - 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, - 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, 0x03, 0x01, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, - 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, - 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, - 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, - 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, - 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, - 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, - 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, - 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, - 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, - 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, - 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, - 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, - 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, - 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, - 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, - 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x9e, 0xd2, - 0x2e, 0x07, 0x15, 0xaf, 0x6d, 0x08, 0xe2, 0xb3, 0x45, 0x1a, 0xf6, - 0xd0, 0x00, 0x01, 0xc5, 0x68, 0x45, 0x17, 0x4a, 0xb4, 0x22, 0xe4, - 0x70, 0x8c, 0x74, 0xa9, 0x3c, 0xa1, 0x8e, 0x95, 0x48, 0x96, 0x31, - 0xe2, 0x18, 0xe9, 0x55, 0xa5, 0x8c, 0x7a, 0x50, 0x05, 0x0b, 0x88, - 0x86, 0x0f, 0x15, 0x8f, 0x75, 0x1f, 0x26, 0x93, 0x19, 0x91, 0x77, - 0x18, 0xc1, 0xac, 0x4b, 0xc8, 0xfa, 0xd6, 0x63, 0x37, 0x6d, 0x31, - 0xb4, 0x73, 0x5b, 0x36, 0xa0, 0x1c, 0x50, 0x80, 0xd7, 0x83, 0xa0, - 0xab, 0xd1, 0x62, 0xad, 0x09, 0x8f, 0x17, 0x29, 0x03, 0xb2, 0xcc, - 0xe0, 0x77, 0x14, 0xa3, 0x56, 0xb3, 0x27, 0x1e, 0x67, 0xe9, 0x52, - 0xea, 0xc6, 0x3a, 0x36, 0x48, 0xef, 0x3d, 0x27, 0x70, 0x22, 0x60, - 0x47, 0x52, 0x69, 0xb2, 0xe2, 0xad, 0x3b, 0xea, 0x80, 0xa3, 0x38, - 0xe0, 0xd6, 0x3d, 0xd8, 0x1c, 0xd0, 0xca, 0x46, 0x3d, 0xd0, 0x18, - 0x35, 0x89, 0x78, 0xa3, 0x9a, 0xcd, 0x8c, 0xd2, 0xb3, 0x93, 0x2a, - 0x2b, 0x66, 0xd5, 0xf1, 0x8a, 0x10, 0x1a, 0xd6, 0xf2, 0x03, 0x8a, - 0x9e, 0xe6, 0xf4, 0x5a, 0xdb, 0xef, 0xfe, 0x23, 0xc0, 0xa7, 0x27, - 0xcb, 0x16, 0xc4, 0xcc, 0xdd, 0xe2, 0x78, 0x9a, 0x69, 0x66, 0xcc, - 0x99, 0xe1, 0x4d, 0x47, 0xba, 0xbc, 0xd9, 0x6a, 0xee, 0x26, 0x59, - 0x59, 0x4d, 0xac, 0x69, 0x34, 0x52, 0xe5, 0x8f, 0x55, 0xad, 0x58, - 0xae, 0x85, 0xc4, 0x22, 0x41, 0xdf, 0xad, 0x76, 0x61, 0xe5, 0x6f, - 0x74, 0x45, 0x69, 0xdc, 0x00, 0x79, 0xac, 0x8b, 0xa6, 0xc9, 0x35, - 0xd4, 0x34, 0x64, 0xdc, 0x37, 0x06, 0xb1, 0xae, 0x88, 0xc1, 0xac, - 0xd8, 0xc9, 0x2c, 0xa6, 0xe0, 0x73, 0x5b, 0x36, 0xf3, 0x74, 0xe6, - 0x84, 0x05, 0xe3, 0xa9, 0x47, 0x6a, 0x14, 0xb6, 0x49, 0x3d, 0x85, - 0x3a, 0xee, 0xee, 0x2b, 0xa8, 0xe2, 0x6f, 0x30, 0x81, 0xe9, 0x8a, - 0xca, 0xa4, 0xe2, 0xd3, 0x8b, 0x01, 0xb1, 0xf9, 0x04, 0x7f, 0xaf, - 0x23, 0xf0, 0xa9, 0x54, 0x41, 0x9c, 0xfd, 0xa3, 0xf4, 0xae, 0x65, - 0x18, 0xf7, 0x25, 0x8a, 0xe2, 0x02, 0x38, 0xb8, 0xfd, 0x2a, 0x7b, - 0x5b, 0xa8, 0x6d, 0x6d, 0x5d, 0x9a, 0x5d, 0xcb, 0xbb, 0xd2, 0xb6, - 0xa6, 0xa3, 0x19, 0x5e, 0xe2, 0x03, 0x7b, 0x1d, 0xc2, 0x17, 0x8d, - 0xb8, 0xac, 0xfb, 0x89, 0x39, 0x35, 0xd6, 0x9a, 0x6a, 0xe8, 0x66, - 0x55, 0xcb, 0xf5, 0xac, 0x7b, 0x96, 0xeb, 0x50, 0xc6, 0x88, 0x6d, - 0x66, 0xe9, 0xcd, 0x6c, 0xdb, 0x4f, 0xd3, 0x9a, 0x00, 0x2f, 0xe6, - 0xf9, 0xa3, 0xe7, 0xb5, 0x4a, 0x93, 0x7f, 0xa2, 0xc6, 0x73, 0xdc, - 0xd7, 0x15, 0x55, 0xef, 0x48, 0x7d, 0x09, 0x52, 0x6e, 0x3a, 0xd4, - 0xab, 0x2f, 0xbd, 0x61, 0x16, 0x0c, 0x73, 0x49, 0xc5, 0x24, 0x92, - 0x7f, 0xa2, 0x63, 0xfd, 0xaa, 0xd6, 0x2f, 0x71, 0x0e, 0xb1, 0x93, - 0xf7, 0x2d, 0xf5, 0xa4, 0x9e, 0x4e, 0xb5, 0xdd, 0x4b, 0xf8, 0x68, - 0x4c, 0xcb, 0xb9, 0x93, 0xad, 0x65, 0xce, 0xd9, 0x26, 0xa9, 0x8d, - 0x19, 0xf6, 0xf2, 0xf4, 0xe6, 0xb5, 0xad, 0xe7, 0xc6, 0x39, 0xa0, - 0x18, 0xeb, 0xc9, 0x77, 0x6c, 0x35, 0x2a, 0x4b, 0xfe, 0x8a, 0x9c, - 0xff, 0x00, 0x11, 0xae, 0x3a, 0x8b, 0xde, 0x61, 0xd0, 0x9e, 0x39, - 0xb8, 0xeb, 0x53, 0xac, 0xb9, 0xae, 0x5b, 0x00, 0xf3, 0x27, 0x14, - 0x92, 0xc9, 0xfe, 0x8a, 0x3f, 0xde, 0x35, 0xac, 0x3a, 0x88, 0x92, - 0xcd, 0xb1, 0x6e, 0x7d, 0xcd, 0x32, 0x67, 0xeb, 0xcd, 0x7a, 0x14, - 0xfe, 0x04, 0x26, 0x66, 0xce, 0xf9, 0x26, 0xb3, 0xe6, 0x6e, 0xb4, - 0xd9, 0x48, 0xc8, 0x82, 0x4e, 0x07, 0x35, 0xa7, 0x6f, 0x2f, 0x02, - 0x9a, 0x06, 0x5f, 0x8c, 0xa4, 0x83, 0x0e, 0x32, 0x2a, 0x69, 0xe3, - 0xdd, 0x12, 0x08, 0x97, 0x85, 0xec, 0x2a, 0x2a, 0x42, 0xf1, 0x76, - 0x26, 0xe4, 0x6a, 0x59, 0x0e, 0x18, 0x10, 0x6a, 0xd2, 0x89, 0x02, - 0x6e, 0x2a, 0x71, 0xeb, 0x5c, 0x1c, 0x8c, 0xa6, 0x48, 0xbb, 0xdc, - 0x61, 0x41, 0x35, 0x72, 0x28, 0x87, 0xd9, 0xf6, 0x4a, 0xb9, 0xe7, - 0x38, 0xae, 0x8c, 0x3d, 0x36, 0xdd, 0xde, 0xc4, 0xb0, 0x21, 0x51, - 0x76, 0xa8, 0xc0, 0xaa, 0x93, 0x31, 0xe6, 0xbb, 0x2d, 0x65, 0x61, - 0x19, 0xd3, 0x1e, 0xb5, 0x46, 0x5a, 0x96, 0x5a, 0x30, 0xa0, 0x7e, - 0x05, 0x69, 0x5b, 0xc9, 0xc6, 0x28, 0x40, 0xcd, 0x08, 0x64, 0x3c, - 0x73, 0x57, 0xe1, 0x94, 0xf1, 0xcd, 0x5a, 0x21, 0x8c, 0xb9, 0x63, - 0xe7, 0x67, 0x1d, 0xab, 0x40, 0xb1, 0xfb, 0x00, 0x1d, 0xf0, 0x2b, - 0x99, 0x2d, 0x66, 0x3e, 0x88, 0x75, 0x81, 0x3f, 0x31, 0xf6, 0xab, - 0x64, 0xd6, 0xb4, 0x17, 0xee, 0xd0, 0x9e, 0xe4, 0x32, 0x1a, 0xa7, - 0x31, 0xad, 0x18, 0x14, 0x26, 0xef, 0x54, 0xa5, 0xa8, 0x65, 0xa3, - 0x9c, 0x81, 0xfa, 0x56, 0x8c, 0x2d, 0xce, 0x68, 0x40, 0xcb, 0xf1, - 0x37, 0xbd, 0x5e, 0x85, 0xea, 0xd1, 0x0c, 0xbb, 0x19, 0x56, 0x23, - 0x20, 0x1f, 0xad, 0x5c, 0x42, 0x08, 0x03, 0xb5, 0x55, 0x91, 0x04, - 0xc9, 0x80, 0x38, 0x00, 0x0a, 0x71, 0x34, 0x6c, 0x32, 0x27, 0xe9, - 0x55, 0x25, 0x15, 0x2c, 0x68, 0xa3, 0x30, 0xeb, 0x54, 0xa5, 0x15, - 0x0c, 0xd1, 0x00, 0xff, 0xd9}; - int length = photoIntArray.length; - byte[] photoByteArray = new byte[length]; - for (int i = 0; i < length; i++) { - photoByteArray[i] = (byte)photoIntArray[i]; - } - PropertyNodesVerifier verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "2.1", - null, null, null, null, null), - new PropertyNode("N", "Gump;Forrest;Hoge;Pos;Tao", - Arrays.asList("Gump", "Forrest", - "Hoge", "Pos", "Tao"), - null, null, null, null), - new PropertyNode("FN", "Joe Due", - null, null, null, null, null), - new PropertyNode("ORG", - "Gump Shrimp Co.;Sales Dept.;Manager;Fish keeper", - Arrays.asList("Gump Shrimp Co.", - "Sales Dept.;Manager", - "Fish keeper"), - null, null, null, null), - new PropertyNode("ROLE", "Fish Cake Keeper!", - null, null, null, null, null), - new PropertyNode("TITLE", "Shrimp Man", - null, null, null, null, null), - new PropertyNode("X-CLASS", "PUBLIC", - null, null, null, null, null), - new PropertyNode("TEL", "(111) 555-1212", - null, null, null, - new HashSet<String>(Arrays.asList("WORK", "VOICE")), null), - new PropertyNode("TEL", "(404) 555-1212", - null, null, null, - new HashSet<String>(Arrays.asList("HOME", "VOICE")), null), - new PropertyNode("TEL", "0311111111", - null, null, null, - new HashSet<String>(Arrays.asList("CELL")), null), - new PropertyNode("TEL", "0322222222", - null, null, null, - new HashSet<String>(Arrays.asList("VIDEO")), null), - new PropertyNode("TEL", "0333333333", - null, null, null, - new HashSet<String>(Arrays.asList("VOICE")), null), - new PropertyNode("ADR", - ";;100 Waters Edge;Baytown;LA;30314;United States of America", - Arrays.asList("", "", "100 Waters Edge", "Baytown", - "LA", "30314", "United States of America"), - null, null, - new HashSet<String>(Arrays.asList("WORK")), null), - new PropertyNode("LABEL", - "100 Waters Edge\r\nBaytown, LA 30314\r\nUnited States of America", - null, null, contentValuesForQP, - new HashSet<String>(Arrays.asList("WORK")), null), - new PropertyNode("ADR", - ";;42 Plantation St.;Baytown;LA;30314;United States of America", - Arrays.asList("", "", "42 Plantation St.", "Baytown", - "LA", "30314", "United States of America"), null, null, - new HashSet<String>(Arrays.asList("HOME")), null), - new PropertyNode("LABEL", - "42 Plantation St.\r\nBaytown, LA 30314\r\nUnited States of America", - null, null, contentValuesForQP, - new HashSet<String>(Arrays.asList("HOME")), null), - new PropertyNode("EMAIL", "forrestgump@walladalla.com", - null, null, null, - new HashSet<String>(Arrays.asList("PREF", "INTERNET")), null), - new PropertyNode("EMAIL", "cell@example.com", - null, null, null, - new HashSet<String>(Arrays.asList("CELL")), null), - new PropertyNode("NOTE", "The following note is the example from RFC 2045.", - null, null, null, null, null), - new PropertyNode("NOTE", - "Now's the time for all folk to come to the aid of their country.", - null, null, contentValuesForQP, null, null), - new PropertyNode("PHOTO", null, - null, photoByteArray, contentValuesForPhoto, - new HashSet<String>(Arrays.asList("JPEG")), null), - new PropertyNode("X-ATTRIBUTE", "Some String", - null, null, null, null, null), - new PropertyNode("BDAY", "19800101", - null, null, null, null, null), - new PropertyNode("GEO", "35.6563854,139.6994233", - null, null, null, null, null), - new PropertyNode("URL", "http://www.example.com/", - null, null, null, null, null), - new PropertyNode("REV", "20080424T195243Z", - null, null, null, null, null)); - verifier.verify(builder.vNodeList.get(0)); - } - - public void testV21Japanese1() throws IOException, VCardException { - VCardParser_V21 parser = new VCardParser_V21(); - VNodeBuilder builder = new VNodeBuilder(); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_japanese_1); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, builder.vNodeList.size()); - ContentValues contentValuesForShiftJis = new ContentValues(); - contentValuesForShiftJis.put("CHARSET", "SHIFT_JIS"); - ContentValues contentValuesForQP = new ContentValues(); - contentValuesForQP.put("ENCODING", "QUOTED-PRINTABLE"); - contentValuesForQP.put("CHARSET", "SHIFT_JIS"); - // Though Japanese careers append ";;;;" at the end of the value of "SOUND", - // vCard 2.1/3.0 specification does not allow multiple values. - // Do not need to handle it as multiple values. - PropertyNodesVerifier verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "2.1", - null, null, null, null, null), - new PropertyNode("N", "\u5B89\u85E4\u30ED\u30A4\u30C9;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9", "", "", "", ""), - null, contentValuesForShiftJis, null, null), - new PropertyNode("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E;;;;", - null, null, contentValuesForShiftJis, - new HashSet<String>(Arrays.asList("X-IRMC-N")), null), - new PropertyNode("TEL", "0300000000", - null, null, null, - new HashSet<String>(Arrays.asList("VOICE", "PREF")), null)); - verifier.verify(builder.vNodeList.get(0)); - } - - public void testV21Japanese2() throws IOException, VCardException { - VCardParser_V21 parser = new VCardParser_V21(); - VNodeBuilder builder = new VNodeBuilder(); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_japanese_2); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, builder.vNodeList.size()); - ContentValues contentValuesForShiftJis = new ContentValues(); - contentValuesForShiftJis.put("CHARSET", "SHIFT_JIS"); - ContentValues contentValuesForQP = new ContentValues(); - contentValuesForQP.put("ENCODING", "QUOTED-PRINTABLE"); - contentValuesForQP.put("CHARSET", "SHIFT_JIS"); - PropertyNodesVerifier verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "2.1", - null, null, null, null, null), - new PropertyNode("N", "\u5B89\u85E4;\u30ED\u30A4\u30C9\u0031;;;", - Arrays.asList("\u5B89\u85E4", "\u30ED\u30A4\u30C9\u0031", - "", "", ""), - null, contentValuesForShiftJis, null, null), - new PropertyNode("FN", - "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9\u0020\u0031", - null, null, contentValuesForShiftJis, null, null), - new PropertyNode("SOUND", - ("\uFF71\uFF9D\uFF84\uFF9E\uFF73" + - ";\uFF9B\uFF72\uFF84\uFF9E\u0031;;;"), - null, null, contentValuesForShiftJis, - new HashSet<String>(Arrays.asList("X-IRMC-N")), null), - new PropertyNode("ADR", - (";\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + - "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + - "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC\u0036" + - "\u968E;;;;150-8512;"), - Arrays.asList("", - "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + - "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + - "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + - "\u0036\u968E", "", "", "", "150-8512", ""), - null, contentValuesForQP, - new HashSet<String>(Arrays.asList("HOME")), null), - new PropertyNode("NOTE", "\u30E1\u30E2", - null, null, contentValuesForQP, null, null)); - verifier.verify(builder.vNodeList.get(0)); - } - - public void testV21MultipleEntryCase() throws IOException, VCardException { - VCardParser_V21 parser = new VCardParser_V21(); - VNodeBuilder builder = new VNodeBuilder(); - InputStream is = getContext().getResources().openRawResource(R.raw.v21_multiple_entry); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(3, builder.vNodeList.size()); - ContentValues contentValuesForShiftJis = new ContentValues(); - contentValuesForShiftJis.put("CHARSET", "SHIFT_JIS"); - PropertyNodesVerifier verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "2.1", - null, null, null, null, null), - new PropertyNode("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0033", "", "", "", ""), - null, contentValuesForShiftJis, null, null), - new PropertyNode("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0033;;;;", - null, null, contentValuesForShiftJis, - new HashSet<String>(Arrays.asList("X-IRMC-N")), null), - new PropertyNode("TEL", "9", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-SECRET")), null), - new PropertyNode("TEL", "10", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-HOTEL")), null), - new PropertyNode("TEL", "11", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-SCHOOL")), null), - new PropertyNode("TEL", "12", - null, null, null, - new HashSet<String>(Arrays.asList("FAX", "HOME")), null)); - verifier.verify(builder.vNodeList.get(0)); - - verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "2.1", - null, null, null, null, null), - new PropertyNode("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0034", "", "", "", ""), - null, contentValuesForShiftJis, null, null), - new PropertyNode("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0034;;;;", - null, null, contentValuesForShiftJis, - new HashSet<String>(Arrays.asList("X-IRMC-N")), null), - new PropertyNode("TEL", "13", - null, null, null, - new HashSet<String>(Arrays.asList("MODEM")), null), - new PropertyNode("TEL", "14", - null, null, null, - new HashSet<String>(Arrays.asList("PAGER")), null), - new PropertyNode("TEL", "15", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-FAMILY")), null), - new PropertyNode("TEL", "16", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-GIRL")), null)); - verifier.verify(builder.vNodeList.get(1)); - verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "2.1", - null, null, null, null, null), - new PropertyNode("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0035", "", "", "", ""), - null, contentValuesForShiftJis, null, null), - new PropertyNode("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0035;;;;", - null, null, contentValuesForShiftJis, - new HashSet<String>(Arrays.asList("X-IRMC-N")), null), - new PropertyNode("TEL", "17", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-BOY")), null), - new PropertyNode("TEL", "18", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-FRIEND")), null), - new PropertyNode("TEL", "19", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-PHS")), null), - new PropertyNode("TEL", "20", - null, null, null, - new HashSet<String>(Arrays.asList("X-NEC-RESTAURANT")), null)); - verifier.verify(builder.vNodeList.get(2)); - } - - public void testV30SimpleCase() throws IOException, VCardException { - VCardParser_V21 parser = new VCardParser_V30(); - VNodeBuilder builder = new VNodeBuilder(); - InputStream is = getContext().getResources().openRawResource(R.raw.v30_simple); - assertEquals(true, parser.parse(is,"ISO-8859-1", builder)); - is.close(); - assertEquals(1, builder.vNodeList.size()); - PropertyNodesVerifier verifier = new PropertyNodesVerifier( - new PropertyNode("VERSION", "3.0", - null, null, null, null, null), - new PropertyNode("FN", "And Roid", - null, null, null, null, null), - new PropertyNode("N", "And;Roid;;;", - Arrays.asList("And", "Roid", "", "", ""), - null, null, null, null), - new PropertyNode("ORG", "Open;Handset; Alliance", - Arrays.asList("Open", "Handset", " Alliance"), - null, null, null, null), - new PropertyNode("SORT-STRING", "android", null, null, null, null, null), - new PropertyNode("TEL", "0300000000", - null, null, null, - new HashSet<String>(Arrays.asList("PREF", "VOICE")), null), - new PropertyNode("CLASS", "PUBLIC", null, null, null, null, null), - new PropertyNode("X-GNO", "0", null, null, null, null, null), - new PropertyNode("X-GN", "group0", null, null, null, null, null), - new PropertyNode("X-REDUCTION", "0", - null, null, null, null, null), - new PropertyNode("REV", "20081031T065854Z", - null, null, null, null, null)); - verifier.verify(builder.vNodeList.get(0)); - } -} diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTestsBase.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTestsBase.java new file mode 100644 index 0000000..6176f5c --- /dev/null +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardTestsBase.java @@ -0,0 +1,980 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.unit_tests.vcard; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.content.EntityIterator; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.IBulkCursor; +import android.database.IContentObserver; +import android.net.Uri; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.pim.vcard.ContactStruct; +import android.pim.vcard.EntryCommitter; +import android.pim.vcard.EntryHandler; +import android.pim.vcard.VCardBuilder; +import android.pim.vcard.VCardBuilderCollection; +import android.pim.vcard.VCardComposer; +import android.pim.vcard.VCardConfig; +import android.pim.vcard.VCardDataBuilder; +import android.pim.vcard.VCardParser; +import android.pim.vcard.VCardParser_V21; +import android.pim.vcard.VCardParser_V30; +import android.pim.vcard.exception.VCardException; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.test.AndroidTestCase; +import android.test.mock.MockContentResolver; +import android.test.mock.MockContext; +import android.test.mock.MockCursor; +import android.text.TextUtils; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.Map.Entry; + +/** + * Almost a dead copy of android.test.mock.MockContentProvider, but different in that this + * class extends ContentProvider, not implementing IContentProvider, + * so that MockContentResolver is able to accept this class :( + */ +class MockContentProvider extends ContentProvider { + @Override + public boolean onCreate() { + return true; + } + + @Override + public int bulkInsert(Uri url, ContentValues[] initialValues) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @SuppressWarnings("unused") + public IBulkCursor bulkQuery(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder, IContentObserver observer, + CursorWindow window) throws RemoteException { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + @SuppressWarnings("unused") + public int delete(Uri url, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public String getType(Uri url) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public Uri insert(Uri url, ContentValues initialValues) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public ParcelFileDescriptor openFile(Uri url, String mode) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public AssetFileDescriptor openAssetFile(Uri uri, String mode) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + /** + * @hide + */ + @Override + public EntityIterator queryEntities(Uri url, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + public IBinder asBinder() { + throw new UnsupportedOperationException("unimplemented mock method"); + } +} + +class CustomMockContext extends MockContext { + final ContentResolver mResolver; + public CustomMockContext(ContentResolver resolver) { + mResolver = resolver; + } + + @Override + public ContentResolver getContentResolver() { + return mResolver; + } +} + +/** + * BaseClass for vCard unit tests with utility classes. + * Please do not add each unit test here. + */ +/* package */ class VCardTestsBase extends AndroidTestCase { + public static final int V21 = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8; + public static final int V30 = VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8; + + // Do not modify these during tests. + protected final ContentValues mContentValuesForQP; + protected final ContentValues mContentValuesForSJis; + protected final ContentValues mContentValuesForUtf8; + protected final ContentValues mContentValuesForQPAndSJis; + protected final ContentValues mContentValuesForQPAndUtf8; + protected final ContentValues mContentValuesForBase64V21; + protected final ContentValues mContentValuesForBase64V30; + + public VCardTestsBase() { + super(); + mContentValuesForQP = new ContentValues(); + mContentValuesForQP.put("ENCODING", "QUOTED-PRINTABLE"); + mContentValuesForSJis = new ContentValues(); + mContentValuesForSJis.put("CHARSET", "SHIFT_JIS"); + mContentValuesForUtf8 = new ContentValues(); + mContentValuesForUtf8.put("CHARSET", "UTF-8"); + mContentValuesForQPAndSJis = new ContentValues(); + mContentValuesForQPAndSJis.put("ENCODING", "QUOTED-PRINTABLE"); + mContentValuesForQPAndSJis.put("CHARSET", "SHIFT_JIS"); + mContentValuesForQPAndUtf8 = new ContentValues(); + mContentValuesForQPAndUtf8.put("ENCODING", "QUOTED-PRINTABLE"); + mContentValuesForQPAndUtf8.put("CHARSET", "UTF-8"); + mContentValuesForBase64V21 = new ContentValues(); + mContentValuesForBase64V21.put("ENCODING", "BASE64"); + mContentValuesForBase64V30 = new ContentValues(); + mContentValuesForBase64V30.put("ENCODING", "b"); + } + + public class ImportTestResolver extends MockContentResolver { + ImportTestProvider mProvider = new ImportTestProvider(); + @Override + public ContentProviderResult[] applyBatch(String authority, + ArrayList<ContentProviderOperation> operations) { + equalsString(authority, RawContacts.CONTENT_URI.toString()); + return mProvider.applyBatch(operations); + } + + public void addExpectedContentValues(ContentValues expectedContentValues) { + mProvider.addExpectedContentValues(expectedContentValues); + } + + public void verify() { + mProvider.verify(); + } + } + + private static final Set<String> sKnownMimeTypeSet = + new HashSet<String>(Arrays.asList(StructuredName.CONTENT_ITEM_TYPE, + Nickname.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE, + Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE, + Im.CONTENT_ITEM_TYPE, Organization.CONTENT_ITEM_TYPE, + Event.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE, + Note.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE, + Relation.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, + GroupMembership.CONTENT_ITEM_TYPE)); + + public class ImportTestProvider extends MockContentProvider { + final Map<String, Collection<ContentValues>> mMimeTypeToExpectedContentValues; + + public ImportTestProvider() { + mMimeTypeToExpectedContentValues = + new HashMap<String, Collection<ContentValues>>(); + for (String acceptanbleMimeType : sKnownMimeTypeSet) { + // Do not use HashSet since the current implementation changes the content of + // ContentValues after the insertion, which make the result of hashCode() + // changes... + mMimeTypeToExpectedContentValues.put( + acceptanbleMimeType, new ArrayList<ContentValues>()); + } + } + + public void addExpectedContentValues(ContentValues expectedContentValues) { + final String mimeType = expectedContentValues.getAsString(Data.MIMETYPE); + if (!sKnownMimeTypeSet.contains(mimeType)) { + fail(String.format( + "Unknow MimeType %s in the test code. Test code should be broken.", + mimeType)); + } + + final Collection<ContentValues> contentValuesCollection = + mMimeTypeToExpectedContentValues.get(mimeType); + contentValuesCollection.add(expectedContentValues); + } + + @Override + public ContentProviderResult[] applyBatch( + ArrayList<ContentProviderOperation> operations) { + if (operations == null) { + fail("There is no operation."); + } + + final int size = operations.size(); + ContentProviderResult[] fakeResultArray = new ContentProviderResult[size]; + for (int i = 0; i < size; i++) { + Uri uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, String.valueOf(i)); + fakeResultArray[i] = new ContentProviderResult(uri); + } + + for (int i = 0; i < size; i++) { + ContentProviderOperation operation = operations.get(i); + ContentValues contentValues = operation.resolveValueBackReferences( + fakeResultArray, i); + } + for (int i = 0; i < size; i++) { + ContentProviderOperation operation = operations.get(i); + ContentValues actualContentValues = operation.resolveValueBackReferences( + fakeResultArray, i); + final Uri uri = operation.getUri(); + if (uri.equals(RawContacts.CONTENT_URI)) { + assertNull(actualContentValues.get(RawContacts.ACCOUNT_NAME)); + assertNull(actualContentValues.get(RawContacts.ACCOUNT_TYPE)); + } else if (uri.equals(Data.CONTENT_URI)) { + final String mimeType = actualContentValues.getAsString(Data.MIMETYPE); + if (!sKnownMimeTypeSet.contains(mimeType)) { + fail(String.format( + "Unknown MimeType %s. Probably added after developing this test", + mimeType)); + } + // Remove data meaningless in this unit tests. + // Specifically, Data.DATA1 - DATA7 are set to null or empty String + // regardless of the input, but it may change depending on how + // resolver-related code handles it. + // Here, we ignore these implementation-dependent specs and + // just check whether vCard importer correctly inserts rellevent data. + Set<String> keyToBeRemoved = new HashSet<String>(); + for (Entry<String, Object> entry : actualContentValues.valueSet()) { + Object value = entry.getValue(); + if (value == null || TextUtils.isEmpty(value.toString())) { + keyToBeRemoved.add(entry.getKey()); + } + } + for (String key: keyToBeRemoved) { + actualContentValues.remove(key); + } + /* for testing + Log.d("@@@", + String.format("MimeType: %s, data: %s", + mimeType, actualContentValues.toString())); */ + // Remove RAW_CONTACT_ID entry just for safety, since we do not care + // how resolver-related code handles the entry in this unit test, + if (actualContentValues.containsKey(Data.RAW_CONTACT_ID)) { + actualContentValues.remove(Data.RAW_CONTACT_ID); + } + final Collection<ContentValues> contentValuesCollection = + mMimeTypeToExpectedContentValues.get(mimeType); + if (contentValuesCollection.isEmpty()) { + fail("ContentValues for MimeType " + mimeType + + " is not expected at all (" + actualContentValues + ")"); + } + boolean checked = false; + for (ContentValues expectedContentValues : contentValuesCollection) { + /*for testing + Log.d("@@@", "expected: " + + convertToEasilyReadableString(expectedContentValues)); + Log.d("@@@", "actual : " + + convertToEasilyReadableString(actualContentValues));*/ + if (equalsForContentValues(expectedContentValues, + actualContentValues)) { + assertTrue(contentValuesCollection.remove(expectedContentValues)); + checked = true; + break; + } + } + if (!checked) { + final String failMsg = + "Unexpected ContentValues for MimeType " + mimeType + + ": " + actualContentValues; + fail(failMsg); + } + } else { + fail("Unexpected Uri has come: " + uri); + } + } // for (int i = 0; i < size; i++) { + return null; + } + + public void verify() { + StringBuilder builder = new StringBuilder(); + for (Collection<ContentValues> contentValuesCollection : + mMimeTypeToExpectedContentValues.values()) { + for (ContentValues expectedContentValues: contentValuesCollection) { + builder.append(convertToEasilyReadableString(expectedContentValues)); + builder.append("\n"); + } + } + if (builder.length() > 0) { + final String failMsg = + "There is(are) remaining expected ContentValues instance(s): \n" + + builder.toString(); + fail(failMsg); + } + } + } + + class ImportVerifierElem { + private final ImportTestResolver mResolver; + private final EntryHandler mHandler; + + public ImportVerifierElem() { + mResolver = new ImportTestResolver(); + mHandler = new EntryCommitter(mResolver); + } + + public ContentValuesBuilder addExpected(String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Data.MIMETYPE, mimeType); + mResolver.addExpectedContentValues(contentValues); + return new ContentValuesBuilder(contentValues); + } + + public void verify(int resId, int vCardType) + throws IOException, VCardException { + verify(getContext().getResources().openRawResource(resId), vCardType); + } + + public void verify(InputStream is, int vCardType) throws IOException, VCardException { + final VCardParser vCardParser; + if (VCardConfig.isV30(vCardType)) { + vCardParser = new VCardParser_V30(true); // use StrictParsing + } else { + vCardParser = new VCardParser_V21(); + } + VCardDataBuilder builder = + new VCardDataBuilder(null, null, false, vCardType, null); + builder.addEntryHandler(mHandler); + try { + vCardParser.parse(is, builder); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + verifyResolver(); + } + + public void verifyResolver() { + mResolver.verify(); + } + + public void onParsingStart() { + mHandler.onParsingStart(); + } + + public void onEntryCreated(ContactStruct entry) { + mHandler.onEntryCreated(entry); + } + + public void onParsingEnd() { + mHandler.onParsingEnd(); + } + } + + class ImportVerifier implements EntryHandler { + private List<ImportVerifierElem> mImportVerifierElemList = + new ArrayList<ImportVerifierElem>(); + private int mIndex; + + public ImportVerifierElem addImportVerifierElem() { + ImportVerifierElem importVerifier = new ImportVerifierElem(); + mImportVerifierElemList.add(importVerifier); + return importVerifier; + } + + public void verify(int resId, int vCardType) throws IOException, VCardException { + verify(getContext().getResources().openRawResource(resId), vCardType); + } + + public void verify(int resId, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + verify(getContext().getResources().openRawResource(resId), vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType) throws IOException, VCardException { + final VCardParser vCardParser; + if (VCardConfig.isV30(vCardType)) { + vCardParser = new VCardParser_V30(true); // use StrictParsing + } else { + vCardParser = new VCardParser_V21(); + } + verify(is, vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + VCardDataBuilder builder = + new VCardDataBuilder(null, null, false, vCardType, null); + builder.addEntryHandler(this); + try { + vCardParser.parse(is, builder); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } + + public void onParsingStart() { + for (ImportVerifierElem elem : mImportVerifierElemList) { + elem.onParsingStart(); + } + } + + public void onEntryCreated(ContactStruct entry) { + assertTrue(mIndex < mImportVerifierElemList.size()); + mImportVerifierElemList.get(mIndex).onEntryCreated(entry); + mIndex++; + } + + public void onParsingEnd() { + for (ImportVerifierElem elem : mImportVerifierElemList) { + elem.onParsingEnd(); + elem.verifyResolver(); + } + } + } + + public class ExportTestResolver extends MockContentResolver { + ExportTestProvider mProvider = new ExportTestProvider(); + public ExportTestResolver() { + addProvider(VCardComposer.VCARD_TEST_AUTHORITY, mProvider); + addProvider(RawContacts.CONTENT_URI.getAuthority(), mProvider); + } + + public ContactEntry buildContactEntry() { + return mProvider.buildInputEntry(); + } + } + + public static class MockEntityIterator implements EntityIterator { + List<Entity> mEntityList; + Iterator<Entity> mIterator; + + public MockEntityIterator(List<ContentValues> contentValuesList) { + mEntityList = new ArrayList<Entity>(); + Entity entity = new Entity(new ContentValues()); + for (ContentValues contentValues : contentValuesList) { + entity.addSubValue(Data.CONTENT_URI, contentValues); + } + mEntityList.add(entity); + mIterator = mEntityList.iterator(); + } + + public boolean hasNext() { + return mIterator.hasNext(); + } + + public Entity next() { + return mIterator.next(); + } + + public void reset() { + mIterator = mEntityList.iterator(); + } + + public void close() { + } + } + + /** + * Represents one contact, which should contain multiple ContentValues like + * StructuredName, Email, etc. + */ + static class ContactEntry { + private final List<ContentValues> mContentValuesList = new ArrayList<ContentValues>(); + + public ContentValuesBuilder buildData(String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Data.MIMETYPE, mimeType); + mContentValuesList.add(contentValues); + return new ContentValuesBuilder(contentValues); + } + + public List<ContentValues> getList() { + return mContentValuesList; + } + } + + class ExportTestProvider extends MockContentProvider { + ArrayList<ContactEntry> mContactEntryList = new ArrayList<ContactEntry>(); + + public ContactEntry buildInputEntry() { + ContactEntry contactEntry = new ContactEntry(); + mContactEntryList.add(contactEntry); + return contactEntry; + } + + @Override + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) { + assertTrue(uri != null); + assertTrue(ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())); + final String authority = uri.getAuthority(); + assertTrue(RawContacts.CONTENT_URI.getAuthority().equals(authority)); + assertTrue((Data.CONTACT_ID + "=?").equals(selection)); + assertEquals(1, selectionArgs.length); + int id = Integer.parseInt(selectionArgs[0]); + assertTrue(id >= 0 && id < mContactEntryList.size()); + + return new MockEntityIterator(mContactEntryList.get(id).getList()); + } + + @Override + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + assertTrue(VCardComposer.CONTACTS_TEST_CONTENT_URI.equals(uri)); + // In this test, following arguments are not supported. + assertNull(selection); + assertNull(selectionArgs); + assertNull(sortOrder); + + return new MockCursor() { + int mCurrentPosition = -1; + + @Override + public int getCount() { + return mContactEntryList.size(); + } + + @Override + public boolean moveToFirst() { + mCurrentPosition = 0; + return true; + } + + @Override + public boolean moveToNext() { + if (mCurrentPosition < mContactEntryList.size()) { + mCurrentPosition++; + return true; + } else { + return false; + } + } + + @Override + public boolean isBeforeFirst() { + return mCurrentPosition < 0; + } + + @Override + public boolean isAfterLast() { + return mCurrentPosition >= mContactEntryList.size(); + } + + @Override + public int getColumnIndex(String columnName) { + assertEquals(Contacts._ID, columnName); + return 0; + } + + @Override + public int getInt(int columnIndex) { + assertEquals(0, columnIndex); + assertTrue(mCurrentPosition >= 0 + && mCurrentPosition < mContactEntryList.size()); + return mCurrentPosition; + } + + @Override + public String getString(int columnIndex) { + return String.valueOf(getInt(columnIndex)); + } + + @Override + public void close() { + } + }; + } + } + + class LineVerifierElem { + private final List<String> mExpectedLineList = new ArrayList<String>(); + private final boolean mIsV30; + + public LineVerifierElem(boolean isV30) { + mIsV30 = isV30; + } + + public LineVerifierElem addExpected(final String line) { + if (!TextUtils.isEmpty(line)) { + mExpectedLineList.add(line); + } + return this; + } + + public void verify(final String vcard) { + final String[] lineArray = vcard.split("\\r?\\n"); + final int length = lineArray.length; + final TestCase testCase = VCardTestsBase.this; + boolean beginExists = false; + boolean endExists = false; + boolean versionExists = false; + + for (int i = 0; i < length; i++) { + final String line = lineArray[i]; + if (TextUtils.isEmpty(line)) { + continue; + } + + if ("BEGIN:VCARD".equalsIgnoreCase(line)) { + if (beginExists) { + testCase.fail("Multiple \"BEGIN:VCARD\" line found"); + } else { + beginExists = true; + continue; + } + } else if ("END:VCARD".equalsIgnoreCase(line)) { + if (endExists) { + testCase.fail("Multiple \"END:VCARD\" line found"); + } else { + endExists = true; + continue; + } + } else if ( + (mIsV30 ? "VERSION:3.0" : "VERSION:2.1").equalsIgnoreCase(line)) { + if (versionExists) { + testCase.fail("Multiple VERSION line + found"); + } else { + versionExists = true; + continue; + } + } + + if (!beginExists) { + testCase.fail( + "Property other than BEGIN came before BEGIN property: " + line); + } else if (endExists) { + testCase.fail("Property other than END came after END property: " + line); + } + + final int index = mExpectedLineList.indexOf(line); + if (index >= 0) { + mExpectedLineList.remove(index); + } else { + testCase.fail("Unexpected line: " + line); + } + } + + if (!mExpectedLineList.isEmpty()) { + StringBuffer buffer = new StringBuffer(); + for (String expectedLine : mExpectedLineList) { + buffer.append(expectedLine); + buffer.append("\n"); + } + + testCase.fail("Expected line(s) not found:" + buffer.toString()); + } + } + } + + class LineVerifier implements VCardComposer.OneEntryHandler { + private final ArrayList<LineVerifierElem> mLineVerifierElemList; + private final boolean mIsV30; + private int index; + + public LineVerifier(final boolean isV30) { + mLineVerifierElemList = new ArrayList<LineVerifierElem>(); + mIsV30 = isV30; + } + + public LineVerifierElem addLineVerifierElem() { + LineVerifierElem lineVerifier = new LineVerifierElem(mIsV30); + mLineVerifierElemList.add(lineVerifier); + return lineVerifier; + } + + public void verify(String vcard) { + if (index >= mLineVerifierElemList.size()) { + VCardTestsBase.this.fail("Insufficient number of LineVerifier (" + index + ")"); + } + + LineVerifierElem lineVerifier = mLineVerifierElemList.get(index); + lineVerifier.verify(vcard); + + index++; + } + + public boolean onEntryCreated(String vcard) { + verify(vcard); + return true; + } + + public boolean onInit(Context context) { + return true; + } + + public void onTerminate() { + } + } + + class VCardVerifier { + private class VCardVerifierInternal implements VCardComposer.OneEntryHandler { + public boolean onInit(Context context) { + return true; + } + public boolean onEntryCreated(String vcard) { + verifyOneVCard(vcard); + return true; + } + public void onTerminate() { + } + } + + private final VCardVerifierInternal mVCardVerifierInternal; + private final ExportTestResolver mResolver; + private final int mVCardType; + private final boolean mIsV30; + + // To allow duplication, use list instead of set. + // When null, we don't need to do the verification. + private PropertyNodesVerifier mPropertyNodesVerifier; + private LineVerifier mLineVerificationHandler; + private ImportVerifier mImportVerifier; + + public VCardVerifier(ExportTestResolver resolver, int vcardType) { + mVCardVerifierInternal = new VCardVerifierInternal(); + mResolver = resolver; + mIsV30 = VCardConfig.isV30(vcardType); + mVCardType = vcardType; + } + + public PropertyNodesVerifierElem addPropertyNodesVerifierElem() { + if (mPropertyNodesVerifier == null) { + mPropertyNodesVerifier = new PropertyNodesVerifier(VCardTestsBase.this); + } + PropertyNodesVerifierElem elem = + mPropertyNodesVerifier.addPropertyNodesVerifierElem(); + elem.addNodeWithOrder("VERSION", (mIsV30 ? "3.0" : "2.1")); + + return elem; + } + + public PropertyNodesVerifierElem addPropertyNodesVerifierWithEmptyName() { + PropertyNodesVerifierElem elem = addPropertyNodesVerifierElem(); + if (mIsV30) { + elem.addNodeWithOrder("N", "").addNodeWithOrder("FN", ""); + } + return elem; + } + + public LineVerifierElem addLineVerifier() { + if (mLineVerificationHandler == null) { + mLineVerificationHandler = new LineVerifier(mIsV30); + } + return mLineVerificationHandler.addLineVerifierElem(); + } + + public ImportVerifierElem addImportVerifier() { + if (mImportVerifier == null) { + mImportVerifier = new ImportVerifier(); + } + + return mImportVerifier.addImportVerifierElem(); + } + + private void verifyOneVCard(final String vcard) { + final VCardBuilder builder; + if (mImportVerifier != null) { + final VNodeBuilder vnodeBuilder = mPropertyNodesVerifier; + final VCardDataBuilder vcardDataBuilder = new VCardDataBuilder(); + vcardDataBuilder.addEntryHandler(mImportVerifier); + if (mPropertyNodesVerifier != null) { + builder = new VCardBuilderCollection(Arrays.asList( + vcardDataBuilder, mPropertyNodesVerifier)); + } else { + builder = vnodeBuilder; + } + } else { + if (mPropertyNodesVerifier != null) { + builder = mPropertyNodesVerifier; + } else { + return; + } + } + + final VCardParser parser = + (mIsV30 ? new VCardParser_V30(true) : new VCardParser_V21()); + final TestCase testCase = VCardTestsBase.this; + + InputStream is = null; + try { + String charset = + (VCardConfig.usesShiftJis(mVCardType) ? "SHIFT_JIS" : "UTF-8"); + is = new ByteArrayInputStream(vcard.getBytes(charset)); + testCase.assertEquals(true, parser.parse(is, null, builder)); + } catch (IOException e) { + testCase.fail("Unexpected IOException: " + e.getMessage()); + } catch (VCardException e) { + testCase.fail("Unexpected VCardException: " + e.getMessage()); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } + + public void verify() { + VCardComposer composer = + new VCardComposer(new CustomMockContext(mResolver), mVCardType); + composer.addHandler(mLineVerificationHandler); + composer.addHandler(mVCardVerifierInternal); + if (!composer.init(VCardComposer.CONTACTS_TEST_CONTENT_URI, null, null, null)) { + fail("init() failed. Reason: " + composer.getErrorReason()); + } + assertFalse(composer.isAfterLast()); + try { + while (!composer.isAfterLast()) { + assertTrue(composer.createOneEntry()); + } + } finally { + composer.terminate(); + } + } + } + + /** + * Utility method to print ContentValues whose content is printed with sorted keys. + */ + private static String convertToEasilyReadableString(ContentValues contentValues) { + if (contentValues == null) { + return "null"; + } + String mimeTypeValue = ""; + SortedMap<String, String> sortedMap = new TreeMap<String, String>(); + for (Entry<String, Object> entry : contentValues.valueSet()) { + final String key = entry.getKey(); + final Object value = entry.getValue(); + final String valueString = (value != null ? value.toString() : null); + if (Data.MIMETYPE.equals(key)) { + mimeTypeValue = valueString; + } else { + assertNotNull(key); + sortedMap.put(key, valueString); + } + } + StringBuilder builder = new StringBuilder(); + builder.append(Data.MIMETYPE); + builder.append('='); + builder.append(mimeTypeValue); + for (Entry<String, String> entry : sortedMap.entrySet()) { + final String key = entry.getKey(); + final String value = entry.getValue(); + builder.append(' '); + builder.append(key); + builder.append('='); + builder.append(value); + } + return builder.toString(); + } + + private static boolean equalsForContentValues( + ContentValues expected, ContentValues actual) { + if (expected == actual) { + return true; + } else if (expected == null || actual == null || expected.size() != actual.size()) { + return false; + } + + for (Entry<String, Object> entry : expected.valueSet()) { + final String key = entry.getKey(); + final Object value = entry.getValue(); + if (!actual.containsKey(key)) { + return false; + } + if (value instanceof byte[]) { + Object actualValue = actual.get(key); + if (!Arrays.equals((byte[])value, (byte[])actualValue)) { + return false; + } + } else if (!value.equals(actual.get(key))) { + return false; + } + } + return true; + } + + private static boolean equalsString(String a, String b) { + if (a == null || a.length() == 0) { + return b == null || b.length() == 0; + } else { + return a.equals(b); + } + } +}
\ No newline at end of file diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VNode.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VNode.java index 3eb827b..7587320 100644 --- a/tests/AndroidTests/src/com/android/unit_tests/vcard/VNode.java +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VNode.java @@ -18,7 +18,7 @@ package com.android.unit_tests.vcard; import java.util.ArrayList; /** - * @hide old class. Just for testing + * Previously used in main vCard handling code but now exists only for testing. */ public class VNode { public String VName; diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VNodeBuilder.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VNodeBuilder.java index 6d69223..ce4de03 100644 --- a/tests/AndroidTests/src/com/android/unit_tests/vcard/VNodeBuilder.java +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VNodeBuilder.java @@ -36,7 +36,8 @@ import java.util.List; * Maybe several vcard instance, so use vNodeList to store. * VNode: standy by a vcard instance. * PropertyNode: standy by a property line of a card. - * @hide old class, just for testing use + * + * Previously used in main vCard handling code but now exists only for testing. */ public class VNodeBuilder implements VCardBuilder { static private String LOG_TAG = "VDATABuilder"; @@ -189,6 +190,7 @@ public class VNodeBuilder implements VCardBuilder { private String handleOneValue(String value, String targetCharset, String encoding) { if (encoding != null) { + encoding = encoding.toUpperCase(); if (encoding.equals("BASE64") || encoding.equals("B")) { // Assume BASE64 is used only when the number of values is 1. mCurrentPropNode.propValue_bytes = diff --git a/tests/BrowserTestPlugin/jni/main.cpp b/tests/BrowserTestPlugin/jni/main.cpp index e3ad4a7..2f295f0 100644 --- a/tests/BrowserTestPlugin/jni/main.cpp +++ b/tests/BrowserTestPlugin/jni/main.cpp @@ -65,7 +65,6 @@ ANPCanvasInterfaceV0 gCanvasI; ANPLogInterfaceV0 gLogI; ANPPaintInterfaceV0 gPaintI; ANPPathInterfaceV0 gPathI; -ANPSystemInterfaceV0 gSystemI; ANPTypefaceInterfaceV0 gTypefaceI; ANPWindowInterfaceV0 gWindowI; diff --git a/tests/CoreTests/android/core/MathTest.java b/tests/CoreTests/android/core/MathTest.java index 64c8c1e..50009db 100644 --- a/tests/CoreTests/android/core/MathTest.java +++ b/tests/CoreTests/android/core/MathTest.java @@ -687,8 +687,7 @@ public class MathTest extends TestCase { /** * @tests java.lang.Math#tanh(double) */ - // TODO: Known failure, temporarily remove from SmallSuite - // @SmallTest + @SmallTest public void testTanhD() { // Test for special situations assertTrue("Should return NaN", Double.isNaN(Math.tanh(Double.NaN))); diff --git a/tests/CoreTests/android/core/StrictMathTest.java b/tests/CoreTests/android/core/StrictMathTest.java index c98c91d..92e6cb6 100644 --- a/tests/CoreTests/android/core/StrictMathTest.java +++ b/tests/CoreTests/android/core/StrictMathTest.java @@ -709,8 +709,7 @@ public class StrictMathTest extends TestCase { /** * @tests java.lang.StrictMath#tanh(double) */ - // TODO: Known failure, temporarily remove from small suite - // @SmallTest + @SmallTest public void testTanhD() { // Test for special situations assertTrue(Double.isNaN(StrictMath.tanh(Double.NaN))); diff --git a/tests/CoreTests/com/android/internal/telephony/PhoneNumberUtilsTest.java b/tests/CoreTests/com/android/internal/telephony/PhoneNumberUtilsTest.java index 20ea4d7..aa2981b 100644 --- a/tests/CoreTests/com/android/internal/telephony/PhoneNumberUtilsTest.java +++ b/tests/CoreTests/com/android/internal/telephony/PhoneNumberUtilsTest.java @@ -25,7 +25,7 @@ import junit.framework.TestCase; public class PhoneNumberUtilsTest extends TestCase { @SmallTest - public void testA() throws Exception { + public void testExtractNetworkPortion() throws Exception { assertEquals( "+17005554141", PhoneNumberUtils.extractNetworkPortion("+17005554141") @@ -181,6 +181,68 @@ public class PhoneNumberUtilsTest extends TestCase { } @SmallTest + public void testExtractNetworkPortionAlt() throws Exception { + assertEquals( + "+17005554141", + PhoneNumberUtils.extractNetworkPortionAlt("+17005554141") + ); + + assertEquals( + "+17005554141", + PhoneNumberUtils.extractNetworkPortionAlt("+1 (700).555-4141") + ); + + assertEquals( + "17005554141", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-4141") + ); + + // This may seem wrong, but it's probably ok + assertEquals( + "17005554141*#", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-4141*#") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-41NN") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-41NN,1234") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-41NN;1234") + ); + + // An MMI string is unperterbed, even though it contains a + // (valid in this case) embedded + + assertEquals( + "**21**+17005554141#", + PhoneNumberUtils.extractNetworkPortionAlt("**21**+17005554141#") + ); + + assertEquals( + "*31#+447966164208", + PhoneNumberUtils.extractNetworkPortionAlt("*31#+447966164208") + ); + + assertEquals( + "*31#+447966164208", + PhoneNumberUtils.extractNetworkPortionAlt("*31# (+44) 79 6616 4208") + ); + + assertEquals("", PhoneNumberUtils.extractNetworkPortionAlt("")); + + assertEquals("", PhoneNumberUtils.extractNetworkPortionAlt(",1234")); + + assertNull(PhoneNumberUtils.extractNetworkPortionAlt(null)); + } + + @SmallTest public void testB() throws Exception { assertEquals("", PhoneNumberUtils.extractPostDialPortion("+17005554141")); assertEquals("", PhoneNumberUtils.extractPostDialPortion("+1 (700).555-4141")); @@ -252,6 +314,9 @@ public class PhoneNumberUtilsTest extends TestCase { // 444 is not a valid country code, but // matchIntlPrefixAndCC doesnt know this assertTrue(PhoneNumberUtils.compare("+444 207 792 3490", "0 207 792 3490")); + + // compare SMS short code + assertTrue(PhoneNumberUtils.compare("404-04", "40404")); } diff --git a/wifi/java/android/net/wifi/IWifiManager.aidl b/wifi/java/android/net/wifi/IWifiManager.aidl index 73dbb6f..f3738e3 100644 --- a/wifi/java/android/net/wifi/IWifiManager.aidl +++ b/wifi/java/android/net/wifi/IWifiManager.aidl @@ -70,6 +70,8 @@ interface IWifiManager boolean releaseWifiLock(IBinder lock); + void initializeMulticastFiltering(); + boolean isMulticastEnabled(); void acquireMulticastLock(IBinder binder, String tag); diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java index 27755ed..178f76e 100644 --- a/wifi/java/android/net/wifi/WifiManager.java +++ b/wifi/java/android/net/wifi/WifiManager.java @@ -1056,4 +1056,17 @@ public class WifiManager { return false; } } + + /** + * Initialize the multicast filtering to 'on' + * @hide no intent to publish + */ + public boolean initializeMulticastFiltering() { + try { + mService.initializeMulticastFiltering(); + return true; + } catch (RemoteException e) { + return false; + } + } } diff --git a/wifi/java/android/net/wifi/WifiMonitor.java b/wifi/java/android/net/wifi/WifiMonitor.java index fc750e2..1e322bd 100644 --- a/wifi/java/android/net/wifi/WifiMonitor.java +++ b/wifi/java/android/net/wifi/WifiMonitor.java @@ -255,6 +255,8 @@ public class WifiMonitor { mWifiStateTracker.notifyDriverStopped(); } else if (state.equals("STARTED")) { mWifiStateTracker.notifyDriverStarted(); + } else if (state.equals("HANGED")) { + mWifiStateTracker.notifyDriverHung(); } } diff --git a/wifi/java/android/net/wifi/WifiStateTracker.java b/wifi/java/android/net/wifi/WifiStateTracker.java index b7d3a6e..66552e4 100644 --- a/wifi/java/android/net/wifi/WifiStateTracker.java +++ b/wifi/java/android/net/wifi/WifiStateTracker.java @@ -87,12 +87,19 @@ public class WifiStateTracker extends NetworkStateTracker { /** * The driver is started or stopped. The object will be the state: true for * started, false for stopped. - */ + */ private static final int EVENT_DRIVER_STATE_CHANGED = 12; private static final int EVENT_PASSWORD_KEY_MAY_BE_INCORRECT = 13; private static final int EVENT_MAYBE_START_SCAN_POST_DISCONNECT = 14; /** + * The driver state indication. + */ + private static final int DRIVER_STARTED = 0; + private static final int DRIVER_STOPPED = 1; + private static final int DRIVER_HUNG = 2; + + /** * Interval in milliseconds between polling for connection * status items that are not sent via asynchronous events. * An example is RSSI (signal strength). @@ -556,7 +563,7 @@ public class WifiStateTracker extends NetworkStateTracker { mRunState = RUN_STATE_STOPPED; // Send a driver stopped message to our handler - Message.obtain(this, EVENT_DRIVER_STATE_CHANGED, 0, 0).sendToTarget(); + Message.obtain(this, EVENT_DRIVER_STATE_CHANGED, DRIVER_STOPPED, 0).sendToTarget(); } /** @@ -565,9 +572,17 @@ public class WifiStateTracker extends NetworkStateTracker { */ void notifyDriverStarted() { // Send a driver started message to our handler - Message.obtain(this, EVENT_DRIVER_STATE_CHANGED, 1, 0).sendToTarget(); + Message.obtain(this, EVENT_DRIVER_STATE_CHANGED, DRIVER_STARTED, 0).sendToTarget(); } - + + /** + * Send the tracker a notification that the Wi-Fi driver has hung and needs restarting. + */ + void notifyDriverHung() { + // Send a driver hanged message to our handler + Message.obtain(this, EVENT_DRIVER_STATE_CHANGED, DRIVER_HUNG, 0).sendToTarget(); + } + /** * Set the interval timer for polling connection information * that is not delivered asynchronously. @@ -786,16 +801,8 @@ public class WifiStateTracker extends NetworkStateTracker { * Filter out multicast packets. This saves battery power, since * the CPU doesn't have to spend time processing packets that * are going to end up being thrown away. - * - * Note that rather than turn this off directly, we use the - * public api - this keeps us all in sync - turn multicast on - * first and then off.. if nobody else wants it on it'll be - * off then and it's all synchronized within the API. */ - WifiManager.MulticastLock l = - mWM.createMulticastLock("WifiStateTracker"); - l.acquire(); - l.release(); + mWM.initializeMulticastFiltering(); if (mBluetoothA2dp == null) { mBluetoothA2dp = new BluetoothA2dp(mContext); @@ -1157,15 +1164,14 @@ public class WifiStateTracker extends NetworkStateTracker { break; case EVENT_DRIVER_STATE_CHANGED: - boolean driverStarted = msg.arg1 != 0; - // Wi-Fi driver state changed: - // [31- 1] Reserved for future use - // [ 0- 0] Driver start (1) or stopped (0) - eventLogParam = driverStarted ? 1 : 0; - EventLog.writeEvent(EVENTLOG_DRIVER_STATE_CHANGED, eventLogParam); - - if (driverStarted) { + // 0 STARTED + // 1 STOPPED + // 2 HUNG + EventLog.writeEvent(EVENTLOG_DRIVER_STATE_CHANGED, msg.arg1); + + switch (msg.arg1) { + case DRIVER_STARTED: /** * Set the number of allowed radio channels according * to the system setting, since it gets reset by the @@ -1184,6 +1190,15 @@ public class WifiStateTracker extends NetworkStateTracker { } } } + break; + case DRIVER_HUNG: + Log.e(TAG, "Wifi Driver reports HUNG - reloading."); + /** + * restart the driver - toggle off and on + */ + mWM.setWifiEnabled(false); + mWM.setWifiEnabled(true); + break; } noteRunState(); break; |
