Merge "MediaSessionCompat: Fix hasCallback"
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index f693884..73cdf37 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -16,7 +16,7 @@
def build_versions = [:]
-build_versions.kotlin = '1.2.0'
+build_versions.kotlin = '1.2.20'
rootProject.ext['build_versions'] = build_versions
diff --git a/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
index ea9edf9..76a1e7f 100644
--- a/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
@@ -30,7 +30,8 @@
const val JAVAPOET = "com.squareup:javapoet:1.8.0"
const val JSR250 = "javax.annotation:javax.annotation-api:1.2"
const val JUNIT = "junit:junit:4.12"
-const val KOTLIN_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:1.2.0"
+const val KOTLIN_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:1.2.20"
+const val KOTLIN_METADATA = "me.eugeniomarletti:kotlin-metadata:1.2.1"
const val LINT = "com.android.tools.lint:lint:26.0.0"
const val MOCKITO_CORE = "org.mockito:mockito-core:2.7.6"
const val MULTIDEX = "com.android.support:multidex:1.0.1"
diff --git a/car/res/layout/car_alert_dialog.xml b/car/res/layout/car_alert_dialog.xml
new file mode 100644
index 0000000..63dc70c
--- /dev/null
+++ b/car/res/layout/car_alert_dialog.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Note: the width is 0dp because ColumnCardView will automatically set a width based
+ on the number of columns it should take up. See ColumnCardView for more details. -->
+<androidx.car.widget.ColumnCardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_gravity="center"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:elevation="@dimen/car_dialog_elevation"
+ app:cardBackgroundColor="@color/car_card"
+ app:cardCornerRadius="@dimen/car_radius_3">
+
+ <LinearLayout
+ android:id="@+id/content_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_keyline_1"
+ android:layout_marginEnd="@dimen/car_keyline_1"
+ android:paddingTop="@dimen/car_padding_4"
+ android:paddingBottom="@dimen/car_padding_4"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/car_dialog_header_height"
+ android:textAppearance="@style/CarTitle2"
+ android:gravity="center_vertical|start"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/CarBody2"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:id="@+id/button_panel"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_dialog_action_bar_height"
+ android:layout_marginTop="@dimen/car_padding_2"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <Button
+ android:id="@+id/positive_button"
+ android:layout_marginEnd="@dimen/car_padding_4"
+ android:layout_width="wrap_content"
+ android:minWidth="0dp"
+ android:padding="0dp"
+ android:textColor="@color/car_accent"
+ android:visibility="gone"
+ style="@style/Widget.Car.Button.Borderless.Colored" />
+
+ <Button
+ android:id="@+id/negative_button"
+ android:layout_width="wrap_content"
+ android:minWidth="0dp"
+ android:padding="0dp"
+ android:textColor="@color/car_accent"
+ android:visibility="gone"
+ style="@style/Widget.Car.Button.Borderless.Colored" />
+ </LinearLayout>
+ </LinearLayout>
+</androidx.car.widget.ColumnCardView>
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
index d397df0..389bed9 100644
--- a/car/res/values/dimens.xml
+++ b/car/res/values/dimens.xml
@@ -103,6 +103,7 @@
<!-- Dialogs -->
<dimen name="car_dialog_header_height">@dimen/car_card_header_height</dimen>
<dimen name="car_dialog_action_bar_height">@dimen/car_card_action_bar_height</dimen>
+ <dimen name="car_dialog_elevation">16dp</dimen>
<!-- Slide Up Menu -->
<dimen name="car_slide_up_menu_initial_height">76dp</dimen>
diff --git a/car/src/main/java/androidx/car/app/CarAlertDialog.java b/car/src/main/java/androidx/car/app/CarAlertDialog.java
new file mode 100644
index 0000000..6a85546
--- /dev/null
+++ b/car/src/main/java/androidx/car/app/CarAlertDialog.java
@@ -0,0 +1,546 @@
+/*
+ * 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.
+ */
+
+package androidx.car.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.text.TextUtils;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.car.R;
+
+/**
+ * A subclass of {@link Dialog} that is tailored for the car environment. This dialog can display a
+ * title text, body text, and up to two buttons -- a positive and negative button. There is no
+ * affordance for displaying a custom view or list of content, differentiating it from a regular
+ * {@code AlertDialog}.
+ */
+public class CarAlertDialog extends Dialog {
+ private final DialogData mData;
+
+ private final int mTopPadding;
+ private final int mBottomPadding;
+ private final int mButtonMinWidth;
+ private final int mButtonSpacing;
+
+ private View mContentView;
+ private TextView mTitleView;
+ private TextView mBodyView;
+
+ private View mButtonPanel;
+ private Button mPositiveButton;
+ private Button mNegativeButton;
+ private ButtonPanelTouchDelegate mButtonPanelTouchDelegate;
+
+ private CarAlertDialog(Context context, DialogData data) {
+ super(context);
+ mData = data;
+
+ Resources res = context.getResources();
+ mTopPadding = res.getDimensionPixelSize(R.dimen.car_padding_4);
+ mBottomPadding = res.getDimensionPixelSize(R.dimen.car_padding_4);
+ mButtonMinWidth = res.getDimensionPixelSize(R.dimen.car_button_min_width);
+ mButtonSpacing = res.getDimensionPixelSize(R.dimen.car_padding_4);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ // Ideally this method should be private; the dialog should only be modifiable through the
+ // Builder. Unfortunately, this method is defined with the Dialog itself and is public.
+ // So, throw an error if this method is ever called. setTitleInternal() should be used
+ // to set the title within this class.
+ throw new UnsupportedOperationException("Title should only be set from the Builder");
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Window window = getWindow();
+ window.setContentView(R.layout.car_alert_dialog);
+
+ // By default, the decor background is white. Set this to be transparent so that
+ // the dialog can have rounded corners and will show the background.
+ window.getDecorView().setBackgroundColor(Color.TRANSPARENT);
+
+ initializeViews();
+ initializeDialogWithData();
+ }
+
+ private void setTitleInternal(CharSequence title) {
+ boolean hasTitle = !TextUtils.isEmpty(title);
+
+ mTitleView.setText(title);
+ mTitleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE);
+
+ // If there's a title, then remove the padding at the top of the content view.
+ int topPadding = hasTitle ? 0 : mTopPadding;
+ mContentView.setPaddingRelative(
+ mContentView.getPaddingStart(),
+ topPadding,
+ mContentView.getPaddingEnd(),
+ mContentView.getPaddingBottom());
+ }
+
+ private void setBody(CharSequence body) {
+ mBodyView.setText(body);
+ mBodyView.setVisibility(TextUtils.isEmpty(body) ? View.GONE : View.VISIBLE);
+ }
+
+ private void setPositiveButton(CharSequence text) {
+ boolean showButton = !TextUtils.isEmpty(text);
+
+ mPositiveButton.setText(text);
+ mPositiveButton.setVisibility(showButton ? View.VISIBLE : View.GONE);
+
+ updateTargetTargetForButton(mPositiveButton);
+ updateButtonPanelVisibility();
+ updateButtonSpacing();
+ }
+
+ private void setNegativeButton(CharSequence text) {
+ mNegativeButton.setText(text);
+ mNegativeButton.setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
+
+ updateTargetTargetForButton(mNegativeButton);
+ updateButtonPanelVisibility();
+ updateButtonSpacing();
+ }
+
+ /**
+ * Checks if the given view that represents a positive or negative button currently meets the
+ * minimum touch target size that is dictated by {@link #mButtonMinWidth}. If it does not, then
+ * this method will utilize a {@link TouchDelegate} to expand the touch target size of that
+ * button.
+ *
+ * @param button One of {@link #mPositiveButton} or {@link #mNegativeButton}.
+ */
+ private void updateTargetTargetForButton(View button) {
+ if (button != mPositiveButton && button != mNegativeButton) {
+ throw new IllegalArgumentException("Method must be passed one of mPositiveButton or "
+ + "mNegativeButton");
+ }
+
+ if (button.getVisibility() != View.VISIBLE) {
+ return;
+ }
+
+ // The TouchDelegate needs to be set after the panel has been laid out in order to get the
+ // hit Rect.
+ mButtonPanel.post(() -> {
+ Rect rect = new Rect();
+ button.getHitRect(rect);
+
+ int hitWidth = Math.abs(rect.right - rect.left);
+
+ TouchDelegate touchDelegate = null;
+
+ // If the button does not meet the minimum requirements for touch target size, then
+ // expand its hit area with a TouchDelegate.
+ if (hitWidth < mButtonMinWidth) {
+ int amountToIncrease = (mButtonMinWidth - hitWidth) / 2;
+ rect.left -= amountToIncrease;
+ rect.right += amountToIncrease;
+
+ touchDelegate = new TouchDelegate(rect, button);
+ }
+
+ if (button == mPositiveButton) {
+ mButtonPanelTouchDelegate.setPositiveButtonDelegate(touchDelegate);
+ } else {
+ mButtonPanelTouchDelegate.setNegativeButtonDelegate(touchDelegate);
+ }
+ });
+ }
+
+ /**
+ * Checks if spacing should be added between the positive and negative button. The spacing is
+ * only needed if both buttons are visible.
+ */
+ private void updateButtonSpacing() {
+ int marginEnd;
+
+ // If both buttons are visible, then there needs to be spacing between them.
+ if ((mPositiveButton.getVisibility() == View.VISIBLE
+ && mNegativeButton.getVisibility() == View.VISIBLE)) {
+ marginEnd = mButtonSpacing;
+ } else {
+ marginEnd = 0;
+ }
+
+ ViewGroup.MarginLayoutParams layoutParams =
+ (ViewGroup.MarginLayoutParams) mPositiveButton.getLayoutParams();
+ layoutParams.setMarginEnd(marginEnd);
+ mPositiveButton.requestLayout();
+ }
+
+ /**
+ * Toggles whether or not the panel containing the action buttons are visible depending on if
+ * a button should be shown.
+ */
+ private void updateButtonPanelVisibility() {
+ boolean hasButtons = mPositiveButton.getVisibility() == View.VISIBLE
+ || mNegativeButton.getVisibility() == View.VISIBLE;
+
+ int visibility = hasButtons ? View.VISIBLE : View.GONE;
+
+ // Visibility is already correct, so nothing further needs to be done.
+ if (mButtonPanel.getVisibility() == visibility) {
+ return;
+ }
+
+ mButtonPanel.setVisibility(visibility);
+
+ // If there are buttons, then remove the padding at the bottom of the content view.
+ int buttonPadding = hasButtons ? 0 : mBottomPadding;
+ mContentView.setPaddingRelative(
+ mContentView.getPaddingStart(),
+ mContentView.getPaddingTop(),
+ mContentView.getPaddingEnd(),
+ buttonPadding);
+ }
+
+ /**
+ * Looks through the {@link DialogData} that was passed to this dialog and initialize its
+ * contents based on what data is present.
+ */
+ private void initializeDialogWithData() {
+ setTitleInternal(mData.mTitle);
+ setBody(mData.mBody);
+ setPositiveButton(mData.mPositiveButtonText);
+ setNegativeButton(mData.mNegativeButtonText);
+ }
+
+ /**
+ * Initializes the views within the dialog that are modifiable based on the data that has been
+ * set on it. Also responsible for hooking up listeners for button clicks.
+ */
+ private void initializeViews() {
+ Window window = getWindow();
+
+ mContentView = window.findViewById(R.id.content_view);
+ mTitleView = window.findViewById(R.id.title);
+ mBodyView = window.findViewById(R.id.body);
+
+ mButtonPanel = window.findViewById(R.id.button_panel);
+ mButtonPanelTouchDelegate = new ButtonPanelTouchDelegate(mButtonPanel);
+ mButtonPanel.setTouchDelegate(mButtonPanelTouchDelegate);
+
+ mPositiveButton = window.findViewById(R.id.positive_button);
+ mNegativeButton = window.findViewById(R.id.negative_button);
+
+ mPositiveButton.setOnClickListener(v -> onPositiveButtonClick());
+ mNegativeButton.setOnClickListener(v -> onNegativeButtonClick());
+ }
+
+ /** Delegates to a listener on the positive button if it exists or dismisses the dialog. */
+ private void onPositiveButtonClick() {
+ if (mData.mPositiveButtonListener != null) {
+ mData.mPositiveButtonListener.onClick(this /* dialog */, BUTTON_POSITIVE);
+ } else {
+ dismiss();
+ }
+ }
+
+ /** Delegates to a listener on the negative button if it exists or dismisses the dialog. */
+ private void onNegativeButtonClick() {
+ if (mData.mNegativeButtonListener != null) {
+ mData.mNegativeButtonListener.onClick(this /* dialog */, BUTTON_NEGATIVE);
+ } else {
+ dismiss();
+ }
+ }
+
+ /**
+ * A composite {@link TouchDelegate} for a button panel that has two buttons. It can hold
+ * multiple {@code TouchDelegate}s and will delegate out touch events to each.
+ */
+ private static final class ButtonPanelTouchDelegate extends TouchDelegate {
+ @Nullable private TouchDelegate mPositiveButtonDelegate;
+ @Nullable private TouchDelegate mNegativeButtonDelegate;
+
+ ButtonPanelTouchDelegate(View view) {
+ super(new Rect(), view);
+ }
+
+ public void setPositiveButtonDelegate(@Nullable TouchDelegate delegate) {
+ mPositiveButtonDelegate = delegate;
+ }
+
+ public void setNegativeButtonDelegate(@Nullable TouchDelegate delegate) {
+ mNegativeButtonDelegate = delegate;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean result = false;
+ float x = event.getX();
+ float y = event.getY();
+ event.setLocation(x, y);
+
+ if (mPositiveButtonDelegate != null) {
+ result = mPositiveButtonDelegate.onTouchEvent(event);
+ }
+
+ if (mNegativeButtonDelegate != null) {
+ result |= mNegativeButtonDelegate.onTouchEvent(event);
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * A class that holds the data that is settable by the {@link Builder} and should be displayed
+ * in the {@link CarAlertDialog}.
+ */
+ private static class DialogData {
+ private CharSequence mTitle;
+ private CharSequence mBody;
+ private CharSequence mPositiveButtonText;
+ private OnClickListener mPositiveButtonListener;
+ private CharSequence mNegativeButtonText;
+ private OnClickListener mNegativeButtonListener;
+ }
+
+ /**
+ * Builder class that can be used to create a {@link CarAlertDialog} by configuring the options
+ * for what shows up in the resulting dialog.
+ */
+ public static class Builder {
+ private final Context mContext;
+ private final DialogData mDialogData;
+
+ private boolean mCancelable = true;
+ private OnCancelListener mOnCancelListener;
+ private OnDismissListener mOnDismissListener;
+
+ /**
+ * Creates a new instance of the {@code Builder}.
+ *
+ * @param context The {@code Context} that the dialog is to be created in.
+ */
+ public Builder(Context context) {
+ mContext = context;
+ mDialogData = new DialogData();
+ }
+
+ /**
+ * Sets the main title of the dialog to be the given string resource.
+ *
+ * @param titleId The resource id of the string to be used as the title.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setTitle(@StringRes int titleId) {
+ mDialogData.mTitle = mContext.getString(titleId);
+ return this;
+ }
+
+ /**
+ * Sets the main title of the dialog for be the given string.
+ *
+ * @param title The string to be used as the title.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setTitle(CharSequence title) {
+ mDialogData.mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets the body text of the dialog to be the given string resource.
+ *
+ * @param bodyId The resource id of the string to be used as the body text.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setBody(@StringRes int bodyId) {
+ mDialogData.mBody = mContext.getString(bodyId);
+ return this;
+ }
+
+ /**
+ * Sets the body text of the dialog to be the given string.
+ *
+ * @param body The string to be used as the body text.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setBody(CharSequence body) {
+ mDialogData.mBody = body;
+ return this;
+ }
+
+ /**
+ * Sets the text of the positive button and the listener that will be invoked when the
+ * button is pressed. If a listener is not provided, then the dialog will dismiss itself
+ * when the positive button is clicked.
+ *
+ * <p>The positive button should be used to accept and continue with the action (e.g.
+ * an "OK" action).
+ *
+ * @param textId The resource id of the string to be used for the positive button text.
+ * @param listener A {@link OnClickListener} to be invoked when the button is clicked. Can
+ * be {@code null} to represent no listener.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setPositiveButton(@StringRes int textId,
+ @Nullable OnClickListener listener) {
+ mDialogData.mPositiveButtonText = mContext.getString(textId);
+ mDialogData.mPositiveButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets the text of the positive button and the listener that will be invoked when the
+ * button is pressed. If a listener is not provided, then the dialog will dismiss itself
+ * when the positive button is clicked.
+ *
+ * <p>The positive button should be used to accept and continue with the action (e.g.
+ * an "OK" action).
+ *
+ * @param text The string to be used for the positive button text.
+ * @param listener A {@link OnClickListener} to be invoked when the button is clicked. Can
+ * be {@code null} to represent no listener.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setPositiveButton(CharSequence text, @Nullable OnClickListener listener) {
+ mDialogData.mPositiveButtonText = text;
+ mDialogData.mPositiveButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets the text of the negative button and the listener that will be invoked when the
+ * button is pressed. If a listener is not provided, then the dialog will dismiss itself
+ * when the negative button is clicked.
+ *
+ * <p>The negative button should be used to cancel any actions the dialog represents.
+ *
+ * @param textId The resource id of the string to be used for the negative button text.
+ * @param listener A {@link OnClickListener} to be invoked when the button is clicked. Can
+ * be {@code null} to represent no listener.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setNegativeButton(@StringRes int textId,
+ @Nullable OnClickListener listener) {
+ mDialogData.mNegativeButtonText = mContext.getString(textId);
+ mDialogData.mNegativeButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets the text of the negative button and the listener that will be invoked when the
+ * button is pressed. If a listener is not provided, then the dialog will dismiss itself
+ * when the negative button is clicked.
+ *
+ * <p>The negative button should be used to cancel any actions the dialog represents.
+ *
+ * @param text The string to be used for the negative button text.
+ * @param listener A {@link OnClickListener} to be invoked when the button is clicked. Can
+ * be {@code null} to represent no listener.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setNegativeButton(CharSequence text, @Nullable OnClickListener listener) {
+ mDialogData.mNegativeButtonText = text;
+ mDialogData.mNegativeButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets whether the dialog is cancelable or not. Default is {@code true}.
+ *
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setCancelable(boolean cancelable) {
+ mCancelable = cancelable;
+ return this;
+ }
+
+ /**
+ * Sets the callback that will be called if the dialog is canceled.
+ *
+ * <p>Even in a cancelable dialog, the dialog may be dismissed for reasons other than
+ * being canceled or one of the supplied choices being selected.
+ * If you are interested in listening for all cases where the dialog is dismissed
+ * and not just when it is canceled, see {@link #setOnDismissListener(OnDismissListener)}.
+ *
+ * @param onCancelListener The listener to be invoked when this dialog is canceled.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ *
+ * @see #setCancelable(boolean)
+ * @see #setOnDismissListener(OnDismissListener)
+ */
+ public Builder setOnCancelListener(OnCancelListener onCancelListener) {
+ mOnCancelListener = onCancelListener;
+ return this;
+ }
+
+ /**
+ * Sets the callback that will be called when the dialog is dismissed for any reason.
+ *
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ public Builder setOnDismissListener(OnDismissListener onDismissListener) {
+ mOnDismissListener = onDismissListener;
+ return this;
+ }
+
+ /**
+ * Creates an {@link CarAlertDialog} with the arguments supplied to this {@code Builder}.
+ *
+ * <p>Calling this method does not display the dialog. If no additional processing is
+ * needed, {@link #show()} may be called instead to both create and display the dialog.
+ */
+ public CarAlertDialog create() {
+ CarAlertDialog dialog = new CarAlertDialog(mContext, mDialogData);
+
+ dialog.setCancelable(mCancelable);
+ dialog.setCanceledOnTouchOutside(mCancelable);
+ dialog.setOnCancelListener(mOnCancelListener);
+ dialog.setOnDismissListener(mOnDismissListener);
+
+ return dialog;
+ }
+
+ /**
+ * Creates an {@link CarAlertDialog} with the arguments supplied to this {@code Builder}
+ * and immediately displays the dialog.
+ *
+ * <p>Calling this method is functionally identical to:
+ * <pre>
+ * CarAlertDialog dialog = new CarAlertDialog.Builder().create();
+ * dialog.show();
+ * </pre>
+ */
+ public CarAlertDialog show() {
+ CarAlertDialog dialog = create();
+ dialog.show();
+ return dialog;
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/android/support/v4/app/LoaderViewModelTest.java b/fragment/src/androidTest/java/android/support/v4/app/LoaderViewModelTest.java
index b53f7dd..911e169 100644
--- a/fragment/src/androidTest/java/android/support/v4/app/LoaderViewModelTest.java
+++ b/fragment/src/androidTest/java/android/support/v4/app/LoaderViewModelTest.java
@@ -17,6 +17,7 @@
package android.support.v4.app;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.Context;
@@ -63,6 +64,8 @@
assertFalse("LoaderInfo shouldn't be destroyed before onCleared", info.mDestroyed);
loaderViewModel.onCleared();
assertTrue("LoaderInfo should be destroyed after onCleared", info.mDestroyed);
+ assertNull("LoaderInfo should be removed from LoaderViewModel after onCleared",
+ loaderViewModel.getLoader(0));
}
private class AlwaysRunningLoaderInfo extends LoaderManagerImpl.LoaderInfo<Boolean> {
diff --git a/fragment/src/main/java/android/support/v4/app/LoaderManagerImpl.java b/fragment/src/main/java/android/support/v4/app/LoaderManagerImpl.java
index 951d908..fdde79f 100644
--- a/fragment/src/main/java/android/support/v4/app/LoaderManagerImpl.java
+++ b/fragment/src/main/java/android/support/v4/app/LoaderManagerImpl.java
@@ -294,6 +294,7 @@
LoaderInfo info = mLoaders.valueAt(index);
info.destroy();
}
+ mLoaders.clear();
}
public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
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 caebb50..aa2e1e3 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
@@ -23,6 +23,7 @@
import android.support.tools.jetifier.core.transform.TransformationContext
import android.support.tools.jetifier.core.transform.Transformer
import android.support.tools.jetifier.core.transform.bytecode.ByteCodeTransformer
+import android.support.tools.jetifier.core.transform.metainf.MetaInfTransformer
import android.support.tools.jetifier.core.transform.pom.PomDocument
import android.support.tools.jetifier.core.transform.pom.PomScanner
import android.support.tools.jetifier.core.transform.proguard.ProGuardTransformer
@@ -37,8 +38,10 @@
* the registered [Transformer]s over the set and creates new archives that will contain the
* transformed files.
*/
-class Processor private constructor (private val context: TransformationContext)
- : ArchiveItemVisitor {
+class Processor private constructor (
+ private val context: TransformationContext,
+ private val transformers: List<Transformer>
+) : ArchiveItemVisitor {
companion object {
private const val TAG = "Processor"
@@ -49,6 +52,27 @@
private const val REVERSE_RESTRICT_TO_PACKAGE = "androidx"
/**
+ * Transformers to be used when refactoring general libraries.
+ */
+ private fun createTransformers(context: TransformationContext) = listOf(
+ // Register your transformers here
+ ByteCodeTransformer(context),
+ XmlResourcesTransformer(context),
+ ProGuardTransformer(context)
+ )
+
+ /**
+ * Transformers to be used when refactoring the support library itself.
+ */
+ private fun createSLTransformers(context: TransformationContext) = listOf(
+ // Register your transformers here
+ ByteCodeTransformer(context),
+ XmlResourcesTransformer(context),
+ ProGuardTransformer(context),
+ MetaInfTransformer(context)
+ )
+
+ /**
* Creates a new instance of the [Processor].
* [config] Transformation configuration
* [reversedMode] Whether the processor should run in reversed mode
@@ -73,18 +97,17 @@
)
}
- val context = TransformationContext(newConfig, rewritingSupportLib)
- return Processor(context)
+ val context = TransformationContext(newConfig, rewritingSupportLib, reversedMode)
+ val transformers = if (rewritingSupportLib) {
+ createSLTransformers(context)
+ } else {
+ createTransformers(context)
+ }
+
+ return Processor(context, transformers)
}
}
- private val transformers = listOf(
- // Register your transformers here
- ByteCodeTransformer(context),
- XmlResourcesTransformer(context),
- ProGuardTransformer(context)
- )
-
/**
* Transforms the input libraries given in [inputLibraries] using all the registered
* [Transformer]s and returns new libraries stored in [outputPath].
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
index b5b4064..08a22a4 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
@@ -25,7 +25,11 @@
/**
* Context to share the transformation state between individual [Transformer]s.
*/
-class TransformationContext(val config: Config, val rewritingSupportLib: Boolean) {
+class TransformationContext(
+ val config: Config,
+ val rewritingSupportLib: Boolean = false,
+ val isInReversedMode: Boolean = false
+) {
// Merges all packages prefixes into one regEx pattern
private val packagePrefixPattern = Pattern.compile(
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/metainf/MetaInfTransformer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/metainf/MetaInfTransformer.kt
new file mode 100644
index 0000000..3220dae
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/metainf/MetaInfTransformer.kt
@@ -0,0 +1,66 @@
+/*
+ * 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
+ */
+
+package android.support.tools.jetifier.core.transform.metainf
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.Transformer
+import java.nio.charset.StandardCharsets
+
+/**
+ * Transformer for META-INF/(.*).version files.
+ *
+ * Replaces version files from the META-INF directory. This should be used only for processing
+ * of the support library itself.
+ */
+class MetaInfTransformer internal constructor(
+ private val context: TransformationContext
+) : Transformer {
+
+ companion object {
+ const val FROM_VERSION = "28.0.0-SNAPSHOT"
+
+ const val TO_VERSION = "1.0.0-SNAPSHOT"
+
+ const val META_INF_DIR = "meta-inf"
+
+ const val VERSION_FILE_SUFFIX = ".version"
+ }
+
+ override fun canTransform(file: ArchiveFile): Boolean {
+ return context.rewritingSupportLib
+ && file.relativePath.toString().contains(META_INF_DIR, ignoreCase = true)
+ && file.fileName.endsWith(VERSION_FILE_SUFFIX, ignoreCase = true)
+ }
+
+ override fun runTransform(file: ArchiveFile) {
+ val sb = StringBuilder(file.data.toString(StandardCharsets.UTF_8))
+
+ var from = FROM_VERSION
+ var to = TO_VERSION
+ if (context.isInReversedMode) {
+ from = TO_VERSION
+ to = FROM_VERSION
+ }
+
+ if (sb.toString() != from) {
+ return
+ }
+
+ file.data = to.toByteArray()
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/metainf/MetaInfTransformerTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/metainf/MetaInfTransformerTest.kt
new file mode 100644
index 0000000..db6cd21
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/metainf/MetaInfTransformerTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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
+ */
+
+package android.support.tools.jetifier.core.transform.metainf
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.transform.PackageMap
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.proguard.ProGuardTypesMap
+import com.google.common.truth.Truth
+import org.junit.Test
+import java.nio.charset.Charset
+import java.nio.file.Path
+import java.nio.file.Paths
+
+class MetaInfTransformerTest {
+
+ @Test
+ fun rewriteVersion_forward() {
+ testRewrite(
+ given = "28.0.0-SNAPSHOT",
+ expected = "1.0.0-SNAPSHOT",
+ filePath = Paths.get("something/META-INF", "support_preference-v7.version"),
+ reverseMode = false
+ )
+ }
+
+ @Test
+ fun rewriteVersion_reversed() {
+ testRewrite(
+ given = "1.0.0-SNAPSHOT",
+ expected = "28.0.0-SNAPSHOT",
+ filePath = Paths.get("something/META-INF", "support_preference-v7.version"),
+ reverseMode = true
+ )
+ }
+
+ @Test
+ fun rewriteVersion_notSLRewrite_shouldSkip() {
+ testRewrite(
+ given = "28.0.0-SNAPSHOT",
+ expected = "28.0.0-SNAPSHOT",
+ filePath = Paths.get("something/META-INF", "support_preference-v7.version"),
+ reverseMode = false,
+ rewritingSupportLib = false,
+ expectedCanTransform = false
+ )
+ }
+
+ @Test
+ fun rewriteVersion_notMatchingVersion_shouldNoOp() {
+ testRewrite(
+ given = "test",
+ expected = "test",
+ filePath = Paths.get("something/META-INF", "support_preference-v7.version")
+ )
+ }
+
+ @Test
+ fun rewriteVersion_notValidSuffix_shouldSkip() {
+ testRewrite(
+ given = "28.0.0-SNAPSHOT",
+ expected = "28.0.0-SNAPSHOT",
+ filePath = Paths.get("something/META-INF", "support_preference-v7.none"),
+ expectedCanTransform = false
+ )
+ }
+
+ @Test
+ fun rewriteVersion_notInMetaInfDir_shouldSkip() {
+ testRewrite(
+ given = "28.0.0-SNAPSHOT",
+ expected = "28.0.0-SNAPSHOT",
+ filePath = Paths.get("something/else", "support_preference-v7.version"),
+ expectedCanTransform = false
+ )
+ }
+
+ private fun testRewrite(
+ given: String,
+ expected: String,
+ filePath: Path,
+ reverseMode: Boolean = false,
+ expectedCanTransform: Boolean = true,
+ rewritingSupportLib: Boolean = true
+ ) {
+ val config = Config(
+ restrictToPackagePrefixes = emptyList(),
+ rewriteRules = emptyList(),
+ slRules = emptyList(),
+ pomRewriteRules = emptyList(),
+ packageMap = PackageMap.EMPTY,
+ typesMap = TypesMap.EMPTY,
+ proGuardMap = ProGuardTypesMap.EMPTY
+ )
+ val context = TransformationContext(config,
+ rewritingSupportLib = rewritingSupportLib,
+ isInReversedMode = reverseMode)
+ val transformer = MetaInfTransformer(context)
+
+ val file = ArchiveFile(filePath, given.toByteArray())
+
+ val canTransform = transformer.canTransform(file)
+ if (canTransform) {
+ transformer.runTransform(file)
+ }
+
+ val strResult = file.data.toString(Charset.defaultCharset())
+
+ Truth.assertThat(canTransform).isEqualTo(expectedCanTransform)
+ Truth.assertThat(strResult).isEqualTo(expected)
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
index cb43c5f..37075d3 100644
--- a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
@@ -85,7 +85,7 @@
class ProGuardTesterForFile(private val config: Config, private val given: String) {
fun rewritesTo(expected: String) {
- val context = TransformationContext(config, rewritingSupportLib = false)
+ val context = TransformationContext(config)
val transformer = ProGuardTransformer(context)
val file = ArchiveFile(Paths.get("proguard.txt"), given.toByteArray())
transformer.runTransform(file)
@@ -100,7 +100,7 @@
class ProGuardTesterForType(private val config: Config, private val given: String) {
fun getsRewrittenTo(expectedType: String) {
- val context = TransformationContext(config, rewritingSupportLib = false)
+ val context = TransformationContext(config)
val mapper = ProGuardTypesMapper(context)
val result = mapper.replaceType(given)
@@ -112,7 +112,7 @@
class ProGuardTesterForArgs(private val config: Config, private val given: String) {
fun getRewrittenTo(expectedArguments: String) {
- val context = TransformationContext(config, rewritingSupportLib = false)
+ val context = TransformationContext(config)
val mapper = ProGuardTypesMapper(context)
val result = mapper.replaceMethodArgs(given)
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
index c020814..4aaaae0 100644
--- a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
@@ -330,7 +330,7 @@
proGuardMap = ProGuardTypesMap.EMPTY,
packageMap = packageMap
)
- val context = TransformationContext(config, rewritingSupportLib)
+ val context = TransformationContext(config, rewritingSupportLib = rewritingSupportLib)
context.libraryName = libraryName
val processor = XmlResourcesTransformer(context)
val fileName = if (isManifestFile) {
diff --git a/room/compiler/build.gradle b/room/compiler/build.gradle
index 0d62cf8..c3319ac 100644
--- a/room/compiler/build.gradle
+++ b/room/compiler/build.gradle
@@ -49,6 +49,7 @@
compile(JAVAPOET)
compile(ANTLR)
compile(XERIAL)
+ compile(KOTLIN_METADATA)
compile(APACHE_COMMONS_CODEC)
testCompile(GOOGLE_COMPILE_TESTING)
testCompile project(":paging:common")
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
index 0ba311e..3d73948 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
@@ -47,6 +47,13 @@
import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreTypes
+import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
+import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
+import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
+import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
+import org.jetbrains.kotlin.serialization.ProtoBuf
+import org.jetbrains.kotlin.serialization.deserialization.NameResolver
+import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier.ABSTRACT
import javax.lang.model.element.Modifier.PRIVATE
@@ -69,12 +76,28 @@
val element: TypeElement,
val bindingScope: FieldProcessor.BindingScope,
val parent: EmbeddedField?,
- val referenceStack: LinkedHashSet<Name> = LinkedHashSet<Name>()) {
+ val referenceStack: LinkedHashSet<Name> = LinkedHashSet())
+ : KotlinMetadataUtils {
val context = baseContext.fork(element)
+
+ // for KotlinMetadataUtils
+ override val processingEnv: ProcessingEnvironment
+ get() = context.processingEnv
+
+ // opportunistic kotlin metadata
+ private val kotlinMetadata by lazy {
+ try {
+ element.kotlinMetadata
+ } catch (throwable: Throwable) {
+ context.logger.d(element, "failed to read get kotlin metadata from %s", element)
+ }
+ }
+
companion object {
val PROCESSED_ANNOTATIONS = listOf(ColumnInfo::class, Embedded::class,
- Relation::class)
+ Relation::class)
}
+
fun process(): Pojo {
return context.cache.pojos.get(Cache.PojoKey(element, bindingScope, parent), {
referenceStack.add(element.qualifiedName)
@@ -94,9 +117,9 @@
!it.hasAnnotation(Ignore::class)
&& !it.hasAnyOf(STATIC)
&& (!it.hasAnyOf(TRANSIENT)
- || it.hasAnnotation(ColumnInfo::class)
- || it.hasAnnotation(Embedded::class)
- || it.hasAnnotation(Relation::class))
+ || it.hasAnnotation(ColumnInfo::class)
+ || it.hasAnnotation(Embedded::class)
+ || it.hasAnnotation(Relation::class))
}
.groupBy { field ->
context.checker.check(
@@ -199,6 +222,40 @@
constructor = constructor)
}
+ /**
+ * Retrieves the parameter names of a method. If the method is inherited from a dependency
+ * module, the parameter name is not available (not in java spec). For kotlin, since parameter
+ * names are part of the API, we can read them via the kotlin metadata annotation.
+ * <p>
+ * Since we are using an unofficial library to read the metadata, all access to that code
+ * is safe guarded to avoid unexpected failures. In other words, it is a best effort but
+ * better than not supporting these until JB provides a proper API.
+ */
+ private fun getParamNames(method: ExecutableElement): List<String> {
+ val paramNames = method.parameters.map { it.simpleName.toString() }
+ if (paramNames.isEmpty()) {
+ return emptyList()
+ }
+ (kotlinMetadata as? KotlinClassMetadata)?.let {
+ try {
+ val kotlinParams = it
+ .findConstructor(method)
+ ?.tryGetParameterNames(it.data.nameResolver)
+ if (kotlinParams != null) {
+ return kotlinParams
+ }
+ } catch (throwable: Throwable) {
+ context.logger.d(
+ method,
+ "Cannot read kotlin metadata, falling back to jvm signature. %s",
+ throwable.message as Any)
+ }
+ }
+ // either it is java or something went wrong w/ kotlin metadata. default to whatever
+ // we can read.
+ return paramNames
+ }
+
private fun chooseConstructor(
myFields: List<Field>,
embedded: List<EmbeddedField>,
@@ -208,10 +265,12 @@
val fieldMap = myFields.associateBy { it.name }
val embeddedMap = embedded.associateBy { it.field.name }
val typeUtils = context.processingEnv.typeUtils
- val failedConstructors = mutableMapOf<ExecutableElement, List<Constructor.Param?>>()
+ // list of param names -> matched params pairs for each failed constructor
+ val failedConstructors = arrayListOf<FailedConstructor>()
val goodConstructors = constructors.map { constructor ->
- val params = constructor.parameters.map param@ { param ->
- val paramName = param.simpleName.toString()
+ val parameterNames = getParamNames(constructor)
+ val params = constructor.parameters.mapIndexed param@ { index, param ->
+ val paramName = parameterNames[index]
val paramType = param.asType()
val matches = fun(field: Field?): Boolean {
@@ -261,7 +320,7 @@
} else {
context.logger.e(param, ProcessorErrors.ambigiousConstructor(
pojo = element.qualifiedName.toString(),
- paramName = param.simpleName.toString(),
+ paramName = paramName,
matchingFields = matchingFields.map { it.getPath() }
+ embedded.map { it.field.getPath() }
))
@@ -269,7 +328,7 @@
}
}
if (params.any { it == null }) {
- failedConstructors.put(constructor, params)
+ failedConstructors.add(FailedConstructor(constructor, parameterNames, params))
null
} else {
@Suppress("UNCHECKED_CAST")
@@ -278,12 +337,8 @@
}.filterNotNull()
if (goodConstructors.isEmpty()) {
if (failedConstructors.isNotEmpty()) {
- val failureMsg = failedConstructors.entries.joinToString("\n") { entry ->
- val paramsMatching = entry.key.parameters.withIndex().joinToString(", ") {
- "param:${it.value.simpleName} -> matched field:" +
- (entry.value[it.index]?.log() ?: "unmatched")
- }
- "${entry.key} -> [$paramsMatching]"
+ val failureMsg = failedConstructors.joinToString("\n") { entry ->
+ entry.log()
}
context.logger.e(element, ProcessorErrors.MISSING_POJO_CONSTRUCTOR +
"\nTried the following constructors but they failed to match:\n$failureMsg")
@@ -512,7 +567,7 @@
val recursiveTailTypeName = typeElement.qualifiedName
val referenceRecursionList = mutableListOf<Name>()
- with (referenceRecursionList) {
+ with(referenceRecursionList) {
add(recursiveTailTypeName)
addAll(referenceStack.toList().takeLastWhile { it != recursiveTailTypeName })
add(recursiveTailTypeName)
@@ -650,4 +705,51 @@
}
return candidates.first()
}
+
+ /**
+ * Finds the kotlin meteadata for a constructor.
+ */
+ private fun KotlinClassMetadata.findConstructor(
+ executableElement: ExecutableElement
+ ): ProtoBuf.Constructor? {
+ val (nameResolver, classProto) = data
+ val jvmSignature = executableElement.jvmMethodSignature
+ // find constructor
+ return classProto.constructorList.singleOrNull {
+ it.getJvmConstructorSignature(nameResolver, classProto.typeTable) == jvmSignature
+ }
+ }
+
+ /**
+ * Tries to get the parameter names of a kotlin method
+ */
+ private fun ProtoBuf.Constructor.tryGetParameterNames(
+ nameResolver: NameResolver
+ ): List<String>? {
+ return valueParameterList.map {
+ if (it.hasName()) {
+ nameResolver.getName(it.name)
+ .asString()
+ .replace("`", "")
+ .removeSuffix("?")
+ .trim()
+ } else {
+ // early return bad parameter
+ return null
+ }
+ }
+ }
+
+ private data class FailedConstructor(
+ val method: ExecutableElement,
+ val params: List<String>,
+ val matches: List<Constructor.Param?>
+ ) {
+ fun log(): String {
+ val logPerParam = params.withIndex().joinToString(", ") {
+ "param:${it.value} -> matched field:" + (matches[it.index]?.log() ?: "unmatched")
+ }
+ return "$method -> [$logPerParam]"
+ }
+ }
}
diff --git a/room/integration-tests/kotlintestapp/build.gradle b/room/integration-tests/kotlintestapp/build.gradle
index 64db19c..421e192 100644
--- a/room/integration-tests/kotlintestapp/build.gradle
+++ b/room/integration-tests/kotlintestapp/build.gradle
@@ -43,7 +43,6 @@
implementation(project(":arch:runtime"))
implementation(SUPPORT_APPCOMPAT, libs.support_exclude_config)
- kapt project(":room:compiler")
kaptAndroidTest project(":room:compiler")
androidTestImplementation(TEST_RUNNER) {
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json b/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
index 6362ae8..014d98c 100644
--- a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
+++ b/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
- "identityHash": "0729a81dd89abcbdd49f45ab0807043d",
+ "identityHash": "f78c9a055453bd6db377a3fcc3007e54",
"entities": [
{
"tableName": "Book",
@@ -199,11 +199,37 @@
},
"indices": [],
"foreignKeys": []
+ },
+ {
+ "tableName": "DataClassFromDependency",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"0729a81dd89abcbdd49f45ab0807043d\")"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"f78c9a055453bd6db377a3fcc3007e54\")"
]
}
}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/TestDatabase.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/TestDatabase.kt
index 74d8853..33162ee 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/TestDatabase.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/TestDatabase.kt
@@ -19,19 +19,22 @@
import android.arch.persistence.room.Database
import android.arch.persistence.room.RoomDatabase
import android.arch.persistence.room.integration.kotlintestapp.dao.BooksDao
+import android.arch.persistence.room.integration.kotlintestapp.dao.DependencyDao
import android.arch.persistence.room.integration.kotlintestapp.dao.DerivedDao
import android.arch.persistence.room.integration.kotlintestapp.vo.Author
import android.arch.persistence.room.integration.kotlintestapp.vo.Book
import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
+import android.arch.persistence.room.integration.kotlintestapp.vo.DataClassFromDependency
import android.arch.persistence.room.integration.kotlintestapp.vo.NoArgClass
import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
-@Database(entities = arrayOf(Book::class, Author::class, Publisher::class, BookAuthor::class,
- NoArgClass::class),
- version = 1)
+@Database(entities = [Book::class, Author::class, Publisher::class, BookAuthor::class,
+ NoArgClass::class, DataClassFromDependency::class], version = 1)
abstract class TestDatabase : RoomDatabase() {
abstract fun booksDao(): BooksDao
abstract fun derivedDao(): DerivedDao
+
+ abstract fun dependencyDao(): DependencyDao
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/DependencyDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/DependencyDaoTest.kt
new file mode 100644
index 0000000..2008fca
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/DependencyDaoTest.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package android.arch.persistence.room.integration.kotlintestapp.test
+
+import android.arch.persistence.room.integration.kotlintestapp.vo.DataClassFromDependency
+import android.support.test.runner.AndroidJUnit4
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DependencyDaoTest : TestDatabaseTest() {
+ @Test
+ fun insertAndGet() {
+ val dao = database.dependencyDao()
+ val data = DataClassFromDependency(
+ id = 3,
+ name = "foo"
+ )
+ dao.insert(data)
+ assertThat(dao.selectAll(), `is`(listOf(data)))
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/dao/DependencyDao.kt b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/dao/DependencyDao.kt
new file mode 100644
index 0000000..efe3c5b
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/dao/DependencyDao.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+package android.arch.persistence.room.integration.kotlintestapp.dao
+
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Insert
+import android.arch.persistence.room.OnConflictStrategy
+import android.arch.persistence.room.Query
+import android.arch.persistence.room.integration.kotlintestapp.vo.DataClassFromDependency
+
+@Dao
+interface DependencyDao {
+ @Query("select * from DataClassFromDependency")
+ fun selectAll(): List<DataClassFromDependency>
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg input: DataClassFromDependency)
+}
diff --git a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/DataClassFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/DataClassFromDependency.kt
new file mode 100644
index 0000000..258ada6
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/DataClassFromDependency.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+package android.arch.persistence.room.integration.kotlintestapp.vo
+
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
+
+/**
+ * used to test the case where kotlin classes from dependencies cannot be read properly.
+ * Since the main db in this app is in the test module, the original classes serve as a dependency.
+ */
+@Entity
+data class DataClassFromDependency(
+ @PrimaryKey(autoGenerate = true)
+ val id: Int,
+ val name: String)
\ No newline at end of file
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
index 49818e6..85af90f 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
@@ -85,6 +85,7 @@
public Slice onBindSlice(Uri sliceUri) {
String path = sliceUri.getPath();
switch (path) {
+ // TODO: add list / grid slices with 'see more' options
case "/message":
return createMessagingSlice(sliceUri);
case "/wifi":
diff --git a/slices/builders/api/current.txt b/slices/builders/api/current.txt
index 6b7f915..8a953cb 100644
--- a/slices/builders/api/current.txt
+++ b/slices/builders/api/current.txt
@@ -5,6 +5,9 @@
ctor public GridBuilder(androidx.app.slice.builders.ListBuilder);
method public androidx.app.slice.builders.GridBuilder addCell(androidx.app.slice.builders.GridBuilder.CellBuilder);
method public androidx.app.slice.builders.GridBuilder addCell(java.util.function.Consumer<androidx.app.slice.builders.GridBuilder.CellBuilder>);
+ method public androidx.app.slice.builders.GridBuilder addSeeMoreAction(android.app.PendingIntent);
+ method public androidx.app.slice.builders.GridBuilder addSeeMoreCell(androidx.app.slice.builders.GridBuilder.CellBuilder);
+ method public androidx.app.slice.builders.GridBuilder addSeeMoreCell(java.util.function.Consumer<androidx.app.slice.builders.GridBuilder.CellBuilder>);
}
public static final class GridBuilder.CellBuilder extends androidx.app.slice.builders.TemplateSliceBuilder {
@@ -31,6 +34,9 @@
method public androidx.app.slice.builders.ListBuilder addRange(java.util.function.Consumer<androidx.app.slice.builders.ListBuilder.RangeBuilder>);
method public androidx.app.slice.builders.ListBuilder addRow(androidx.app.slice.builders.ListBuilder.RowBuilder);
method public androidx.app.slice.builders.ListBuilder addRow(java.util.function.Consumer<androidx.app.slice.builders.ListBuilder.RowBuilder>);
+ method public androidx.app.slice.builders.ListBuilder addSeeMoreAction(android.app.PendingIntent);
+ method public androidx.app.slice.builders.ListBuilder addSeeMoreRow(androidx.app.slice.builders.ListBuilder.RowBuilder);
+ method public androidx.app.slice.builders.ListBuilder addSeeMoreRow(java.util.function.Consumer<androidx.app.slice.builders.ListBuilder.RowBuilder>);
method public androidx.app.slice.builders.ListBuilder setActions(androidx.app.slice.builders.ListBuilder.ActionBuilder);
method public androidx.app.slice.builders.ListBuilder setActions(java.util.function.Consumer<androidx.app.slice.builders.ListBuilder.ActionBuilder>);
method public androidx.app.slice.builders.ListBuilder setHeader(androidx.app.slice.builders.ListBuilder.HeaderBuilder);
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/GridBuilder.java b/slices/builders/src/main/java/androidx/app/slice/builders/GridBuilder.java
index 6f967cb..0dff405 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/GridBuilder.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/GridBuilder.java
@@ -36,6 +36,7 @@
import androidx.app.slice.builders.impl.GridBuilderListV1Impl;
import androidx.app.slice.builders.impl.TemplateBuilderImpl;
+
/**
* Builder to construct a row of slice content in a grid format.
* <p>
@@ -46,6 +47,7 @@
public class GridBuilder extends TemplateSliceBuilder {
private androidx.app.slice.builders.impl.GridBuilder mImpl;
+ private boolean mHasSeeMore;
/**
* Create a builder which will construct a slice displayed in a grid format.
@@ -109,6 +111,76 @@
}
/**
+ * If all content in a slice cannot be shown, the cell added here may be displayed where the
+ * content is cut off.
+ * <p>
+ * This method should only be used if you want to display a custom cell to indicate more
+ * content, consider using {@link #addSeeMoreAction(PendingIntent)} otherwise. If you do
+ * choose to specify a custom cell, the cell should have
+ * {@link CellBuilder#setContentIntent(PendingIntent)} specified to take the user to an
+ * activity to see all of the content.
+ * </p>
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @NonNull
+ public GridBuilder addSeeMoreCell(@NonNull CellBuilder builder) {
+ if (mHasSeeMore) {
+ throw new IllegalStateException("Trying to add see more cell when one has "
+ + "already been added");
+ }
+ mImpl.addSeeMoreCell((TemplateBuilderImpl) builder.mImpl);
+ mHasSeeMore = true;
+ return this;
+ }
+
+ /**
+ * If all content in a slice cannot be shown, the cell added here may be displayed where the
+ * content is cut off.
+ * <p>
+ * This method should only be used if you want to display a custom cell to indicate more
+ * content, consider using {@link #addSeeMoreAction(PendingIntent)} otherwise. If you do
+ * choose to specify a custom cell, the cell should have
+ * {@link CellBuilder#setContentIntent(PendingIntent)} specified to take the user to an
+ * activity to see all of the content.
+ * </p>
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @RequiresApi(Build.VERSION_CODES.N)
+ @NonNull
+ public GridBuilder addSeeMoreCell(@NonNull Consumer<CellBuilder> c) {
+ CellBuilder b = new CellBuilder(this);
+ c.accept(b);
+ return addSeeMoreCell(b);
+ }
+
+ /**
+ * If all content in a slice cannot be shown, a "see more" affordance may be displayed where
+ * the content is cut off. The action added here should take the user to an activity to see
+ * all of the content, and will be invoked when the "see more" affordance is tapped.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @NonNull
+ public GridBuilder addSeeMoreAction(@NonNull PendingIntent intent) {
+ if (mHasSeeMore) {
+ throw new IllegalStateException("Trying to add see more action when one has "
+ + "already been added");
+ }
+ mImpl.addSeeMoreAction(intent);
+ mHasSeeMore = true;
+ return this;
+ }
+
+
+ /**
* @hide
*/
@RestrictTo(LIBRARY)
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/ListBuilder.java b/slices/builders/src/main/java/androidx/app/slice/builders/ListBuilder.java
index d27a41a..dbd3a98 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/ListBuilder.java
@@ -57,6 +57,7 @@
*/
public class ListBuilder extends TemplateSliceBuilder {
+ private boolean mHasSeeMore;
private androidx.app.slice.builders.impl.ListBuilder mImpl;
/**
@@ -180,6 +181,74 @@
}
/**
+ * If all content in a slice cannot be shown, the row added here may be displayed where the
+ * content is cut off. This row should have an affordance to take the user to an activity to
+ * see all of the content.
+ * <p>
+ * This method should only be used if you want to display a custom row to indicate more
+ * content, consider using {@link #addSeeMoreAction(PendingIntent)} otherwise. If you do
+ * choose to specify a custom row, the row should have a content intent or action end item
+ * specified to take the user to an activity to see all of the content.
+ * </p>
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @NonNull
+ public ListBuilder addSeeMoreRow(@NonNull RowBuilder builder) {
+ if (mHasSeeMore) {
+ throw new IllegalArgumentException("Trying to add see more row when one has "
+ + "already been added");
+ }
+ mImpl.addSeeMoreRow((TemplateBuilderImpl) builder.mImpl);
+ mHasSeeMore = true;
+ return this;
+ }
+
+ /**
+ * If all content in a slice cannot be shown, the row added here may be displayed where the
+ * content is cut off. This row should have an affordance to take the user to an activity to
+ * see all of the content.
+ * <p>
+ * This method should only be used if you want to display a custom row to indicate more
+ * content, consider using {@link #addSeeMoreAction(PendingIntent)} otherwise. If you do
+ * choose to specify a custom row, the row should have a content intent or action end item
+ * specified to take the user to an activity to see all of the content.
+ * </p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @NonNull
+ public ListBuilder addSeeMoreRow(@NonNull Consumer<RowBuilder> c) {
+ RowBuilder b = new RowBuilder(this);
+ c.accept(b);
+ return addSeeMoreRow(b);
+ }
+
+ /**
+ * If all content in a slice cannot be shown, a "see more" affordance may be displayed where
+ * the content is cut off. The action added here should take the user to an activity to see
+ * all of the content, and will be invoked when the "see more" affordance is tapped.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @NonNull
+ public ListBuilder addSeeMoreAction(@NonNull PendingIntent intent) {
+ if (mHasSeeMore) {
+ throw new IllegalArgumentException("Trying to add see more action when one has "
+ + "already been added");
+ }
+ mImpl.addSeeMoreAction(intent);
+ mHasSeeMore = true;
+ return this;
+ }
+
+ /**
* @hide
*/
@RestrictTo(LIBRARY)
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilder.java b/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilder.java
index 91266a4..302e1b3 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilder.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilder.java
@@ -43,11 +43,33 @@
TemplateBuilderImpl createGridBuilder(Uri uri);
/**
- * Add a cell to this builder. Expected to be a builder from {@link #createGridBuilder};
+ * Add a cell to this builder. Expected to be a builder from {@link #createGridBuilder}.
*/
void addCell(TemplateBuilderImpl impl);
/**
+ * If all content in a slice cannot be shown, the cell added here will be displayed where the
+ * content is cut off. This cell should have an affordance to take the user to an activity to
+ * see all of the content. Expected to be a builder from {@link #createGridBuilder}.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ void addSeeMoreCell(TemplateBuilderImpl impl);
+
+ /**
+ * If all content in a slice cannot be shown, a "see more" affordance will be displayed where
+ * the content is cut off. The action added here should take the user to an activity to see
+ * all of the content, and will be invoked when the "see more" affordance is tapped.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ void addSeeMoreAction(PendingIntent intent);
+
+ /**
* Builds a standalone slice of this grid builder (i.e. not contained within a List).
*/
Slice buildIndividual();
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderBasicImpl.java b/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderBasicImpl.java
index 63f7506..67bbfa1 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderBasicImpl.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderBasicImpl.java
@@ -65,6 +65,18 @@
/**
*/
@Override
+ public void addSeeMoreCell(TemplateBuilderImpl impl) {
+ }
+
+ /**
+ */
+ @Override
+ public void addSeeMoreAction(PendingIntent intent) {
+ }
+
+ /**
+ */
+ @Override
public Slice buildIndividual() {
// Empty slice, nothing useful from a grid to basic.
return getBuilder().build();
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderListV1Impl.java b/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderListV1Impl.java
index 140bd9b..fe755bd 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderListV1Impl.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/impl/GridBuilderListV1Impl.java
@@ -20,6 +20,7 @@
import static android.app.slice.Slice.HINT_LARGE;
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_PARTIAL;
+import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import android.app.PendingIntent;
@@ -72,6 +73,26 @@
getBuilder().addSubSlice(builder.build());
}
+
+ /**
+ */
+ @Override
+ public void addSeeMoreCell(@NonNull TemplateBuilderImpl builder) {
+ builder.getBuilder().addHints(HINT_SEE_MORE);
+ getBuilder().addSubSlice(builder.build());
+ }
+
+ /**
+ */
+ @Override
+ public void addSeeMoreAction(PendingIntent intent) {
+ getBuilder().addSubSlice(
+ new Slice.Builder(getBuilder())
+ .addHints(HINT_SEE_MORE)
+ .addAction(intent, new Slice.Builder(getBuilder()).build(), null)
+ .build());
+ }
+
/**
*/
@Override
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilder.java b/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilder.java
index b3fa934..d92083e 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilder.java
@@ -64,6 +64,27 @@
void addRange(TemplateBuilderImpl builder);
/**
+ * If all content in a slice cannot be shown, the row added here will be displayed where the
+ * content is cut off. This row should have an affordance to take the user to an activity to
+ * see all of the content.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ void addSeeMoreRow(TemplateBuilderImpl builder);
+ /**
+ * If all content in a slice cannot be shown, a "see more" affordance will be displayed where
+ * the content is cut off. The action added here should take the user to an activity to see
+ * all of the content, and will be invoked when the "see more" affordance is tapped.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ void addSeeMoreAction(PendingIntent intent);
+
+ /**
* Sets the color to tint items displayed by this template (e.g. icons).
*/
void setColor(int color);
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderBasicImpl.java b/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderBasicImpl.java
index 689f0fc..8657c90 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderBasicImpl.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderBasicImpl.java
@@ -80,8 +80,19 @@
/**
*/
@Override
- public void setColor(int color) {
+ public void addSeeMoreRow(TemplateBuilderImpl builder) {
+ }
+ /**
+ */
+ @Override
+ public void addSeeMoreAction(PendingIntent intent) {
+ }
+
+ /**
+ */
+ @Override
+ public void setColor(int color) {
}
/**
diff --git a/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderV1Impl.java b/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderV1Impl.java
index 1e8b4e8..4ec4ac0 100644
--- a/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderV1Impl.java
+++ b/slices/builders/src/main/java/androidx/app/slice/builders/impl/ListBuilderV1Impl.java
@@ -21,6 +21,7 @@
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_PARTIAL;
+import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.app.slice.Slice.HINT_SELECTED;
import static android.app.slice.Slice.HINT_SUMMARY;
import static android.app.slice.Slice.HINT_TITLE;
@@ -119,6 +120,26 @@
}
/**
+ */
+ @Override
+ public void addSeeMoreRow(TemplateBuilderImpl builder) {
+ builder.getBuilder().addHints(HINT_SEE_MORE);
+ getBuilder().addSubSlice(builder.build());
+ }
+
+ /**
+ */
+ @Override
+ public void addSeeMoreAction(PendingIntent intent) {
+ getBuilder().addSubSlice(
+ new Slice.Builder(getBuilder())
+ .addHints(HINT_SEE_MORE)
+ .addAction(intent, new Slice.Builder(getBuilder()).build(), null)
+ .build());
+ }
+
+
+ /**
* Builder to construct an input row.
*/
public static class RangeBuilderImpl extends TemplateBuilderImpl implements RangeBuilder {
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 b528dfb..779460e 100644
--- a/slices/core/src/main/java/androidx/app/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/app/slice/Slice.java
@@ -23,6 +23,7 @@
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_PARTIAL;
+import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.app.slice.Slice.HINT_SELECTED;
import static android.app.slice.Slice.HINT_SUMMARY;
import static android.app.slice.Slice.HINT_TITLE;
@@ -33,7 +34,6 @@
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
-
import static androidx.app.slice.SliceConvert.unwrap;
import android.annotation.TargetApi;
@@ -80,7 +80,7 @@
*/
@RestrictTo(Scope.LIBRARY)
@StringDef({HINT_TITLE, HINT_LIST, HINT_LIST_ITEM, HINT_LARGE, HINT_ACTIONS, HINT_SELECTED,
- HINT_HORIZONTAL, HINT_NO_TINT, HINT_PARTIAL, HINT_SUMMARY})
+ HINT_HORIZONTAL, HINT_NO_TINT, HINT_PARTIAL, HINT_SUMMARY, HINT_SEE_MORE})
public @interface SliceHint{ }
private final SliceItem[] mItems;
diff --git a/v7/appcompat/src/main/java/android/support/v7/view/menu/MenuPopupHelper.java b/v7/appcompat/src/main/java/android/support/v7/view/menu/MenuPopupHelper.java
index 35721b8..a6ee759 100644
--- a/v7/appcompat/src/main/java/android/support/v7/view/menu/MenuPopupHelper.java
+++ b/v7/appcompat/src/main/java/android/support/v7/view/menu/MenuPopupHelper.java
@@ -269,7 +269,7 @@
final int hgrav = GravityCompat.getAbsoluteGravity(mDropDownGravity,
ViewCompat.getLayoutDirection(mAnchorView)) & Gravity.HORIZONTAL_GRAVITY_MASK;
if (hgrav == Gravity.RIGHT) {
- xOffset += mAnchorView.getWidth();
+ xOffset -= mAnchorView.getWidth();
}
popup.setHorizontalOffset(xOffset);
diff --git a/v7/appcompat/src/main/java/android/support/v7/view/menu/StandardMenuPopup.java b/v7/appcompat/src/main/java/android/support/v7/view/menu/StandardMenuPopup.java
index d94ff72..7026959 100644
--- a/v7/appcompat/src/main/java/android/support/v7/view/menu/StandardMenuPopup.java
+++ b/v7/appcompat/src/main/java/android/support/v7/view/menu/StandardMenuPopup.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.content.res.Resources;
import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
import android.support.v7.appcompat.R;
import android.support.v7.widget.MenuPopupWindow;
import android.view.Gravity;
@@ -260,7 +261,6 @@
mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes);
subPopup.setPresenterCallback(mPresenterCallback);
subPopup.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(subMenu));
- subPopup.setGravity(mDropDownGravity);
// Pass responsibility for handling onDismiss to the submenu.
subPopup.setOnDismissListener(mOnDismissListener);
@@ -270,8 +270,17 @@
mMenu.close(false /* closeAllMenus */);
// Show the new sub-menu popup at the same location as this popup.
- final int horizontalOffset = mPopup.getHorizontalOffset();
+ int horizontalOffset = mPopup.getHorizontalOffset();
final int verticalOffset = mPopup.getVerticalOffset();
+
+ // As xOffset of parent menu popup is subtracted with Anchor width for Gravity.RIGHT,
+ // So, again to display sub-menu popup in same xOffset, add the Anchor width.
+ final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
+ ViewCompat.getLayoutDirection(mAnchorView)) & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if (hgrav == Gravity.RIGHT) {
+ horizontalOffset += mAnchorView.getWidth();
+ }
+
if (subPopup.tryShow(horizontalOffset, verticalOffset)) {
if (mPresenterCallback != null) {
mPresenterCallback.onOpenSubMenu(subMenu);
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeSupportResumeTest.java b/wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportResumeTest.java
similarity index 100%
rename from wear/tests/src/android/support/wear/ambient/AmbientModeSupportResumeTest.java
rename to wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportResumeTest.java
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeSupportResumeTestActivity.java b/wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportResumeTestActivity.java
similarity index 100%
rename from wear/tests/src/android/support/wear/ambient/AmbientModeSupportResumeTestActivity.java
rename to wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportResumeTestActivity.java
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeSupportTest.java b/wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportTest.java
similarity index 100%
rename from wear/tests/src/android/support/wear/ambient/AmbientModeSupportTest.java
rename to wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportTest.java
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeSupportTestActivity.java b/wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportTestActivity.java
similarity index 100%
rename from wear/tests/src/android/support/wear/ambient/AmbientModeSupportTestActivity.java
rename to wear/src/androidTest/java/android/support/wear/ambient/AmbientModeSupportTestActivity.java