Merge "Embed branch in project name display"
diff --git a/build.gradle b/build.gradle
index 84753dd..0307507 100644
--- a/build.gradle
+++ b/build.gradle
@@ -49,6 +49,8 @@
 
 init.setupRelease()
 
+apply from: 'buildSrc/jetify.gradle'
+
 init.enableDoclavaAndJDiff(this, new DacOptions("android/support", "SUPPORT_DATA"))
 
 ///// FLATFOOT START
diff --git a/buildSrc/jetify.gradle b/buildSrc/jetify.gradle
new file mode 100644
index 0000000..6fdc5eb
--- /dev/null
+++ b/buildSrc/jetify.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+def standaloneProject = project(":jetifier-standalone")
+def jetifierBin = file("${standaloneProject.buildDir}/install/jetifier-standalone/bin/jetifier-standalone")
+
+task jetifyArchive(type: Exec) {
+    description "Produces a zip of jetified artifacts by running Jetifier against top-of-tree*.zip" +
+            " This task only exists to enable dejetifyArchive to have a realistic archive to test " +
+            " with. This task should be deleted once Jetpack exists."
+
+    dependsOn tasks['createArchive']
+    dependsOn ':jetifier-standalone:installDist'
+    inputs.file project.tasks['createArchive'].archivePath
+
+    outputs.file "${buildDir}/top-of-tree-m2-repository-jetified-${project.ext.buildNumber}.zip"
+
+    commandLine ("bash", "-c",
+        "if ${jetifierBin} -s -outputfile ${outputs.files.singleFile} -i ${inputs.files.singleFile}; then\n" +
+        "  echo success;\n" +
+        "else\n" +
+        "  echo jetifyArchive was not expected to work anyway. Ask jeffrygaston@ or pavlis@ to make it work.\n" +
+        "  exit 1\n" +
+        "fi")
+}
+
+task dejetifyArchive(type: Exec) {
+    description "Produces a zip of dejetified artifacts by running Dejetifier against jetified" +
+            " artifacts, for temporary usage by external clients that haven't upgraded to Jetpack" +
+            " yet."
+
+    dependsOn tasks['jetifyArchive']
+    inputs.file project.tasks['jetifyArchive'].outputs.files.singleFile
+
+    outputs.file "${buildDir}/top-of-tree-m2-repository-dejetified-${project.ext.buildNumber}.zip"
+
+
+    commandLine ("${jetifierBin}", "-s", "-outputfile", "${outputs.files.singleFile}",  "-i", "${inputs.files.singleFile}")
+}
+
diff --git a/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java b/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java
index 97706a0..8371485 100644
--- a/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java
+++ b/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java
@@ -22,6 +22,7 @@
 import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsEqual.equalTo;
@@ -68,6 +69,7 @@
 
     private PagedListViewTestActivity mActivity;
     private PagedListView mPagedListView;
+    private ListItemAdapter mAdapter;
 
     @Before
     public void setUp() {
@@ -79,8 +81,9 @@
         ListItemProvider provider = new ListItemProvider.ListProvider(
                 new ArrayList<>(items));
         try {
+            mAdapter = new ListItemAdapter(mActivity, provider);
             mActivityRule.runOnUiThread(() -> {
-                mPagedListView.setAdapter(new ListItemAdapter(mActivity, provider));
+                mPagedListView.setAdapter(mAdapter);
             });
         } catch (Throwable throwable) {
             throwable.printStackTrace();
@@ -204,6 +207,45 @@
     }
 
     @Test
+    public void testSetSwitchState() {
+        TextListItem item0 = new TextListItem(mActivity);
+        item0.setSwitch(true, true, null);
+
+        setupPagedListView(Arrays.asList(item0));
+
+        item0.setSwitchState(false);
+        try {
+            mActivityRule.runOnUiThread(() -> mAdapter.notifyItemChanged(0));
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+        }
+        // Wait for paged list view to layout by using espresso to scroll to a position.
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
+
+        TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+        assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
+        assertThat(viewHolder.getSwitch().isChecked(), is(equalTo(false)));
+    }
+
+    @Test
+    public void testSetSwitchStateHasNoEffectIfSwitchIsNotEnabled() {
+        TextListItem item0 = new TextListItem(mActivity);
+        setupPagedListView(Arrays.asList(item0));
+
+        item0.setSwitchState(false);
+        try {
+            mActivityRule.runOnUiThread(() -> mAdapter.notifyItemChanged(0));
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+        }
+        // Wait for paged list view to layout by using espresso to scroll to a position.
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
+
+        TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+        assertThat(viewHolder.getSwitch().getVisibility(), is(not(equalTo(View.VISIBLE))));
+    }
+
+    @Test
     public void testDividersAreOptional() {
         TextListItem item0 = new TextListItem(mActivity);
         item0.setSupplementalIcon(android.R.drawable.sym_def_app_icon, false);
diff --git a/car/src/main/java/androidx/car/widget/TextListItem.java b/car/src/main/java/androidx/car/widget/TextListItem.java
index 871c14b..c87321c 100644
--- a/car/src/main/java/androidx/car/widget/TextListItem.java
+++ b/car/src/main/java/androidx/car/widget/TextListItem.java
@@ -705,6 +705,18 @@
     }
 
     /**
+     * Sets the state of {@code Switch}. For this method to take effect,
+     * {@link #setSwitch(boolean, boolean, CompoundButton.OnCheckedChangeListener)} must be called
+     * first to set {@code Supplemental Action} as a {@code Switch}.
+     *
+     * @param isChecked sets the "checked/unchecked, namely on/off" state of switch.
+     */
+    public void setSwitchState(boolean isChecked) {
+        mSwitchChecked = isChecked;
+        markDirty();
+    }
+
+    /**
      * Holds views of TextListItem.
      */
     public static class ViewHolder extends ListItem.ViewHolder {
diff --git a/compat/api/current.txt b/compat/api/current.txt
index 7902a1e..e97db8c 100644
--- a/compat/api/current.txt
+++ b/compat/api/current.txt
@@ -138,10 +138,13 @@
 
   public final class AppOpsManagerCompat {
     method public static int noteOp(android.content.Context, java.lang.String, int, java.lang.String);
+    method public static int noteOpNoThrow(android.content.Context, java.lang.String, int, java.lang.String);
     method public static int noteProxyOp(android.content.Context, java.lang.String, java.lang.String);
+    method public static int noteProxyOpNoThrow(android.content.Context, java.lang.String, java.lang.String);
     method public static java.lang.String permissionToOp(java.lang.String);
     field public static final int MODE_ALLOWED = 0; // 0x0
     field public static final int MODE_DEFAULT = 3; // 0x3
+    field public static final int MODE_ERRORED = 2; // 0x2
     field public static final int MODE_IGNORED = 1; // 0x1
   }
 
diff --git a/compat/src/main/java/android/support/v4/app/AppOpsManagerCompat.java b/compat/src/main/java/android/support/v4/app/AppOpsManagerCompat.java
index 7e97199..796e81f 100644
--- a/compat/src/main/java/android/support/v4/app/AppOpsManagerCompat.java
+++ b/compat/src/main/java/android/support/v4/app/AppOpsManagerCompat.java
@@ -32,14 +32,21 @@
      * Result from {@link #noteOp}: the given caller is allowed to
      * perform the given operation.
      */
-    public static final int MODE_ALLOWED = 0;
+    public static final int MODE_ALLOWED = AppOpsManager.MODE_ALLOWED;
 
     /**
      * Result from {@link #noteOp}: the given caller is not allowed to perform
      * the given operation, and this attempt should <em>silently fail</em> (it
      * should not cause the app to crash).
      */
-    public static final int MODE_IGNORED = 1;
+    public static final int MODE_IGNORED = AppOpsManager.MODE_IGNORED;
+
+    /**
+     * Result from {@link #noteOpNoThrow}: the
+     * given caller is not allowed to perform the given operation, and this attempt should
+     * cause it to have a fatal error, typically a {@link SecurityException}.
+     */
+    public static final int MODE_ERRORED = AppOpsManager.MODE_ERRORED;
 
     /**
      * Result from {@link #noteOp}: the given caller should use its default
@@ -47,12 +54,17 @@
      * with appop permissions, and callers must explicitly check for it and
      * deal with it.
      */
-    public static final int MODE_DEFAULT = 3;
+    public static final int MODE_DEFAULT = AppOpsManager.MODE_DEFAULT;
 
     private AppOpsManagerCompat() {}
 
     /**
      * Gets the app op name associated with a given permission.
+     * <p>
+     * <strong>Compatibility</strong>
+     * <ul>
+     * <li>On API 22 and lower, this method always returns {@code null}
+     * </ul>
      *
      * @param permission The permission.
      * @return The app op associated with the permission or null.
@@ -72,6 +84,11 @@
      * that these two match, and if not, return {@link #MODE_IGNORED}.  If this call
      * succeeds, the last execution time of the operation for this app will be updated to
      * the current time.
+     * <p>
+     * <strong>Compatibility</strong>
+     * <ul>
+     * <li>On API 18 and lower, this method always returns {@link #MODE_IGNORED}
+     * </ul>
      * @param context Your context.
      * @param op The operation to note.  One of the OPSTR_* constants.
      * @param uid The user id of the application attempting to perform the operation.
@@ -83,8 +100,9 @@
      */
     public static int noteOp(@NonNull Context context, @NonNull String op, int uid,
             @NonNull String packageName) {
-        if (SDK_INT >= 23) {
-            AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+        if (SDK_INT >= 19) {
+            AppOpsManager appOpsManager =
+                    (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
             return appOpsManager.noteOp(op, uid, packageName);
         } else {
             return MODE_IGNORED;
@@ -92,6 +110,26 @@
     }
 
     /**
+     * Like {@link #noteOp} but instead of throwing a {@link SecurityException} it
+     * returns {@link #MODE_ERRORED}.
+     * <p>
+     * <strong>Compatibility</strong>
+     * <ul>
+     * <li>On API 18 and lower, this method always returns {@link #MODE_IGNORED}
+     * </ul>
+     */
+    public static int noteOpNoThrow(@NonNull Context context, @NonNull String op, int uid,
+            @NonNull String packageName) {
+        if (SDK_INT >= 19) {
+            AppOpsManager appOpsManager =
+                    (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            return appOpsManager.noteOpNoThrow(op, uid, packageName);
+        } else {
+            return MODE_IGNORED;
+        }
+    }
+
+    /**
      * Make note of an application performing an operation on behalf of another
      * application when handling an IPC. Note that you must pass the package name
      * of the application that is being proxied while its UID will be inferred from
@@ -99,6 +137,11 @@
      * package name match, and if not, return {@link #MODE_IGNORED}. If this call
      * succeeds, the last execution time of the operation for the proxied app and
      * your app will be updated to the current time.
+     * <p>
+     * <strong>Compatibility</strong>
+     * <ul>
+     * <li>On API 22 and lower, this method always returns {@link #MODE_IGNORED}
+     * </ul>
      * @param context Your context.
      * @param op The operation to note.  One of the OPSTR_* constants.
      * @param proxiedPackageName The name of the application calling into the proxy application.
@@ -116,4 +159,23 @@
             return MODE_IGNORED;
         }
     }
+
+    /**
+     * Like {@link #noteProxyOp(Context, String, String)} but instead
+     * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}.
+     * <p>
+     * <strong>Compatibility</strong>
+     * <ul>
+     * <li>On API 22 and lower, this method always returns {@link #MODE_IGNORED}
+     * </ul>
+     */
+    public static int noteProxyOpNoThrow(@NonNull Context context, @NonNull String op,
+            @NonNull String proxiedPackageName) {
+        if (SDK_INT >= 23) {
+            AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+            return appOpsManager.noteProxyOpNoThrow(op, proxiedPackageName);
+        } else {
+            return MODE_IGNORED;
+        }
+    }
 }
diff --git a/compat/src/main/java/android/support/v4/app/RemoteInput.java b/compat/src/main/java/android/support/v4/app/RemoteInput.java
index 0ae3e81..e50b878 100644
--- a/compat/src/main/java/android/support/v4/app/RemoteInput.java
+++ b/compat/src/main/java/android/support/v4/app/RemoteInput.java
@@ -33,7 +33,7 @@
 /**
  * Helper for using the {@link android.app.RemoteInput}.
  */
-public final class RemoteInput extends RemoteInputCompatBase.RemoteInput {
+public final class RemoteInput {
     private static final String TAG = "RemoteInput";
 
     /** Label used to denote the clip data type used for remote input transport */
@@ -67,7 +67,6 @@
      * Get the key that the result of this input will be set in from the Bundle returned by
      * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
      */
-    @Override
     public String getResultKey() {
         return mResultKey;
     }
@@ -75,7 +74,6 @@
     /**
      * Get the label to display to users when collecting this input.
      */
-    @Override
     public CharSequence getLabel() {
         return mLabel;
     }
@@ -83,12 +81,10 @@
     /**
      * Get possible input choices. This can be {@code null} if there are no choices to present.
      */
-    @Override
     public CharSequence[] getChoices() {
         return mChoices;
     }
 
-    @Override
     public Set<String> getAllowedDataTypes() {
         return mAllowedDataTypes;
     }
@@ -111,7 +107,6 @@
      * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
      * if you set this to false and {@link #getChoices} returns {@code null} or empty.
      */
-    @Override
     public boolean getAllowFreeFormInput() {
         return mAllowFreeFormTextInput;
     }
@@ -119,7 +114,6 @@
     /**
      * Get additional metadata carried around with this remote input.
      */
-    @Override
     public Bundle getExtras() {
         return mExtras;
     }
diff --git a/compat/src/main/java/android/support/v4/app/RemoteInputCompatBase.java b/compat/src/main/java/android/support/v4/app/RemoteInputCompatBase.java
deleted file mode 100644
index fa7f7b6..0000000
--- a/compat/src/main/java/android/support/v4/app/RemoteInputCompatBase.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2017 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.support.v4.app;
-
-import android.os.Bundle;
-
-import java.util.Set;
-
-/**
- * @deprecated This class was not meant to be made public.
- */
-@Deprecated
-class RemoteInputCompatBase {
-
-    /**
-     * @deprecated This class was not meant to be made public.
-     */
-    @Deprecated
-    public abstract static class RemoteInput {
-        /**
-         * @deprecated This method was not meant to be made public.
-         */
-        @Deprecated
-        public RemoteInput() {}
-
-        /**
-         * @deprecated This method was not meant to be made public.
-         */
-        @Deprecated
-        protected abstract String getResultKey();
-
-        /**
-         * @deprecated This method was not meant to be made public.
-         */
-        @Deprecated
-        protected abstract CharSequence getLabel();
-
-        /**
-         * @deprecated This method was not meant to be made public.
-         */
-        @Deprecated
-        protected abstract CharSequence[] getChoices();
-
-        /**
-         * @deprecated This method was not meant to be made public.
-         */
-        @Deprecated
-        protected abstract boolean getAllowFreeFormInput();
-
-        /**
-         * @deprecated This method was not meant to be made public.
-         */
-        @Deprecated
-        protected abstract Bundle getExtras();
-
-        /**
-         * @deprecated This method was not meant to be made public.
-         */
-        @Deprecated
-        protected abstract Set<String> getAllowedDataTypes();
-
-        /**
-         * @deprecated This class was not meant to be made public.
-         */
-        @Deprecated
-        public interface Factory {
-            RemoteInput build(String resultKey, CharSequence label,
-                    CharSequence[] choices, boolean allowFreeFormInput, Bundle extras,
-                    Set<String> allowedDataTypes);
-            RemoteInput[] newArray(int length);
-        }
-    }
-}
diff --git a/core-utils/java/android/support/v4/content/PermissionChecker.java b/core-utils/java/android/support/v4/content/PermissionChecker.java
index c9f18a9..e4274d4 100644
--- a/core-utils/java/android/support/v4/content/PermissionChecker.java
+++ b/core-utils/java/android/support/v4/content/PermissionChecker.java
@@ -110,7 +110,7 @@
             packageName = packageNames[0];
         }
 
-        if (AppOpsManagerCompat.noteProxyOp(context, op, packageName)
+        if (AppOpsManagerCompat.noteProxyOpNoThrow(context, op, packageName)
                 != AppOpsManagerCompat.MODE_ALLOWED) {
             return PERMISSION_DENIED_APP_OP;
         }
diff --git a/core-utils/tests/java/android/support/v4/content/PermissionCheckerTest.java b/core-utils/tests/java/android/support/v4/content/PermissionCheckerTest.java
new file mode 100644
index 0000000..298624b
--- /dev/null
+++ b/core-utils/tests/java/android/support/v4/content/PermissionCheckerTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 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.support.v4.content;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link PermissionChecker}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PermissionCheckerTest {
+    private Context mContext;
+
+    @Before
+    public void setup() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    public void testCheckPermission() throws Exception {
+        assertEquals(PermissionChecker.PERMISSION_DENIED, PermissionChecker.checkSelfPermission(
+                mContext, Manifest.permission.ANSWER_PHONE_CALLS));
+        assertEquals(PermissionChecker.PERMISSION_GRANTED, PermissionChecker.checkSelfPermission(
+                mContext, Manifest.permission.VIBRATE));
+    }
+}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
index e116b23..caebb50 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
@@ -94,7 +94,17 @@
      * - [XmlResourcesTransformer] for java native code
      * - [ProGuardTransformer] for PorGuard files
      */
-    fun transform(inputLibraries: Set<File>, outputPath: Path): TransformationResult {
+    fun transform(inputLibraries: Set<File>,
+            outputPath: Path,
+            outputIsDir: Boolean
+    ): TransformationResult {
+        // 0) Validate arguments
+        if (!outputIsDir && inputLibraries.size > 1) {
+            throw IllegalArgumentException("Cannot process more than 1 library (" + inputLibraries +
+                    ") when it is requested tha the destination (" + outputPath +
+                    ") be made a file")
+        }
+
         // 1) Extract and load all libraries
         val libraries = loadLibraries(inputLibraries)
 
@@ -116,7 +126,13 @@
         transformPomFiles(pomFiles)
 
         // 5) Repackage the libraries back to archives
-        val outputLibraries = libraries.map { it.writeSelfToDir(outputPath) }.toSet()
+        val outputLibraries = libraries.map {
+            if (outputIsDir) {
+                it.writeSelfToDir(outputPath)
+            } else {
+                it.writeSelfToFile(outputPath)
+            }
+        }.toSet()
 
         // TODO: Filter out only the libraries that have been really changed
         return TransformationResult(
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
index 70ea68c..bac2ffb 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
@@ -53,16 +53,21 @@
     }
 
     @Throws(IOException::class)
-    fun writeSelfToDir(outputDirPath: Path) : File {
+    fun writeSelfToDir(outputDirPath: Path): File {
         val outputPath = Paths.get(outputDirPath.toString(), fileName)
 
+        return writeSelfToFile(outputPath)
+    }
+
+    @Throws(IOException::class)
+    fun writeSelfToFile(outputPath: Path): File {
         if (Files.exists(outputPath)) {
             Log.i(TAG, "Deleting old output file")
             Files.delete(outputPath)
         }
 
         // Create directories if they don't exist yet
-        Files.createDirectories(outputDirPath)
+        Files.createDirectories(outputPath.parent)
 
         Log.i(TAG, "Writing archive: %s", outputPath.toUri())
         val file = outputPath.toFile()
@@ -88,7 +93,6 @@
         out.finish()
     }
 
-
     object Builder {
 
         @Throws(IOException::class)
@@ -102,8 +106,7 @@
         }
 
         @Throws(IOException::class)
-        private fun extractArchive(inputStream: InputStream, relativePath: Path)
-                : Archive {
+        private fun extractArchive(inputStream: InputStream, relativePath: Path): Archive {
             val zipIn = ZipInputStream(inputStream)
             val files = mutableListOf<ArchiveItem>()
 
@@ -136,9 +139,8 @@
             return ArchiveFile(relativePath, data)
         }
 
-        private fun isArchive(zipEntry: ZipEntry) : Boolean {
+        private fun isArchive(zipEntry: ZipEntry): Boolean {
             return ARCHIVE_EXTENSIONS.any { zipEntry.name.endsWith(it, ignoreCase = true) }
         }
-
     }
 }
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/bytecode/ClassFilesMoveTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/bytecode/ClassFilesMoveTest.kt
index ba126a6..3b189e6 100644
--- a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/bytecode/ClassFilesMoveTest.kt
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/bytecode/ClassFilesMoveTest.kt
@@ -85,7 +85,7 @@
         val inputFile = File(javaClass.getResource(inputZipPath).file)
 
         val tempDir = createTempDir()
-        val result = processor.transform(setOf(inputFile), tempDir.toPath())
+        val result = processor.transform(setOf(inputFile), tempDir.toPath(), true)
 
         Truth.assertThat(result.filesToAdd).hasSize(1)
         testArchivesAreSame(result.filesToAdd.first(),
@@ -106,7 +106,7 @@
         val inputFile = File(javaClass.getResource(inputZipPath).file)
 
         val tempDir = createTempDir()
-        val result = processor.transform(setOf(inputFile), tempDir.toPath())
+        val result = processor.transform(setOf(inputFile), tempDir.toPath(), true)
 
         Truth.assertThat(result.filesToAdd).hasSize(1)
         testArchivesAreSame(result.filesToAdd.first(),
@@ -129,13 +129,13 @@
             rewritingSupportLib = true)
         val inputFile = File(javaClass.getResource(inputZipPath).file)
         val tempDir = createTempDir()
-        val result = processor.transform(setOf(inputFile), tempDir.toPath())
+        val result = processor.transform(setOf(inputFile), tempDir.toPath(), true)
 
         // Take previous result & reverse it
         val processor2 = Processor.createProcessor(TEST_CONFIG,
             rewritingSupportLib = true,
             reversedMode = true)
-        val result2 = processor2.transform(setOf(result.filesToAdd.first()), tempDir.toPath())
+        val result2 = processor2.transform(setOf(result.filesToAdd.first()), tempDir.toPath(), true)
 
         testArchivesAreSame(result2.filesToAdd.first(),
             File(javaClass.getResource(inputZipPath).file))
@@ -154,7 +154,7 @@
         val inputFile = File(javaClass.getResource(inputZipPath).file)
 
         val tempDir = createTempDir()
-        val result = processor.transform(setOf(inputFile), tempDir.toPath())
+        val result = processor.transform(setOf(inputFile), tempDir.toPath(), true)
 
         Truth.assertThat(result.filesToAdd).hasSize(1)
         testArchivesAreSame(result.filesToAdd.first(),
@@ -197,4 +197,4 @@
             archive.files.forEach { it.accept(this) }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierExtension.kt b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierExtension.kt
index 7b516ec..e0eb60c 100644
--- a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierExtension.kt
+++ b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierExtension.kt
@@ -16,6 +16,7 @@
 
 package android.support.tools.jetifier.plugin.gradle
 
+import groovy.lang.Closure
 import org.gradle.api.Project
 import org.gradle.api.artifacts.Configuration
 import org.gradle.api.artifacts.Dependency
@@ -31,7 +32,6 @@
     /**
      * Adds dependency defined via string notation to be processed by jetifyLibs task.
      *
-     *
      * Example usage in Gradle:
      * dependencies {
      *   compile jetifier.process('groupId:artifactId:1.0')
@@ -42,21 +42,20 @@
     }
 
     /**
-     * Adds dependency defined via string notation to be processed by jetifyLibs task while also
-     * applying the given exclude rules.
-     *
+     * Adds dependency defined via string notation to be processed by jetifyLibs task. This version
+     * supports Gradle's configuration closure that is passed to the Gradle's DependencyHandler.
      *
      * Example usage in Gradle:
      * dependencies {
-     *   compile jetifier.processAndExclude('groupId:artifactId:1.0',
-     *     [group: 'some.package', module: 'moduleName'])
+     *   compile jetifier.process('groupId:artifactId:1.0') {
+     *     exclude group: 'groupId'
+     *
+     *     transitive = false
+     *   }
      * }
      */
-    fun processAndExclude(
-        dependencyNotation: String,
-        vararg excludes: Map<String, String>
-    ): FileCollection {
-        return processAndExclude(project.dependencies.create(dependencyNotation), *excludes)
+    fun process(dependencyNotation: String, closure: Closure<Any>): FileCollection {
+        return process(project.dependencies.create(dependencyNotation, closure))
     }
 
     /**
@@ -69,20 +68,6 @@
     }
 
     /**
-     * Adds dependency to be processed by jetifyLibs task while also applying the given excludes
-     * rules.
-     */
-    fun processAndExclude(
-        dependency: Dependency,
-        vararg excludes: Map<String, String>
-    ): FileCollection {
-        val configuration = project.configurations.detachedConfiguration()
-        configuration.dependencies.add(dependency)
-        excludes.forEach { configuration.exclude(it) }
-        return process(configuration)
-    }
-
-    /**
      * Adds dependencies defined via file collection to be processed by jetifyLibs task.
      *
      * Example usage in Gradle for a single file:
diff --git a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/TasksCommon.kt b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/TasksCommon.kt
index 9114e70..082034fb 100644
--- a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/TasksCommon.kt
+++ b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/TasksCommon.kt
@@ -46,7 +46,7 @@
             Log.logConsumer = JetifierLoggerAdapter(logger)
 
             val processor = Processor.createProcessor(config)
-            return processor.transform(filesToProcess, outputDir.toPath())
+            return processor.transform(filesToProcess, outputDir.toPath(), true)
         }
 
         fun shouldSkipArtifact(artifactId: String, groupId: String?, config: Config): Boolean {
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
index ee2b1d9..8fdb78e 100644
--- a/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
+++ b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
@@ -27,6 +27,7 @@
 import org.apache.commons.cli.Options
 import org.apache.commons.cli.ParseException
 import java.io.File
+import java.nio.file.Path
 import java.nio.file.Paths
 
 class Main {
@@ -37,7 +38,9 @@
 
         val OPTIONS = Options()
         val OPTION_INPUT = createOption("i", "Input libraries paths", multiple = true)
-        val OPTION_OUTPUT = createOption("o", "Output directory path")
+        val OPTION_OUTPUT_DIR = createOption("outputdir", "Output directory path",
+                isRequired = false)
+        val OPTION_OUTPUT_FILE = createOption("outputfile", "Output file", isRequired = false)
         val OPTION_CONFIG = createOption("c", "Input config path", isRequired = false)
         val OPTION_LOG_LEVEL = createOption("l", "Logging level. debug, verbose, error, info " +
             "(default)", isRequired = false)
@@ -75,7 +78,29 @@
         Log.setLevel(cmd.getOptionValue(OPTION_LOG_LEVEL.opt))
 
         val inputLibraries = cmd.getOptionValues(OPTION_INPUT.opt).map { File(it) }.toSet()
-        val outputPath = Paths.get(cmd.getOptionValue(OPTION_OUTPUT.opt))
+        val outputDir = cmd.getOptionValue(OPTION_OUTPUT_DIR.opt)
+        val outputFile = cmd.getOptionValue(OPTION_OUTPUT_FILE.opt)
+        if (outputDir == null && outputFile == null) {
+            throw IllegalArgumentException("Must specify -outputdir or -outputfile")
+        }
+        if (outputDir != null && outputFile != null) {
+            throw IllegalArgumentException("Cannot specify both -outputdir and -outputfile")
+        }
+        if (inputLibraries.size > 1 && outputFile != null) {
+            throw IllegalArgumentException(
+                    "Cannot specify -outputfile when multiple input libraries are given")
+        }
+
+        var outputIsDir = false
+        fun chooseOutputPath(): Path {
+            if (outputFile == null) {
+                outputIsDir = true
+                return Paths.get(outputDir)
+            } else {
+                return Paths.get(outputFile)
+            }
+        }
+        val outputPath = chooseOutputPath()
 
         val config: Config?
         if (cmd.hasOption(OPTION_CONFIG.opt)) {
@@ -97,7 +122,7 @@
             config = config,
             reversedMode = isReversed,
             rewritingSupportLib = rewriteSupportLib)
-        processor.transform(inputLibraries, outputPath)
+        processor.transform(inputLibraries, outputPath, outputIsDir)
     }
 
     private fun parseCmdLine(args: Array<String>): CommandLine? {
diff --git a/slices/core/src/main/java/androidx/app/slice/Slice.java b/slices/core/src/main/java/androidx/app/slice/Slice.java
index 153a34f..b528dfb 100644
--- a/slices/core/src/main/java/androidx/app/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/app/slice/Slice.java
@@ -256,7 +256,9 @@
          */
         public Slice.Builder addAction(@NonNull PendingIntent action,
                 @NonNull Slice s, @Nullable String subType) {
-            mItems.add(new SliceItem(action, s, FORMAT_ACTION, subType, new String[0]));
+            @SliceHint String[] hints = s != null
+                    ? s.getHints().toArray(new String[s.getHints().size()]) : new String[0];
+            mItems.add(new SliceItem(action, s, FORMAT_ACTION, subType, hints));
             return this;
         }