Merge "ExifInterface: add RAW input stream support" into nyc-dev
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 406dc2b..6959137 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -2395,17 +2395,22 @@
                 dragSourceEnd += shift;
             }
 
-            // Delete original selection
-            mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
+            mUndoInputFilter.setForceMerge(true);
+            try {
+                // Delete original selection
+                mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
 
-            // Make sure we do not leave two adjacent spaces.
-            final int prevCharIdx = Math.max(0,  dragSourceStart - 1);
-            final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
-            if (nextCharIdx > prevCharIdx + 1) {
-                CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
-                if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
-                    mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
+                // Make sure we do not leave two adjacent spaces.
+                final int prevCharIdx = Math.max(0,  dragSourceStart - 1);
+                final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
+                if (nextCharIdx > prevCharIdx + 1) {
+                    CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
+                    if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
+                        mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
+                    }
                 }
+            } finally {
+                mUndoInputFilter.setForceMerge(false);
             }
         }
     }
@@ -5497,6 +5502,9 @@
         // rotates the screen during composition.
         private boolean mHasComposition;
 
+        // Whether to merge events into one operation.
+        private boolean mForceMerge;
+
         public UndoInputFilter(Editor editor) {
             mEditor = editor;
         }
@@ -5511,6 +5519,10 @@
             mHasComposition = parcel.readInt() != 0;
         }
 
+        public void setForceMerge(boolean forceMerge) {
+            mForceMerge = forceMerge;
+        }
+
         /**
          * Signals that a user-triggered edit is starting.
          */
@@ -5570,7 +5582,7 @@
                 // Otherwise the user inserted the composition.
                 String newText = TextUtils.substring(source, start, end);
                 EditOperation edit = new EditOperation(mEditor, "", dstart, newText);
-                recordEdit(edit, false /* forceMerge */);
+                recordEdit(edit, mForceMerge);
                 return true;
             }
 
@@ -5584,7 +5596,7 @@
             // the initial input filters run (e.g. a credit card formatter that adds spaces to a
             // string). This results in multiple filter() calls for what the user considers to be
             // a single operation. Always undo the whole set of changes in one step.
-            final boolean forceMerge = isInTextWatcher();
+            final boolean forceMerge = mForceMerge || isInTextWatcher();
 
             // Build a new operation with all the information from this edit.
             String newText = TextUtils.substring(source, start, end);
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index 5dae4a8..844eadb 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -44,6 +44,7 @@
 
 import com.android.frameworks.coretests.R;
 
+import android.support.test.espresso.action.EspressoKey;
 import android.test.ActivityInstrumentationTestCase2;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.KeyEvent;
@@ -172,6 +173,12 @@
         onView(withId(R.id.textview)).check(hasSelection(""));
         assertNoSelectionHandles();
         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex("abc ghi.def".length()));
+
+        // Test undo returns to the original state.
+        onView(withId(R.id.textview)).perform(pressKey(
+                (new EspressoKey.Builder()).withCtrlPressed(true).withKeyCode(KeyEvent.KEYCODE_Z)
+                        .build()));
+        onView(withId(R.id.textview)).check(matches(withText(text)));
     }
 
     @SmallTest
diff --git a/core/tests/systemproperties/Android.mk b/core/tests/systemproperties/Android.mk
index 0c20876..b512396 100644
--- a/core/tests/systemproperties/Android.mk
+++ b/core/tests/systemproperties/Android.mk
@@ -9,7 +9,7 @@
 	$(call all-java-files-under, src)
 
 LOCAL_DX_FLAGS := --core-library
-LOCAL_STATIC_JAVA_LIBRARIES := core-tests android-common frameworks-core-util-lib
+LOCAL_STATIC_JAVA_LIBRARIES := android-common frameworks-core-util-lib
 LOCAL_JAVA_LIBRARIES := android.test.runner
 LOCAL_PACKAGE_NAME := FrameworksCoreSystemPropertiesTests
 LOCAL_JAVA_LANGUAGE_VERSION := 1.8
diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java
index 3642155..fe2796c 100644
--- a/media/java/android/media/browse/MediaBrowser.java
+++ b/media/java/android/media/browse/MediaBrowser.java
@@ -365,7 +365,7 @@
         if (options == null) {
             throw new IllegalArgumentException("options are null");
         }
-        subscribeInternal(parentId, options, callback);
+        subscribeInternal(parentId, new Bundle(options), callback);
     }
 
     /**
@@ -1116,7 +1116,7 @@
                 }
             }
             mCallbacks.add(callback);
-            mOptionsList.add(options == null ? null : new Bundle(options));
+            mOptionsList.add(options);
         }
 
         public boolean removeCallback(Bundle options) {
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
index 203d6dc..72ad2f6 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
@@ -32,6 +32,7 @@
 import android.media.MediaFile;
 import android.mtp.MtpConstants;
 import android.mtp.MtpObjectInfo;
+import android.net.Uri;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.provider.DocumentsContract.Root;
@@ -40,8 +41,9 @@
 import com.android.internal.util.Preconditions;
 
 import java.io.FileNotFoundException;
-import java.io.IOException;
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Database for MTP objects.
@@ -606,7 +608,7 @@
      * @param deviceId Device to find documents.
      * @return Identifier of found document or null.
      */
-    public @Nullable Identifier getUnmappedDocumentsParent(int deviceId) {
+    @Nullable Identifier getUnmappedDocumentsParent(int deviceId) {
         final String fromClosure =
                 TABLE_DOCUMENTS + " AS child INNER JOIN " +
                 TABLE_DOCUMENTS + " AS parent ON " +
@@ -643,6 +645,65 @@
         }
     }
 
+    /**
+     * Removes metadata except for data used by outgoingPersistedUriPermissions.
+     */
+    void cleanDatabase(Uri[] outgoingPersistedUris) {
+        mDatabase.beginTransaction();
+        try {
+            final Set<String> ids = new HashSet<>();
+            for (final Uri uri : outgoingPersistedUris) {
+                String documentId = DocumentsContract.getDocumentId(uri);
+                while (documentId != null) {
+                    if (ids.contains(documentId)) {
+                        break;
+                    }
+                    ids.add(documentId);
+                    try (final Cursor cursor = mDatabase.query(
+                            TABLE_DOCUMENTS,
+                            strings(COLUMN_PARENT_DOCUMENT_ID),
+                            SELECTION_DOCUMENT_ID,
+                            strings(documentId),
+                            null,
+                            null,
+                            null)) {
+                        documentId = cursor.moveToNext() ? cursor.getString(0) : null;
+                    }
+                }
+            }
+            deleteDocumentsAndRoots(
+                    Document.COLUMN_DOCUMENT_ID + " NOT IN " + getIdList(ids), null);
+            mDatabase.setTransactionSuccessful();
+        } finally {
+            mDatabase.endTransaction();
+        }
+    }
+
+    int getLastBootCount() {
+        try (final Cursor cursor = mDatabase.query(
+                TABLE_LAST_BOOT_COUNT, strings(COLUMN_VALUE), null, null, null, null, null)) {
+            if (cursor.moveToNext()) {
+                return cursor.getInt(0);
+            } else {
+                return 0;
+            }
+        }
+    }
+
+    void setLastBootCount(int value) {
+        Preconditions.checkArgumentNonnegative(value, "Boot count must not be negative.");
+        mDatabase.beginTransaction();
+        try {
+            final ContentValues values = new ContentValues();
+            values.put(COLUMN_VALUE, value);
+            mDatabase.delete(TABLE_LAST_BOOT_COUNT, null, null);
+            mDatabase.insert(TABLE_LAST_BOOT_COUNT, null, values);
+            mDatabase.setTransactionSuccessful();
+        } finally {
+            mDatabase.endTransaction();
+        }
+    }
+
     private static class OpenHelper extends SQLiteOpenHelper {
         public OpenHelper(Context context, int flags) {
             super(context,
@@ -655,12 +716,14 @@
         public void onCreate(SQLiteDatabase db) {
             db.execSQL(QUERY_CREATE_DOCUMENTS);
             db.execSQL(QUERY_CREATE_ROOT_EXTRA);
+            db.execSQL(QUERY_CREATE_LAST_BOOT_COUNT);
         }
 
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            db.execSQL("DROP TABLE " + TABLE_DOCUMENTS);
-            db.execSQL("DROP TABLE " + TABLE_ROOT_EXTRA);
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_DOCUMENTS);
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_ROOT_EXTRA);
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_BOOT_COUNT);
             onCreate(db);
         }
     }
@@ -818,4 +881,16 @@
         }
         return results;
     }
+
+    private static String getIdList(Set<String> ids) {
+        String result = "(";
+        for (final String id : ids) {
+            if (result.length() > 1) {
+                result += ",";
+            }
+            result += id;
+        }
+        result += ")";
+        return result;
+    }
 }
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
index ff4b89f..6d98e34 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
@@ -30,7 +30,7 @@
  * Class containing MtpDatabase constants.
  */
 class MtpDatabaseConstants {
-    static final int DATABASE_VERSION = 4;
+    static final int DATABASE_VERSION = 5;
     static final String DATABASE_NAME = "database";
 
     static final int FLAG_DATABASE_IN_MEMORY = 1;
@@ -48,6 +48,11 @@
     static final String TABLE_ROOT_EXTRA = "RootExtra";
 
     /**
+     * Table containing last boot count.
+     */
+    static final String TABLE_LAST_BOOT_COUNT = "LastBootCount";
+
+    /**
      * 'FROM' closure of joining TABLE_DOCUMENTS and TABLE_ROOT_EXTRA.
      */
     static final String JOIN_ROOTS = createJoinFromClosure(
@@ -62,7 +67,13 @@
     static final String COLUMN_PARENT_DOCUMENT_ID = "parent_document_id";
     static final String COLUMN_DOCUMENT_TYPE = "document_type";
     static final String COLUMN_ROW_STATE = "row_state";
-    static final String COLUMN_MAPPING_KEY = "column_mapping_key";
+    static final String COLUMN_MAPPING_KEY = "mapping_key";
+
+    /**
+     * Value for TABLE_LAST_BOOT_COUNT.
+     * Type: INTEGER
+     */
+    static final String COLUMN_VALUE = "value";
 
     /**
      * The state represents that the row has a valid object handle.
@@ -133,6 +144,9 @@
             Root.COLUMN_CAPACITY_BYTES + " INTEGER," +
             Root.COLUMN_MIME_TYPES + " TEXT NOT NULL);";
 
+    static final String QUERY_CREATE_LAST_BOOT_COUNT =
+            "CREATE TABLE " + TABLE_LAST_BOOT_COUNT + " (value INTEGER NOT NULL);";
+
     /**
      * Map for columns names to provide DocumentContract.Root compatible columns.
      * @see SQLiteQueryBuilder#setProjectionMap(Map)
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index 7211253..d4d4591 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -17,6 +17,7 @@
 package com.android.mtp;
 
 import android.content.ContentResolver;
+import android.content.UriPermission;
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources;
 import android.database.Cursor;
@@ -25,6 +26,7 @@
 import android.media.MediaFile;
 import android.mtp.MtpConstants;
 import android.mtp.MtpObjectInfo;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
@@ -33,6 +35,8 @@
 import android.provider.DocumentsContract.Root;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsProvider;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
@@ -42,6 +46,7 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -95,6 +100,21 @@
         mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
         mAppFuse = new AppFuse(TAG, new AppFuseCallback());
         mIntentSender = new ServiceIntentSender(getContext());
+
+        // Check boot count and cleans database if it's first time to launch MtpDocumentsProvider
+        // after booting.
+        final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1);
+        final int lastBootCount = mDatabase.getLastBootCount();
+        if (bootCount != -1 && bootCount != lastBootCount) {
+            mDatabase.setLastBootCount(bootCount);
+            final List<UriPermission> permissions = mResolver.getOutgoingPersistedUriPermissions();
+            final Uri[] uris = new Uri[permissions.size()];
+            for (int i = 0; i < permissions.size(); i++) {
+                uris[i] = permissions.get(i).getUri();
+            }
+            mDatabase.cleanDatabase(uris);
+        }
+
         // TODO: Mount AppFuse on demands.
         try {
             mAppFuse.mount(getContext().getSystemService(StorageManager.class));
@@ -122,6 +142,7 @@
         mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
         mAppFuse = new AppFuse(TAG, new AppFuseCallback());
         mIntentSender = intentSender;
+
         // TODO: Mount AppFuse on demands.
         try {
             mAppFuse.mount(storageManager);
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java
index f9e8225..e49a935 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java
@@ -19,6 +19,7 @@
 import android.database.Cursor;
 import android.mtp.MtpConstants;
 import android.mtp.MtpObjectInfo;
+import android.net.Uri;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.provider.DocumentsContract.Root;
@@ -26,6 +27,7 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import java.io.FileNotFoundException;
+import java.util.Arrays;
 
 import static android.provider.DocumentsContract.Document.*;
 import static com.android.mtp.MtpDatabase.strings;
@@ -1023,6 +1025,62 @@
         assertFalse(mDatabase.getMapper().stopAddingDocuments(null));
     }
 
+    public void testSetBootCount() {
+        assertEquals(0, mDatabase.getLastBootCount());
+        mDatabase.setLastBootCount(10);
+        assertEquals(10, mDatabase.getLastBootCount());
+        try {
+            mDatabase.setLastBootCount(-1);
+            fail();
+        } catch (IllegalArgumentException e) {}
+    }
+
+    public void testCleanDatabase() throws FileNotFoundException {
+        // Add tree.
+        addTestDevice();
+        addTestStorage("1");
+        mDatabase.getMapper().startAddingDocuments("2");
+        mDatabase.getMapper().putChildDocuments(0, "2", OPERATIONS_SUPPORTED, new MtpObjectInfo[] {
+                createDocument(100, "apple.txt", MtpConstants.FORMAT_TEXT, 1024),
+                createDocument(101, "orange.txt", MtpConstants.FORMAT_TEXT, 1024),
+        });
+        mDatabase.getMapper().stopAddingDocuments("2");
+
+        // Disconnect the device.
+        mDatabase.getMapper().startAddingDocuments(null);
+        mDatabase.getMapper().stopAddingDocuments(null);
+
+        // Clean database.
+        mDatabase.cleanDatabase(new Uri[] {
+                DocumentsContract.buildDocumentUri(MtpDocumentsProvider.AUTHORITY, "3")
+        });
+
+        // Add tree again.
+        addTestDevice();
+        addTestStorage("1");
+        mDatabase.getMapper().startAddingDocuments("2");
+        mDatabase.getMapper().putChildDocuments(0, "2", OPERATIONS_SUPPORTED, new MtpObjectInfo[] {
+                createDocument(100, "apple.txt", MtpConstants.FORMAT_TEXT, 1024),
+                createDocument(101, "orange.txt", MtpConstants.FORMAT_TEXT, 1024),
+        });
+        mDatabase.getMapper().stopAddingDocuments("2");
+
+        try (final Cursor cursor = mDatabase.queryChildDocuments(
+                strings(COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME), "2")) {
+            assertEquals(2, cursor.getCount());
+
+            // Persistent uri uses the same ID.
+            cursor.moveToNext();
+            assertEquals("3", cursor.getString(0));
+            assertEquals("apple.txt", cursor.getString(1));
+
+            // Others does not.
+            cursor.moveToNext();
+            assertEquals("5", cursor.getString(0));
+            assertEquals("orange.txt", cursor.getString(1));
+        }
+    }
+
     private void addTestDevice() throws FileNotFoundException {
         TestUtil.addTestDevice(mDatabase);
     }
diff --git a/services/net/java/android/net/dhcp/DhcpPacket.java b/services/net/java/android/net/dhcp/DhcpPacket.java
index 6a255e5..e27f69e 100644
--- a/services/net/java/android/net/dhcp/DhcpPacket.java
+++ b/services/net/java/android/net/dhcp/DhcpPacket.java
@@ -819,7 +819,11 @@
             // server-to-server packets, e.g. for relays.
             if (!isPacketToOrFromClient(udpSrcPort, udpDstPort) &&
                 !isPacketServerToServer(udpSrcPort, udpDstPort)) {
-                return null;
+                // This should almost never happen because we use SO_ATTACH_FILTER on the packet
+                // socket to drop packets that don't have the right source ports. However, it's
+                // possible that a packet arrives between when the socket is bound and when the
+                // filter is set. http://b/26696823 .
+                throw new ParseException("Unexpected UDP ports %d->%d", udpSrcPort, udpDstPort);
             }
         }
 
diff --git a/services/tests/servicestests/src/android/net/dhcp/DhcpPacketTest.java b/services/tests/servicestests/src/android/net/dhcp/DhcpPacketTest.java
index 2a967e6..c322ab8 100644
--- a/services/tests/servicestests/src/android/net/dhcp/DhcpPacketTest.java
+++ b/services/tests/servicestests/src/android/net/dhcp/DhcpPacketTest.java
@@ -558,6 +558,39 @@
     }
 
     @SmallTest
+    public void testUdpInvalidDstPort() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexEncoding.decode((
+            // Ethernet header.
+            "9cd917000000001c2e0000000800" +
+            // IP header.
+            "45a00148000040003d115087d18194fb0a0f7af2" +
+            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
+            // NOTE: The destination port is a non-DHCP port.
+            "0043aaaa01341268" +
+            // BOOTP header.
+            "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" +
+            // MAC address.
+            "9cd91700000000000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options.
+            "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" +
+            "d18180060f0777766d2e6564751c040a0fffffff000000"
+        ).toCharArray(), false));
+
+        try {
+            DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
+            fail("Packet with invalid dst port did not throw ParseException");
+        } catch (ParseException expected) {}
+    }
+
+    @SmallTest
     public void testMultipleRouters() throws Exception {
         final ByteBuffer packet = ByteBuffer.wrap(HexEncoding.decode((
             // Ethernet header.