diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-10-24 19:50:40 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-10-24 19:50:40 -0400 |
commit | 47ed54e5d312f899507d28d6e95ccc18a0de19fe (patch) | |
tree | 7a2d435c55c36fbc1d07e895bd0c68b18f84e12c /android/support | |
parent | 07f9f65561c2b81bcd189b895b31bb2ad0438d74 (diff) | |
download | android-28-47ed54e5d312f899507d28d6e95ccc18a0de19fe.tar.gz |
Import Android SDK Platform P [4413397]
/google/data/ro/projects/android/fetch_artifact \
--bid 4413397 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4413397.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I3cf1f7c36e61c090dcc7de7bcfa812ef2bf96c00
Diffstat (limited to 'android/support')
31 files changed, 1258 insertions, 473 deletions
diff --git a/android/support/LibraryVersions.java b/android/support/LibraryVersions.java index a046d95e..2f5730a2 100644 --- a/android/support/LibraryVersions.java +++ b/android/support/LibraryVersions.java @@ -28,7 +28,7 @@ public class LibraryVersions { /** * Version code for flatfoot 1.0 projects (room, lifecycles) */ - private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0-beta2"); + private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0-rc1"); /** * Version code for Room @@ -45,15 +45,17 @@ public class LibraryVersions { */ public static final Version PAGING = new Version("1.0.0-alpha3"); + private static final Version LIFECYCLES = new Version("1.0.3"); + /** * Version code for Lifecycle libs that are required by the support library */ - public static final Version LIFECYCLES_CORE = new Version("1.0.2"); + public static final Version LIFECYCLES_CORE = LIFECYCLES; /** * Version code for Lifecycle runtime libs that are required by the support library */ - public static final Version LIFECYCLES_RUNTIME = new Version("1.0.0"); + public static final Version LIFECYCLES_RUNTIME = LIFECYCLES; /** * Version code for shared code of flatfoot diff --git a/android/support/car/drawer/CarDrawerActivity.java b/android/support/car/drawer/CarDrawerActivity.java new file mode 100644 index 00000000..7100218a --- /dev/null +++ b/android/support/car/drawer/CarDrawerActivity.java @@ -0,0 +1,152 @@ +/* + * 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.car.drawer; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +/** + * Common base Activity for car apps that need to present a Drawer. + * + * <p>This Activity manages the overall layout. To use it, sub-classes need to: + * + * <ul> + * <li>Provide the root-items for the Drawer by implementing {@link #getRootAdapter()}. + * <li>Add their main content using {@link #setMainContent(int)} or {@link #setMainContent(View)}. + * They can also add fragments to the main-content container by obtaining its id using + * {@link #getContentContainerId()} + * </ul> + * + * <p>This class will take care of drawer toggling and display. + * + * <p>The rootAdapter can implement nested-navigation, in its click-handling, by passing the + * CarDrawerAdapter for the next level to + * {@link CarDrawerController#switchToAdapter(CarDrawerAdapter)}. + * + * <p>Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a + * derivative. + */ +public abstract class CarDrawerActivity extends AppCompatActivity { + private CarDrawerController mDrawerController; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.car_drawer_activity); + + DrawerLayout drawerLayout = findViewById(R.id.drawer_layout); + ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle( + this /* activity */, + drawerLayout, /* DrawerLayout object */ + R.string.car_drawer_open, + R.string.car_drawer_close); + + Toolbar toolbar = findViewById(R.id.car_toolbar); + setSupportActionBar(toolbar); + + mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle); + mDrawerController.setRootAdapter(getRootAdapter()); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + } + + /** + * Returns the {@link CarDrawerController} that is responsible for handling events relating + * to the drawer in this Activity. + * + * @return The {@link CarDrawerController} linked to this Activity. This value will be + * {@code null} if this method is called before {@code onCreate()} has been called. + */ + @Nullable + protected CarDrawerController getDrawerController() { + return mDrawerController; + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mDrawerController.syncState(); + } + + /** + * @return Adapter for root content of the Drawer. + */ + protected abstract CarDrawerAdapter getRootAdapter(); + + /** + * Set main content to display in this Activity. It will be added to R.id.content_frame in + * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}. + * + * @param view View to display as main content. + */ + public void setMainContent(View view) { + ViewGroup parent = findViewById(getContentContainerId()); + parent.addView(view); + } + + /** + * Set main content to display in this Activity. It will be added to R.id.content_frame in + * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}. + * + * @param resourceId Layout to display as main content. + */ + public void setMainContent(@LayoutRes int resourceId) { + ViewGroup parent = findViewById(getContentContainerId()); + LayoutInflater inflater = getLayoutInflater(); + inflater.inflate(resourceId, parent, true); + } + + /** + * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own + * content/fragments inside here. + * + * @return Id of FrameLayout where main content of the subclass Activity can be added. + */ + protected int getContentContainerId() { + return R.id.content_frame; + } + + @Override + protected void onStop() { + super.onStop(); + mDrawerController.closeDrawer(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDrawerController.onConfigurationChanged(newConfig); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return mDrawerController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); + } +} diff --git a/android/support/car/drawer/CarDrawerAdapter.java b/android/support/car/drawer/CarDrawerAdapter.java new file mode 100644 index 00000000..b0fd965d --- /dev/null +++ b/android/support/car/drawer/CarDrawerAdapter.java @@ -0,0 +1,182 @@ +/* + * 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.car.drawer; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.car.widget.PagedListView; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * Base adapter for displaying items in the car navigation drawer, which uses a + * {@link PagedListView}. + * + * <p>Subclasses must set the title that will be displayed when displaying the contents of the + * drawer via {@link #setTitle(CharSequence)}. The title can be updated at any point later on. The + * title of the root adapter will also be the main title showed in the toolbar when the drawer is + * closed. See {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)} for more information. + * + * <p>This class also takes care of implementing the PageListView.ItemCamp contract and subclasses + * should implement {@link #getActualItemCount()}. + */ +public abstract class CarDrawerAdapter extends RecyclerView.Adapter<DrawerItemViewHolder> + implements PagedListView.ItemCap, DrawerItemClickListener { + private final boolean mShowDisabledListOnEmpty; + private final Drawable mEmptyListDrawable; + private int mMaxItems = PagedListView.ItemCap.UNLIMITED; + private CharSequence mTitle; + private TitleChangeListener mTitleChangeListener; + + /** + * Interface for a class that will be notified a new title has been set on this adapter. + */ + interface TitleChangeListener { + /** + * Called when {@link #setTitle(CharSequence)} has been called and the title has been + * changed. + */ + void onTitleChanged(CharSequence newTitle); + } + + protected CarDrawerAdapter(Context context, boolean showDisabledListOnEmpty) { + mShowDisabledListOnEmpty = showDisabledListOnEmpty; + + mEmptyListDrawable = context.getDrawable(R.drawable.ic_list_view_disable); + mEmptyListDrawable.setColorFilter(context.getColor(R.color.car_tint), + PorterDuff.Mode.SRC_IN); + } + + /** Returns the title set via {@link #setTitle(CharSequence)}. */ + CharSequence getTitle() { + return mTitle; + } + + /** Updates the title to display in the toolbar for this Adapter. */ + public final void setTitle(@NonNull CharSequence title) { + if (title == null) { + throw new IllegalArgumentException("setTitle() cannot be passed a null title!"); + } + + mTitle = title; + + if (mTitleChangeListener != null) { + mTitleChangeListener.onTitleChanged(mTitle); + } + } + + /** Sets a listener to be notified whenever the title of this adapter has been changed. */ + void setTitleChangeListener(@Nullable TitleChangeListener listener) { + mTitleChangeListener = listener; + } + + @Override + public final void setMaxItems(int maxItems) { + mMaxItems = maxItems; + } + + @Override + public final int getItemCount() { + if (shouldShowDisabledListItem()) { + return 1; + } + return mMaxItems >= 0 ? Math.min(mMaxItems, getActualItemCount()) : getActualItemCount(); + } + + /** + * Returns the absolute number of items that can be displayed in the list. + * + * <p>A class should implement this method to supply the number of items to be displayed. + * Returning 0 from this method will cause an empty list icon to be displayed in the drawer. + * + * <p>A class should override this method rather than {@link #getItemCount()} because that + * method is handling the logic of when to display the empty list icon. It will return 1 when + * {@link #getActualItemCount()} returns 0. + * + * @return The number of items to be displayed in the list. + */ + protected abstract int getActualItemCount(); + + @Override + public final int getItemViewType(int position) { + if (shouldShowDisabledListItem()) { + return R.layout.car_drawer_list_item_empty; + } + + return usesSmallLayout(position) + ? R.layout.car_drawer_list_item_small + : R.layout.car_drawer_list_item_normal; + } + + /** + * Used to indicate the layout used for the Drawer item at given position. Subclasses can + * override this to use normal layout which includes text element below title. + * + * <p>A small layout is presented by the layout {@code R.layout.car_drawer_list_item_small}. + * Otherwise, the layout {@code R.layout.car_drawer_list_item_normal} will be used. + * + * @param position Adapter position of item. + * @return Whether the item at this position will use a small layout (default) or normal layout. + */ + protected boolean usesSmallLayout(int position) { + return true; + } + + @Override + public final DrawerItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + return new DrawerItemViewHolder(view); + } + + @Override + public final void onBindViewHolder(DrawerItemViewHolder holder, int position) { + if (shouldShowDisabledListItem()) { + holder.getTitle().setText(null); + holder.getIcon().setImageDrawable(mEmptyListDrawable); + holder.setItemClickListener(null); + } else { + holder.setItemClickListener(this); + populateViewHolder(holder, position); + } + } + + /** + * Whether or not this adapter should be displaying an empty list icon. The icon is shown if it + * has been configured to show and there are no items to be displayed. + */ + private boolean shouldShowDisabledListItem() { + return mShowDisabledListOnEmpty && getActualItemCount() == 0; + } + + /** + * Subclasses should set all elements in {@code holder} to populate the drawer-item. If some + * element is not used, it should be nulled out since these ViewHolder/View's are recycled. + */ + protected abstract void populateViewHolder(DrawerItemViewHolder holder, int position); + + /** + * Called when this adapter has been popped off the stack and is no longer needed. Subclasses + * can override to do any necessary cleanup. + */ + public void cleanup() {} +} diff --git a/android/support/car/drawer/CarDrawerController.java b/android/support/car/drawer/CarDrawerController.java new file mode 100644 index 00000000..4d9f4e99 --- /dev/null +++ b/android/support/car/drawer/CarDrawerController.java @@ -0,0 +1,306 @@ +/* + * 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.car.drawer; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.car.widget.PagedListView; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.widget.Toolbar; +import android.view.Gravity; +import android.view.MenuItem; +import android.view.View; +import android.widget.ProgressBar; + +import java.util.Stack; + +/** + * A controller that will handle the set up of the navigation drawer. It will hook up the + * necessary buttons for up navigation, as well as expose methods to allow for a drill down + * navigation. + */ +public class CarDrawerController { + /** The amount that the drawer has been opened before its color should be switched. */ + private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f; + + /** + * A representation of the hierarchy of navigation being displayed in the list. The ordering of + * this stack is the order that the user has visited each level. When the user navigates up, + * the adapters are poopped from this list. + */ + private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>(); + + private final Context mContext; + + private final Toolbar mToolbar; + private final DrawerLayout mDrawerLayout; + private final ActionBarDrawerToggle mDrawerToggle; + + private final PagedListView mDrawerList; + private final ProgressBar mProgressBar; + private final View mDrawerContent; + + /** + * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by + * {@code drawerLayout}. + * + * <p>The given {@code drawerLayout} should either have a child View that is inflated from + * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that + * layout. + * + * @param toolbar The {@link Toolbar} that will serve as the action bar for an Activity. + * @param drawerLayout The top-level container for the window content that shows the + * interactive drawer. + * @param drawerToggle The {@link ActionBarDrawerToggle} that bridges the given {@code toolbar} + * and {@code drawerLayout}. + */ + public CarDrawerController(Toolbar toolbar, + DrawerLayout drawerLayout, + ActionBarDrawerToggle drawerToggle) { + mToolbar = toolbar; + mContext = drawerLayout.getContext(); + + mDrawerLayout = drawerLayout; + + mDrawerContent = drawerLayout.findViewById(R.id.drawer_content); + mDrawerList = drawerLayout.findViewById(R.id.drawer_list); + mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED); + + mProgressBar = drawerLayout.findViewById(R.id.drawer_progress); + + mDrawerToggle = drawerToggle; + setupDrawerToggling(); + } + + /** + * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of + * this root adapter are shown when the drawer is first opened. It is also the top-most level of + * navigation in the drawer. + * + * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then + * this method will do nothing. + */ + public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) { + if (rootAdapter == null) { + return; + } + + mAdapterStack.push(rootAdapter); + setToolbarTitleFrom(rootAdapter); + mDrawerList.setAdapter(rootAdapter); + } + + /** + * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display + * in the navigation drawer. The title will also be updated from the adapter. + * + * <p>This switch is treated as a navigation to the next level in the drawer. Navigation away + * from this level will pop the given adapter off and surface contents of the previous adapter + * that was set via this method. If no such adapter exists, then the root adapter set by + * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead. + * + * @param adapter Adapter for next level of content in the drawer. + */ + public final void switchToAdapter(CarDrawerAdapter adapter) { + mAdapterStack.peek().setTitleChangeListener(null); + mAdapterStack.push(adapter); + switchToAdapterInternal(adapter); + } + + /** Close the drawer. */ + public void closeDrawer() { + if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { + mDrawerLayout.closeDrawer(Gravity.LEFT); + } + } + + /** Opens the drawer. */ + public void openDrawer() { + if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { + mDrawerLayout.openDrawer(Gravity.LEFT); + } + } + + /** Sets a listener to be notified of Drawer events. */ + public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { + mDrawerLayout.addDrawerListener(listener); + } + + /** Removes a listener to be notified of Drawer events. */ + public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { + mDrawerLayout.removeDrawerListener(listener); + } + + /** + * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true}, + * the progress bar is displayed and the navigation list is hidden and vice versa. + */ + public void showLoadingProgressBar(boolean show) { + mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE); + mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE); + } + + /** Scroll to given position in the list. */ + public void scrollToPosition(int position) { + mDrawerList.getRecyclerView().smoothScrollToPosition(position); + } + + /** + * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this + * controller's internal Toolbar. + */ + private void setToolbarTitleFrom(CarDrawerAdapter adapter) { + if (adapter.getTitle() == null) { + throw new RuntimeException("CarDrawerAdapter must supply a title via setTitle()"); + } + + mToolbar.setTitle(adapter.getTitle()); + adapter.setTitleChangeListener(mToolbar::setTitle); + } + + /** + * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer + * hierarchy is properly displayed. + */ + private void setupDrawerToggling() { + mDrawerLayout.addDrawerListener(mDrawerToggle); + mDrawerLayout.addDrawerListener( + new DrawerLayout.DrawerListener() { + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + // Correctly set the title and arrow colors as they are different between + // the open and close states. + updateTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET); + } + + @Override + public void onDrawerClosed(View drawerView) { + // If drawer is closed, revert stack/drawer to initial root state. + cleanupStackAndShowRoot(); + scrollToPosition(0); + } + + @Override + public void onDrawerOpened(View drawerView) {} + + @Override + public void onDrawerStateChanged(int newState) {} + }); + } + + /** Sets the title and arrow color of the drawer depending on if it is open or not. */ + private void updateTitleAndArrowColor(boolean drawerOpen) { + // When the drawer is open, use car_title, which resolves to appropriate color depending on + // day-night mode. When drawer is closed, we always use light color. + int titleColorResId = drawerOpen ? R.color.car_title : R.color.car_title_light; + int titleColor = mContext.getColor(titleColorResId); + mToolbar.setTitleTextColor(titleColor); + mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor); + } + + /** + * Synchronizes the display of the drawer with its linked {@link DrawerLayout}. + * + * <p>This should be called from the associated Activity's + * {@link android.support.v7.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize + * after teh DRawerLayout's instance state has been restored, and any other time when the + * state may have diverged in such a way that this controller's associated + * {@link ActionBarDrawerToggle} had not been notified. + */ + public void syncState() { + mDrawerToggle.syncState(); + + // In case we're restarting after a config change (e.g. day, night switch), set colors + // again. Doing it here so that Drawer state is fully synced and we know if its open or not. + // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout. + updateTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent)); + } + + /** + * Notify this controller that device configurations may have changed. + * + * <p>This method should be called from the associated Activity's + * {@code onConfigurationChanged()} method. + */ + public void onConfigurationChanged(Configuration newConfig) { + // Pass any configuration change to the drawer toggle. + mDrawerToggle.onConfigurationChanged(newConfig); + } + + /** + * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called + * when the Activity's method is called and will return {@code true} if the selection has + * been handled. + * + * @return {@code true} if the item processing was handled by this class. + */ + public boolean onOptionsItemSelected(MenuItem item) { + // Handle home-click and see if we can navigate up in the drawer. + if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) { + return true; + } + + // DrawerToggle gets next chance to handle up-clicks (and any other clicks). + return mDrawerToggle.onOptionsItemSelected(item); + } + + /** + * Sets the navigation drawer's title to be the one supplied by the given adapter and updates + * the navigation drawer list with the adapter's contents. + */ + private void switchToAdapterInternal(CarDrawerAdapter adapter) { + setToolbarTitleFrom(adapter); + // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between + // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts. + mDrawerList.getRecyclerView().setAdapter(adapter); + scrollToPosition(0); + } + + /** + * Switches to the previous level in the drawer hierarchy if the current list being displayed + * is not the root adapter. This is analogous to a navigate up. + * + * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise. + */ + private boolean maybeHandleUpClick() { + // Check if already at the root level. + if (mAdapterStack.size() <= 1) { + return false; + } + + CarDrawerAdapter adapter = mAdapterStack.pop(); + adapter.setTitleChangeListener(null); + adapter.cleanup(); + switchToAdapterInternal(mAdapterStack.peek()); + return true; + } + + /** Clears stack down to root adapter and switches to root adapter. */ + private void cleanupStackAndShowRoot() { + while (mAdapterStack.size() > 1) { + CarDrawerAdapter adapter = mAdapterStack.pop(); + adapter.setTitleChangeListener(null); + adapter.cleanup(); + } + switchToAdapterInternal(mAdapterStack.peek()); + } +} diff --git a/android/support/car/drawer/DrawerItemClickListener.java b/android/support/car/drawer/DrawerItemClickListener.java new file mode 100644 index 00000000..d707dbd0 --- /dev/null +++ b/android/support/car/drawer/DrawerItemClickListener.java @@ -0,0 +1,29 @@ +/* + * 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.car.drawer; + +/** + * Listener for handling clicks on items/views managed by {@link DrawerItemViewHolder}. + */ +public interface DrawerItemClickListener { + /** + * Callback when item is clicked. + * + * @param position Adapter position of the clicked item. + */ + void onItemClick(int position); +} diff --git a/android/support/car/drawer/DrawerItemViewHolder.java b/android/support/car/drawer/DrawerItemViewHolder.java new file mode 100644 index 00000000..d016b2de --- /dev/null +++ b/android/support/car/drawer/DrawerItemViewHolder.java @@ -0,0 +1,87 @@ +/* + * 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.car.drawer; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * Re-usable {@link RecyclerView.ViewHolder} for displaying items in the + * {@link android.support.car.drawer.CarDrawerAdapter}. + */ +public class DrawerItemViewHolder extends RecyclerView.ViewHolder { + private final ImageView mIcon; + private final TextView mTitle; + private final TextView mText; + private final ImageView mEndIcon; + + DrawerItemViewHolder(View view) { + super(view); + mIcon = view.findViewById(R.id.icon); + if (mIcon == null) { + throw new IllegalArgumentException("Icon view cannot be null!"); + } + + mTitle = view.findViewById(R.id.title); + if (mTitle == null) { + throw new IllegalArgumentException("Title view cannot be null!"); + } + + // Next two are optional and may be null. + mText = view.findViewById(R.id.text); + mEndIcon = view.findViewById(R.id.end_icon); + } + + /** Returns the view that should be used to display the main icon. */ + @NonNull + public ImageView getIcon() { + return mIcon; + } + + /** Returns the view that will display the main title. */ + @NonNull + public TextView getTitle() { + return mTitle; + } + + /** Returns the view that is used for text that is smaller than the title text. */ + @Nullable + public TextView getText() { + return mText; + } + + /** Returns the icon that is displayed at the end of the view. */ + @Nullable + public ImageView getEndIcon() { + return mEndIcon; + } + + /** + * Sets the listener that will be notified when the view held by this ViewHolder has been + * clicked. Passing {@code null} will clear any previously set listeners. + */ + void setItemClickListener(@Nullable DrawerItemClickListener listener) { + itemView.setOnClickListener(listener != null + ? v -> listener.onItemClick(getAdapterPosition()) + : null); + } +} diff --git a/android/support/car/widget/PagedListView.java b/android/support/car/widget/PagedListView.java index 8527c659..46527001 100644 --- a/android/support/car/widget/PagedListView.java +++ b/android/support/car/widget/PagedListView.java @@ -27,7 +27,6 @@ import android.os.Handler; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.Px; import android.support.annotation.RestrictTo; import android.support.annotation.UiThread; import android.support.car.R; @@ -61,7 +60,6 @@ public class PagedListView extends FrameLayout { protected final CarLayoutManager mLayoutManager; protected final Handler mHandler = new Handler(); private final boolean mScrollBarEnabled; - private final boolean mRightGutterEnabled; private final PagedScrollBarView mScrollBarView; private int mRowsPerPage = -1; @@ -98,6 +96,11 @@ public class PagedListView extends FrameLayout { */ public interface ItemCap { /** + * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit. + */ + int UNLIMITED = -1; + + /** * Sets the maximum number of items available in the adapter. A value less than '0' means * the list should not be capped. */ @@ -139,7 +142,6 @@ public class PagedListView extends FrameLayout { } LayoutInflater.from(context).inflate(layoutId, this /*root*/, true /*attachToRoot*/); - FrameLayout maxWidthLayout = (FrameLayout) findViewById(R.id.recycler_view_container); TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.PagedListView, defStyleAttrs, defStyleRes); mRecyclerView = (CarRecyclerView) findViewById(R.id.recycler_view); @@ -156,6 +158,16 @@ public class PagedListView extends FrameLayout { mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12); mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager)); + boolean offsetScrollBar = a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false); + if (offsetScrollBar) { + MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); + params.setMarginStart(getResources().getDimensionPixelSize( + R.dimen.car_screen_margin_size)); + params.setMarginEnd( + a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0)); + mRecyclerView.setLayoutParams(params); + } + if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) { int dividerStartMargin = a.getDimensionPixelSize( R.styleable.PagedListView_dividerStartMargin, 0); @@ -199,47 +211,20 @@ public class PagedListView extends FrameLayout { } } }); + mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE); - // Modify the layout if the Gutter or the Scroll Bar are not visible. - mRightGutterEnabled = a.getBoolean(R.styleable.PagedListView_rightGutterEnabled, false); - if (mRightGutterEnabled || !mScrollBarEnabled) { - FrameLayout.LayoutParams maxWidthLayoutLayoutParams = - (FrameLayout.LayoutParams) maxWidthLayout.getLayoutParams(); - if (mRightGutterEnabled) { - maxWidthLayoutLayoutParams.rightMargin = - getResources().getDimensionPixelSize(R.dimen.car_card_margin); - } - if (!mScrollBarEnabled) { - maxWidthLayoutLayoutParams.setMarginStart(0); - } - maxWidthLayout.setLayoutParams(maxWidthLayoutLayoutParams); + // Modify the layout the Scroll Bar is not visible. + if (!mScrollBarEnabled) { + MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); + params.setMarginStart(0); + mRecyclerView.setLayoutParams(params); } setDayNightStyle(DayNightStyle.AUTO); a.recycle(); } - /** - * Sets the starting and ending padding for each view in the list. - * - * @param start The start padding. - * @param end The end padding. - */ - public void setListViewStartEndPadding(@Px int start, @Px int end) { - int carCardMargin = getResources().getDimensionPixelSize(R.dimen.car_card_margin); - int startGutter = mScrollBarEnabled ? carCardMargin : 0; - int startPadding = Math.max(start - startGutter, 0); - int endGutter = mRightGutterEnabled ? carCardMargin : 0; - int endPadding = Math.max(end - endGutter, 0); - mRecyclerView.setPaddingRelative(startPadding, mRecyclerView.getPaddingTop(), - endPadding, mRecyclerView.getPaddingBottom()); - - // Since we're setting padding we'll need to set the clip to padding to the same - // value as clip children to ensure that the cards fly off the screen. - mRecyclerView.setClipToPadding(mRecyclerView.getClipChildren()); - } - @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); diff --git a/android/support/media/tv/BasePreviewProgram.java b/android/support/media/tv/BasePreviewProgram.java index 1423d9d6..39c30140 100644 --- a/android/support/media/tv/BasePreviewProgram.java +++ b/android/support/media/tv/BasePreviewProgram.java @@ -23,14 +23,13 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.support.annotation.IntDef; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.PreviewProgramColumns; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.AspectRatio; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Availability; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.InteractionType; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Type; import android.support.media.tv.TvContractCompat.PreviewPrograms; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.util.Date; @@ -55,6 +54,89 @@ public abstract class BasePreviewProgram extends BaseProgram { private static final int IS_LIVE = 1; private static final int IS_BROWSABLE = 1; + /** @hide */ + @IntDef({ + TYPE_UNKNOWN, + PreviewProgramColumns.TYPE_MOVIE, + PreviewProgramColumns.TYPE_TV_SERIES, + PreviewProgramColumns.TYPE_TV_SEASON, + PreviewProgramColumns.TYPE_TV_EPISODE, + PreviewProgramColumns.TYPE_CLIP, + PreviewProgramColumns.TYPE_EVENT, + PreviewProgramColumns.TYPE_CHANNEL, + PreviewProgramColumns.TYPE_TRACK, + PreviewProgramColumns.TYPE_ALBUM, + PreviewProgramColumns.TYPE_ARTIST, + PreviewProgramColumns.TYPE_PLAYLIST, + PreviewProgramColumns.TYPE_STATION, + PreviewProgramColumns.TYPE_GAME + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface Type {} + + /** + * The unknown program type. + */ + private static final int TYPE_UNKNOWN = -1; + + /** @hide */ + @IntDef({ + ASPECT_RATIO_UNKNOWN, + PreviewProgramColumns.ASPECT_RATIO_16_9, + PreviewProgramColumns.ASPECT_RATIO_3_2, + PreviewProgramColumns.ASPECT_RATIO_4_3, + PreviewProgramColumns.ASPECT_RATIO_1_1, + PreviewProgramColumns.ASPECT_RATIO_2_3, + PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface AspectRatio {} + + /** + * The aspect ratio for unknown aspect ratios. + */ + private static final int ASPECT_RATIO_UNKNOWN = -1; + + /** @hide */ + @IntDef({ + AVAILABILITY_UNKNOWN, + PreviewProgramColumns.AVAILABILITY_AVAILABLE, + PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION, + PreviewProgramColumns.AVAILABILITY_PAID_CONTENT, + PreviewProgramColumns.AVAILABILITY_PURCHASED, + PreviewProgramColumns.AVAILABILITY_FREE + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface Availability {} + + /** + * The unknown availability. + */ + private static final int AVAILABILITY_UNKNOWN = -1; + + /** @hide */ + @IntDef({ + INTERACTION_TYPE_UNKNOWN, + PreviewProgramColumns.INTERACTION_TYPE_VIEWS, + PreviewProgramColumns.INTERACTION_TYPE_LISTENS, + PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS, + PreviewProgramColumns.INTERACTION_TYPE_FANS, + PreviewProgramColumns.INTERACTION_TYPE_LIKES, + PreviewProgramColumns.INTERACTION_TYPE_THUMBS, + PreviewProgramColumns.INTERACTION_TYPE_VIEWERS, + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface InteractionType {} + + /** + * The unknown interaction type. + */ + private static final int INTERACTION_TYPE_UNKNOWN = -1; + BasePreviewProgram(Builder builder) { super(builder); } @@ -127,7 +209,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @Type int getType() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_TYPE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? TYPE_UNKNOWN : i; } /** @@ -137,7 +219,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @AspectRatio int getPosterArtAspectRatio() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? ASPECT_RATIO_UNKNOWN : i; } /** @@ -147,7 +229,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @AspectRatio int getThumbnailAspectRatio() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? ASPECT_RATIO_UNKNOWN : i; } /** @@ -165,7 +247,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @Availability int getAvailability() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_AVAILABILITY); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? AVAILABILITY_UNKNOWN : i; } /** @@ -216,7 +298,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @InteractionType int getInteractionType() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_INTERACTION_TYPE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? INTERACTION_TYPE_UNKNOWN : i; } /** diff --git a/android/support/media/tv/BaseProgram.java b/android/support/media/tv/BaseProgram.java index e4ce9d1f..23b5cf9c 100644 --- a/android/support/media/tv/BaseProgram.java +++ b/android/support/media/tv/BaseProgram.java @@ -22,13 +22,16 @@ import android.database.Cursor; import android.media.tv.TvContentRating; import android.net.Uri; import android.os.Build; +import android.support.annotation.IntDef; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.BaseTvColumns; import android.support.media.tv.TvContractCompat.ProgramColumns; -import android.support.media.tv.TvContractCompat.ProgramColumns.ReviewRatingStyle; import android.support.media.tv.TvContractCompat.Programs; import android.support.media.tv.TvContractCompat.Programs.Genres.Genre; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Base class for derived classes that want to have common fields for programs defined in * {@link TvContractCompat}. @@ -46,6 +49,22 @@ public abstract class BaseProgram { private static final int IS_SEARCHABLE = 1; /** @hide */ + @IntDef({ + REVIEW_RATING_STYLE_UNKNOWN, + ProgramColumns.REVIEW_RATING_STYLE_STARS, + ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN, + ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE, + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + @interface ReviewRatingStyle {} + + /** + * The unknown review rating style. + */ + private static final int REVIEW_RATING_STYLE_UNKNOWN = -1; + + /** @hide */ @RestrictTo(LIBRARY_GROUP) protected ContentValues mValues; @@ -254,7 +273,7 @@ public abstract class BaseProgram { */ public @ReviewRatingStyle int getReviewRatingStyle() { Integer i = mValues.getAsInteger(Programs.COLUMN_REVIEW_RATING_STYLE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? REVIEW_RATING_STYLE_UNKNOWN : i; } /** diff --git a/android/support/media/tv/Program.java b/android/support/media/tv/Program.java index 4e3bd7ac..233f1bab 100644 --- a/android/support/media/tv/Program.java +++ b/android/support/media/tv/Program.java @@ -25,6 +25,7 @@ import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.Programs; +import android.support.media.tv.TvContractCompat.Programs.Genres.Genre; /** * A convenience class to access {@link TvContractCompat.Programs} entries in the system content @@ -282,7 +283,7 @@ public final class Program extends BaseProgram implements Comparable<Program> { * @return This Builder object to allow for chaining of calls to builder methods. * @see Programs#COLUMN_BROADCAST_GENRE */ - public Builder setBroadcastGenres(String[] genres) { + public Builder setBroadcastGenres(@Genre String[] genres) { mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres)); return this; } diff --git a/android/support/media/tv/TvContractCompat.java b/android/support/media/tv/TvContractCompat.java index 5a46e791..de4fd04f 100644 --- a/android/support/media/tv/TvContractCompat.java +++ b/android/support/media/tv/TvContractCompat.java @@ -30,7 +30,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.BaseColumns; -import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; @@ -606,16 +605,6 @@ public final class TvContractCompat { */ @RestrictTo(LIBRARY_GROUP) interface ProgramColumns { - /** @hide */ - @IntDef({ - REVIEW_RATING_STYLE_STARS, - REVIEW_RATING_STYLE_THUMBS_UP_DOWN, - REVIEW_RATING_STYLE_PERCENTAGE, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - @interface ReviewRatingStyle {} - /** * The review rating style for five star rating. * @@ -934,27 +923,6 @@ public final class TvContractCompat { */ @RestrictTo(LIBRARY_GROUP) public interface PreviewProgramColumns { - - /** @hide */ - @IntDef({ - TYPE_MOVIE, - TYPE_TV_SERIES, - TYPE_TV_SEASON, - TYPE_TV_EPISODE, - TYPE_CLIP, - TYPE_EVENT, - TYPE_CHANNEL, - TYPE_TRACK, - TYPE_ALBUM, - TYPE_ARTIST, - TYPE_PLAYLIST, - TYPE_STATION, - TYPE_GAME - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface Type {} - /** * The program type for movie. * @@ -1046,19 +1014,6 @@ public final class TvContractCompat { */ int TYPE_GAME = 12; - /** @hide */ - @IntDef({ - ASPECT_RATIO_16_9, - ASPECT_RATIO_3_2, - ASPECT_RATIO_4_3, - ASPECT_RATIO_1_1, - ASPECT_RATIO_2_3, - ASPECT_RATIO_MOVIE_POSTER, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface AspectRatio {} - /** * The aspect ratio for 16:9. * @@ -1107,18 +1062,6 @@ public final class TvContractCompat { */ int ASPECT_RATIO_MOVIE_POSTER = 5; - /** @hide */ - @IntDef({ - AVAILABILITY_AVAILABLE, - AVAILABILITY_FREE_WITH_SUBSCRIPTION, - AVAILABILITY_PAID_CONTENT, - AVAILABILITY_PURCHASED, - AVAILABILITY_FREE, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface Availability {} - /** * The availability for "available to this user". * @@ -1155,20 +1098,6 @@ public final class TvContractCompat { */ int AVAILABILITY_FREE = 4; - /** @hide */ - @IntDef({ - INTERACTION_TYPE_VIEWS, - INTERACTION_TYPE_LISTENS, - INTERACTION_TYPE_FOLLOWERS, - INTERACTION_TYPE_FANS, - INTERACTION_TYPE_LIKES, - INTERACTION_TYPE_THUMBS, - INTERACTION_TYPE_VIEWERS, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface InteractionType {} - /** * The interaction type for "views". * @@ -2895,17 +2824,6 @@ public final class TvContractCompat { /** The MIME type of a single preview TV program. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watch_next_program"; - /** @hide */ - @IntDef({ - WATCH_NEXT_TYPE_CONTINUE, - WATCH_NEXT_TYPE_NEXT, - WATCH_NEXT_TYPE_NEW, - WATCH_NEXT_TYPE_WATCHLIST, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface WatchNextType {} - /** * The watch next type for CONTINUE. Use this type when the user has already watched more * than 1 minute of this content. diff --git a/android/support/media/tv/WatchNextProgram.java b/android/support/media/tv/WatchNextProgram.java index f4665846..c192745c 100644 --- a/android/support/media/tv/WatchNextProgram.java +++ b/android/support/media/tv/WatchNextProgram.java @@ -22,12 +22,15 @@ import android.content.ContentValues; import android.database.Cursor; import android.media.tv.TvContentRating; // For javadoc gen of super class import android.os.Build; +import android.support.annotation.IntDef; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.PreviewPrograms; // For javadoc gen of super class import android.support.media.tv.TvContractCompat.Programs; // For javadoc gen of super class import android.support.media.tv.TvContractCompat.Programs.Genres; // For javadoc gen of super class import android.support.media.tv.TvContractCompat.WatchNextPrograms; -import android.support.media.tv.TvContractCompat.WatchNextPrograms.WatchNextType; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * A convenience class to access {@link WatchNextPrograms} entries in the system content @@ -87,16 +90,34 @@ public final class WatchNextProgram extends BasePreviewProgram { private static final long INVALID_LONG_VALUE = -1; private static final int INVALID_INT_VALUE = -1; + /** @hide */ + @IntDef({ + WATCH_NEXT_TYPE_UNKNOWN, + WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE, + WatchNextPrograms.WATCH_NEXT_TYPE_NEXT, + WatchNextPrograms.WATCH_NEXT_TYPE_NEW, + WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST, + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface WatchNextType {} + + /** + * The unknown watch next type. Use this type when the actual type is not known. + */ + public static final int WATCH_NEXT_TYPE_UNKNOWN = -1; + private WatchNextProgram(Builder builder) { super(builder); } /** - * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program. + * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program, + * or {@link #WATCH_NEXT_TYPE_UNKNOWN} if it's unknown. */ public @WatchNextType int getWatchNextType() { Integer i = mValues.getAsInteger(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? WATCH_NEXT_TYPE_UNKNOWN : i; } /** diff --git a/android/support/mediacompat/testlib/IntentConstants.java b/android/support/mediacompat/testlib/IntentConstants.java index bc35935e..b0e3d5fe 100644 --- a/android/support/mediacompat/testlib/IntentConstants.java +++ b/android/support/mediacompat/testlib/IntentConstants.java @@ -17,11 +17,18 @@ package android.support.mediacompat.testlib; /** - * Constants used for sending intent between client and service apks. + * Constants used for sending intent between client and service apps. */ public class IntentConstants { public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD = "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"; + public static final String ACTION_CALL_MEDIA_SESSION_METHOD = + "android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"; + public static final String ACTION_CALL_MEDIA_CONTROLLER_METHOD = + "android.support.mediacompat.client.action.CALL_MEDIA_CONTROLLER_METHOD"; + public static final String ACTION_CALL_TRANSPORT_CONTROLS_METHOD = + "android.support.mediacompat.client.action.CALL_TRANSPORT_CONTROLS_METHOD"; public static final String KEY_METHOD_ID = "method_id"; public static final String KEY_ARGUMENT = "argument"; + public static final String KEY_SESSION_TOKEN = "session_token"; } diff --git a/android/support/mediacompat/testlib/MediaControllerConstants.java b/android/support/mediacompat/testlib/MediaControllerConstants.java new file mode 100644 index 00000000..1de00efc --- /dev/null +++ b/android/support/mediacompat/testlib/MediaControllerConstants.java @@ -0,0 +1,53 @@ +/* + * 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.mediacompat.testlib; + +/** + * Constants for testing the media controller. + */ +public class MediaControllerConstants { + + // MediaControllerCompat methods. + public static final int SEND_COMMAND = 201; + public static final int ADD_QUEUE_ITEM = 202; + public static final int ADD_QUEUE_ITEM_WITH_INDEX = 203; + public static final int REMOVE_QUEUE_ITEM = 204; + + // TransportControls methods. + public static final int PLAY = 301; + public static final int PAUSE = 302; + public static final int STOP = 303; + public static final int FAST_FORWARD = 304; + public static final int REWIND = 305; + public static final int SKIP_TO_PREVIOUS = 306; + public static final int SKIP_TO_NEXT = 307; + public static final int SEEK_TO = 308; + public static final int SET_RATING = 309; + public static final int PLAY_FROM_MEDIA_ID = 310; + public static final int PLAY_FROM_SEARCH = 311; + public static final int PLAY_FROM_URI = 312; + public static final int SEND_CUSTOM_ACTION = 313; + public static final int SEND_CUSTOM_ACTION_PARCELABLE = 314; + public static final int SKIP_TO_QUEUE_ITEM = 315; + public static final int PREPARE = 316; + public static final int PREPARE_FROM_MEDIA_ID = 317; + public static final int PREPARE_FROM_SEARCH = 318; + public static final int PREPARE_FROM_URI = 319; + public static final int SET_CAPTIONING_ENABLED = 320; + public static final int SET_REPEAT_MODE = 321; + public static final int SET_SHUFFLE_MODE = 322; +} diff --git a/android/support/mediacompat/testlib/MediaSessionConstants.java b/android/support/mediacompat/testlib/MediaSessionConstants.java new file mode 100644 index 00000000..79381e5e --- /dev/null +++ b/android/support/mediacompat/testlib/MediaSessionConstants.java @@ -0,0 +1,59 @@ +/* + * 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.mediacompat.testlib; + +/** + * Constants for testing the media session. + */ +public class MediaSessionConstants { + + // MediaSessionCompat methods. + public static final int SET_EXTRAS = 101; + public static final int SET_FLAGS = 102; + public static final int SET_METADATA = 103; + public static final int SET_PLAYBACK_STATE = 104; + public static final int SET_QUEUE = 105; + public static final int SET_QUEUE_TITLE = 106; + public static final int SET_SESSION_ACTIVITY = 107; + public static final int SET_CAPTIONING_ENABLED = 108; + public static final int SET_REPEAT_MODE = 109; + public static final int SET_SHUFFLE_MODE = 110; + public static final int SEND_SESSION_EVENT = 112; + public static final int SET_ACTIVE = 113; + public static final int RELEASE = 114; + public static final int SET_PLAYBACK_TO_LOCAL = 115; + public static final int SET_PLAYBACK_TO_REMOTE = 116; + public static final int SET_RATING_TYPE = 117; + + public static final String TEST_SESSION_TAG = "test-session-tag"; + public static final String SERVICE_PACKAGE_NAME = "android.support.mediacompat.service.test"; + public static final String TEST_KEY = "test-key"; + public static final String TEST_VALUE = "test-val"; + public static final String TEST_SESSION_EVENT = "test-session-event"; + public static final String TEST_COMMAND = "test-command"; + public static final int TEST_FLAGS = 5; + public static final int TEST_CURRENT_VOLUME = 10; + public static final int TEST_MAX_VOLUME = 11; + public static final long TEST_QUEUE_ID_1 = 10L; + public static final long TEST_QUEUE_ID_2 = 20L; + public static final String TEST_MEDIA_ID_1 = "media_id_1"; + public static final String TEST_MEDIA_ID_2 = "media_id_2"; + public static final long TEST_ACTION = 55L; + + public static final int TEST_ERROR_CODE = 0x3; + public static final String TEST_ERROR_MSG = "test-error-msg"; +} diff --git a/android/support/transition/AutoTransition.java b/android/support/transition/AutoTransition.java index 02b49e26..bf39c3c3 100644 --- a/android/support/transition/AutoTransition.java +++ b/android/support/transition/AutoTransition.java @@ -45,9 +45,9 @@ public class AutoTransition extends TransitionSet { private void init() { setOrdering(ORDERING_SEQUENTIAL); - addTransition(new Fade(Fade.OUT)). - addTransition(new ChangeBounds()). - addTransition(new Fade(Fade.IN)); + addTransition(new Fade(Fade.OUT)) + .addTransition(new ChangeBounds()) + .addTransition(new Fade(Fade.IN)); } } diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java index 81431972..af37f77a 100644 --- a/android/support/v17/leanback/widget/GridLayoutManager.java +++ b/android/support/v17/leanback/widget/GridLayoutManager.java @@ -2240,10 +2240,24 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { focusToViewInLayout(hadFocus, scrollToFocus, -deltaPrimary, -deltaSecondary); appendVisibleItems(); prependVisibleItems(); - removeInvisibleViewsAtFront(); - removeInvisibleViewsAtEnd(); + // b/67370222: do not removeInvisibleViewsAtFront/End() in the loop, otherwise + // loop may bounce between scroll forward and scroll backward forever. Example: + // Assuming there are 19 items, child#18 and child#19 are both in RV, we are + // trying to focus to child#18 and there are 200px remaining scroll distance. + // 1 focusToViewInLayout() tries scroll forward 50 px to align focused child#18 on + // right edge, but there to compensate remaining scroll 200px, also scroll + // backward 200px, 150px pushes last child#19 out side of right edge. + // 2 removeInvisibleViewsAtEnd() remove last child#19, updateScrollLimits() + // invalidates scroll max + // 3 In next iteration, when scroll max/min is unknown, focusToViewInLayout() will + // align focused child#18 at center of screen. + // 4 Because #18 is aligned at center, appendVisibleItems() will fill child#19 to + // the right. + // 5 (back to 1 and loop forever) } while (mGrid.getFirstVisibleIndex() != oldFirstVisible || mGrid.getLastVisibleIndex() != oldLastVisible); + removeInvisibleViewsAtFront(); + removeInvisibleViewsAtEnd(); if (state.willRunPredictiveAnimations()) { fillScrapViewsInPostLayout(); diff --git a/android/support/v4/content/res/ResourcesCompat.java b/android/support/v4/content/res/ResourcesCompat.java index 4c70ce93..15b8ce9a 100644 --- a/android/support/v4/content/res/ResourcesCompat.java +++ b/android/support/v4/content/res/ResourcesCompat.java @@ -307,11 +307,11 @@ public final class ResourcesCompat { */ @RestrictTo(LIBRARY_GROUP) public static Typeface getFont(@NonNull Context context, @FontRes int id, TypedValue value, - int style) throws NotFoundException { + int style, @Nullable FontCallback fontCallback) throws NotFoundException { if (context.isRestricted()) { return null; } - return loadFont(context, id, value, style, null /* callback */, null /* handler */, + return loadFont(context, id, value, style, fontCallback, null /* handler */, true /* isXmlRequest */); } diff --git a/android/support/v4/media/RatingCompat.java b/android/support/v4/media/RatingCompat.java index b538cac4..e70243f8 100644 --- a/android/support/v4/media/RatingCompat.java +++ b/android/support/v4/media/RatingCompat.java @@ -18,6 +18,7 @@ package android.support.v4.media; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; +import android.media.Rating; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; @@ -326,25 +327,25 @@ public final class RatingCompat implements Parcelable { */ public static RatingCompat fromRating(Object ratingObj) { if (ratingObj != null && Build.VERSION.SDK_INT >= 19) { - final int ratingStyle = RatingCompatKitkat.getRatingStyle(ratingObj); + final int ratingStyle = ((Rating) ratingObj).getRatingStyle(); final RatingCompat rating; - if (RatingCompatKitkat.isRated(ratingObj)) { + if (((Rating) ratingObj).isRated()) { switch (ratingStyle) { case RATING_HEART: - rating = newHeartRating(RatingCompatKitkat.hasHeart(ratingObj)); + rating = newHeartRating(((Rating) ratingObj).hasHeart()); break; case RATING_THUMB_UP_DOWN: - rating = newThumbRating(RatingCompatKitkat.isThumbUp(ratingObj)); + rating = newThumbRating(((Rating) ratingObj).isThumbUp()); break; case RATING_3_STARS: case RATING_4_STARS: case RATING_5_STARS: rating = newStarRating(ratingStyle, - RatingCompatKitkat.getStarRating(ratingObj)); + ((Rating) ratingObj).getStarRating()); break; case RATING_PERCENTAGE: rating = newPercentageRating( - RatingCompatKitkat.getPercentRating(ratingObj)); + ((Rating) ratingObj).getPercentRating()); break; default: return null; @@ -372,25 +373,25 @@ public final class RatingCompat implements Parcelable { if (isRated()) { switch (mRatingStyle) { case RATING_HEART: - mRatingObj = RatingCompatKitkat.newHeartRating(hasHeart()); + mRatingObj = Rating.newHeartRating(hasHeart()); break; case RATING_THUMB_UP_DOWN: - mRatingObj = RatingCompatKitkat.newThumbRating(isThumbUp()); + mRatingObj = Rating.newThumbRating(isThumbUp()); break; case RATING_3_STARS: case RATING_4_STARS: case RATING_5_STARS: - mRatingObj = RatingCompatKitkat.newStarRating(mRatingStyle, + mRatingObj = Rating.newStarRating(mRatingStyle, getStarRating()); break; case RATING_PERCENTAGE: - mRatingObj = RatingCompatKitkat.newPercentageRating(getPercentRating()); + mRatingObj = Rating.newPercentageRating(getPercentRating()); break; default: return null; } } else { - mRatingObj = RatingCompatKitkat.newUnratedRating(mRatingStyle); + mRatingObj = Rating.newUnratedRating(mRatingStyle); } } return mRatingObj; diff --git a/android/support/v4/media/RatingCompatKitkat.java b/android/support/v4/media/RatingCompatKitkat.java deleted file mode 100644 index 1d3fa505..00000000 --- a/android/support/v4/media/RatingCompatKitkat.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2014 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.media; - -import android.media.Rating; -import android.support.annotation.RequiresApi; - -@RequiresApi(19) -class RatingCompatKitkat { - public static Object newUnratedRating(int ratingStyle) { - return Rating.newUnratedRating(ratingStyle); - } - - public static Object newHeartRating(boolean hasHeart) { - return Rating.newHeartRating(hasHeart); - } - - public static Object newThumbRating(boolean thumbIsUp) { - return Rating.newThumbRating(thumbIsUp); - } - - public static Object newStarRating(int starRatingStyle, float starRating) { - return Rating.newStarRating(starRatingStyle, starRating); - } - - public static Object newPercentageRating(float percent) { - return Rating.newPercentageRating(percent); - } - - public static boolean isRated(Object ratingObj) { - return ((Rating)ratingObj).isRated(); - } - - public static int getRatingStyle(Object ratingObj) { - return ((Rating)ratingObj).getRatingStyle(); - } - - public static boolean hasHeart(Object ratingObj) { - return ((Rating)ratingObj).hasHeart(); - } - - public static boolean isThumbUp(Object ratingObj) { - return ((Rating)ratingObj).isThumbUp(); - } - - public static float getStarRating(Object ratingObj) { - return ((Rating)ratingObj).getStarRating(); - } - - public static float getPercentRating(Object ratingObj) { - return ((Rating)ratingObj).getPercentRating(); - } -} diff --git a/android/support/v4/provider/FontsContractCompat.java b/android/support/v4/provider/FontsContractCompat.java index 9ef1b0b0..09261869 100644 --- a/android/support/v4/provider/FontsContractCompat.java +++ b/android/support/v4/provider/FontsContractCompat.java @@ -303,6 +303,9 @@ public class FontsContractCompat { final ArrayList<ReplyCallback<TypefaceResult>> replies; synchronized (sLock) { replies = sPendingReplies.get(id); + if (replies == null) { + return; // Nobody requested replies. Do nothing. + } sPendingReplies.remove(id); } for (int i = 0; i < replies.size(); ++i) { diff --git a/android/support/v7/app/MediaRouteButton.java b/android/support/v7/app/MediaRouteButton.java index d3f7020b..fdbcf9ad 100644 --- a/android/support/v7/app/MediaRouteButton.java +++ b/android/support/v7/app/MediaRouteButton.java @@ -121,8 +121,7 @@ public class MediaRouteButton extends View { } public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { - super(MediaRouterThemeHelper.createThemedContext(context, defStyleAttr), attrs, - defStyleAttr); + super(MediaRouterThemeHelper.createThemedButtonContext(context), attrs, defStyleAttr); context = getContext(); mRouter = MediaRouter.getInstance(context); diff --git a/android/support/v7/app/MediaRouteChooserDialog.java b/android/support/v7/app/MediaRouteChooserDialog.java index 0ab2eb11..17364efb 100644 --- a/android/support/v7/app/MediaRouteChooserDialog.java +++ b/android/support/v7/app/MediaRouteChooserDialog.java @@ -92,10 +92,8 @@ public class MediaRouteChooserDialog extends AppCompatDialog { } public MediaRouteChooserDialog(Context context, int theme) { - // If we pass theme ID of 0 to AppCompatDialog, it will apply dialogTheme on the context, - // which may override our style settings. Passes our uppermost theme ID to prevent this. - super(MediaRouterThemeHelper.createThemedContext(context, theme), - theme == 0 ? MediaRouterThemeHelper.createThemeForDialog(context, theme) : theme); + super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, false), + MediaRouterThemeHelper.createThemedDialogStyle(context)); context = getContext(); mRouter = MediaRouter.getInstance(context); diff --git a/android/support/v7/app/MediaRouteControllerDialog.java b/android/support/v7/app/MediaRouteControllerDialog.java index 4b9a17a3..d89bf21e 100644 --- a/android/support/v7/app/MediaRouteControllerDialog.java +++ b/android/support/v7/app/MediaRouteControllerDialog.java @@ -201,12 +201,8 @@ public class MediaRouteControllerDialog extends AlertDialog { } public MediaRouteControllerDialog(Context context, int theme) { - // If we pass theme ID of 0 to AppCompatDialog, it will apply dialogTheme on the context, - // which may override our style settings. Passes our uppermost theme ID to prevent this. - super(MediaRouterThemeHelper.createThemedContext(context, - MediaRouterThemeHelper.getAlertDialogResolvedTheme(context, theme)), theme == 0 - ? MediaRouterThemeHelper.createThemeForDialog(context, MediaRouterThemeHelper - .getAlertDialogResolvedTheme(context, theme)) : theme); + super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, true), + MediaRouterThemeHelper.createThemedDialogStyle(context)); mContext = getContext(); mControllerCallback = new MediaControllerCallback(); diff --git a/android/support/v7/app/MediaRouterThemeHelper.java b/android/support/v7/app/MediaRouterThemeHelper.java index 9ef218e0..69e40ac7 100644 --- a/android/support/v7/app/MediaRouterThemeHelper.java +++ b/android/support/v7/app/MediaRouterThemeHelper.java @@ -42,47 +42,76 @@ final class MediaRouterThemeHelper { private MediaRouterThemeHelper() { } - /** - * Creates a themed context based on the explicit style resource or the parent context's default - * theme. - * <p> - * The theme which will be applied on top of the parent {@code context}'s theme is determined - * by the primary color defined in the given {@code style}, or in the parent {@code context}. + static Context createThemedButtonContext(Context context) { + // Apply base Media Router theme. + context = new ContextThemeWrapper(context, getRouterThemeId(context)); + + // Apply custom Media Router theme. + int style = getThemeResource(context, R.attr.mediaRouteTheme); + if (style != 0) { + context = new ContextThemeWrapper(context, style); + } + + return context; + } + + /* + * The following two methods are to be used in conjunction. They should be used to prepare + * the context and theme for a super class constructor (the latter method relies on the + * former method to properly prepare the context): + * super(context = createThemedDialogContext(context, theme), + * createThemedDialogStyle(context)); * - * @param context the parent context - * @param style the resource ID of the style against which to inflate this context, or - * {@code 0} to use the parent {@code context}'s default theme. - * @return The themed context. + * It will apply theme in the following order (style lookups will be done in reverse): + * 1) Current theme + * 2) Supplied theme + * 3) Base Media Router theme + * 4) Custom Media Router theme, if provided */ - static Context createThemedContext(Context context, int style) { - // First, apply dialog property overlay. - Context themedContext = - new ContextThemeWrapper(context, getStyledRouterThemeId(context, style)); - int customizedThemeId = getThemeResource(context, R.attr.mediaRouteTheme); - return customizedThemeId == 0 ? themedContext - : new ContextThemeWrapper(themedContext, customizedThemeId); + static Context createThemedDialogContext(Context context, int theme, boolean alertDialog) { + // 1) Current theme is already applied to the context + + // 2) If no theme is supplied, look it up from the context (dialogTheme/alertDialogTheme) + if (theme == 0) { + theme = getThemeResource(context, !alertDialog + ? android.support.v7.appcompat.R.attr.dialogTheme + : android.support.v7.appcompat.R.attr.alertDialogTheme); + } + // Apply it + context = new ContextThemeWrapper(context, theme); + + // 3) If a custom Media Router theme is provided then apply the base theme + if (getThemeResource(context, R.attr.mediaRouteTheme) != 0) { + context = new ContextThemeWrapper(context, getRouterThemeId(context)); + } + + return context; } + // This method should be used in conjunction with the previous method. + static int createThemedDialogStyle(Context context) { + // 4) Apply the custom Media Router theme + int theme = getThemeResource(context, R.attr.mediaRouteTheme); + if (theme == 0) { + // 3) No custom MediaRouther theme was provided so apply the base theme instead + theme = getRouterThemeId(context); + } - /** - * Creates the theme resource ID intended to be used by dialogs. - */ - static int createThemeForDialog(Context context, int style) { - int customizedThemeId = getThemeResource(context, R.attr.mediaRouteTheme); - return customizedThemeId != 0 ? customizedThemeId : getStyledRouterThemeId(context, style); + return theme; } + // END. Previous two methods should be used in conjunction. - public static int getThemeResource(Context context, int attr) { + static int getThemeResource(Context context, int attr) { TypedValue value = new TypedValue(); return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0; } - public static float getDisabledAlpha(Context context) { + static float getDisabledAlpha(Context context) { TypedValue value = new TypedValue(); return context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true) ? value.getFloat() : 0.5f; } - public static @ControllerColorType int getControllerColor(Context context, int style) { + static @ControllerColorType int getControllerColor(Context context, int style) { int primaryColor = getThemeColor(context, style, android.support.v7.appcompat.R.attr.colorPrimary); if (ColorUtils.calculateContrast(COLOR_WHITE_ON_DARK_BACKGROUND, primaryColor) @@ -92,7 +121,7 @@ final class MediaRouterThemeHelper { return COLOR_DARK_ON_LIGHT_BACKGROUND; } - public static int getButtonTextColor(Context context) { + static int getButtonTextColor(Context context) { int primaryColor = getThemeColor(context, 0, android.support.v7.appcompat.R.attr.colorPrimary); int backgroundColor = getThemeColor(context, 0, android.R.attr.colorBackground); @@ -104,7 +133,7 @@ final class MediaRouterThemeHelper { return primaryColor; } - public static void setMediaControlsBackgroundColor( + static void setMediaControlsBackgroundColor( Context context, View mainControls, View groupControls, boolean hasGroup) { int primaryColor = getThemeColor(context, 0, android.support.v7.appcompat.R.attr.colorPrimary); @@ -124,7 +153,7 @@ final class MediaRouterThemeHelper { groupControls.setTag(primaryDarkColor); } - public static void setVolumeSliderColor( + static void setVolumeSliderColor( Context context, MediaRouteVolumeSlider volumeSlider, View backgroundView) { int controllerColor = getControllerColor(context, 0); if (Color.alpha(controllerColor) != 0xFF) { @@ -136,23 +165,10 @@ final class MediaRouterThemeHelper { volumeSlider.setColor(controllerColor); } - // This is copied from {@link AlertDialog#resolveDialogTheme} to pre-evaluate theme in advance. - public static int getAlertDialogResolvedTheme(Context context, int themeResId) { - if (themeResId >= 0x01000000) { // start of real resource IDs. - return themeResId; - } else { - TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute( - android.support.v7.appcompat.R.attr.alertDialogTheme, outValue, true); - return outValue.resourceId; - } - } - private static boolean isLightTheme(Context context) { TypedValue value = new TypedValue(); - return context.getTheme().resolveAttribute( - android.support.v7.appcompat.R.attr.isLightTheme, value, true) - && value.data != 0; + return context.getTheme().resolveAttribute(android.support.v7.appcompat.R.attr.isLightTheme, + value, true) && value.data != 0; } private static int getThemeColor(Context context, int style, int attr) { @@ -173,16 +189,16 @@ final class MediaRouterThemeHelper { return value.data; } - private static int getStyledRouterThemeId(Context context, int style) { + private static int getRouterThemeId(Context context) { int themeId; if (isLightTheme(context)) { - if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) { + if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) { themeId = R.style.Theme_MediaRouter_Light; } else { themeId = R.style.Theme_MediaRouter_Light_DarkControlPanel; } } else { - if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) { + if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) { themeId = R.style.Theme_MediaRouter_LightControlPanel; } else { themeId = R.style.Theme_MediaRouter; diff --git a/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.java b/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.java deleted file mode 100644 index aab74172..00000000 --- a/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.java +++ /dev/null @@ -1,164 +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.v7.recyclerview.extensions; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; - -import android.arch.paging.TestExecutor; -import android.support.annotation.NonNull; -import android.support.test.filters.SmallTest; -import android.support.v7.util.ListUpdateCallback; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.Arrays; - -@SmallTest -@RunWith(JUnit4.class) -public class ListAdapterHelperTest { - private TestExecutor mMainThread = new TestExecutor(); - private TestExecutor mBackgroundThread = new TestExecutor(); - - - private static final DiffCallback<String> STRING_DIFF_CALLBACK = new DiffCallback<String>() { - @Override - public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) { - return oldItem.equals(newItem); - } - - @Override - public boolean areContentsTheSame(@NonNull String oldItem, @NonNull String newItem) { - return oldItem.equals(newItem); - } - }; - - private static final ListUpdateCallback IGNORE_CALLBACK = new ListUpdateCallback() { - @Override - public void onInserted(int position, int count) { - } - - @Override - public void onRemoved(int position, int count) { - } - - @Override - public void onMoved(int fromPosition, int toPosition) { - } - - @Override - public void onChanged(int position, int count, Object payload) { - } - }; - - - private <T> ListAdapterHelper<T> createHelper( - ListUpdateCallback listUpdateCallback, DiffCallback<T> diffCallback) { - return new ListAdapterHelper<T>(listUpdateCallback, - new ListAdapterConfig.Builder<T>() - .setDiffCallback(diffCallback) - .setMainThreadExecutor(mMainThread) - .setBackgroundThreadExecutor(mBackgroundThread) - .build()); - } - - @Test - public void initialState() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - ListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - assertEquals(0, helper.getItemCount()); - verifyZeroInteractions(callback); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getEmpty() { - ListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.getItem(0); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getNegative() { - ListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.setList(Arrays.asList("a", "b")); - helper.getItem(-1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getPastEnd() { - ListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.setList(Arrays.asList("a", "b")); - helper.getItem(2); - } - - @Test - public void setListSimple() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - ListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - helper.setList(Arrays.asList("a", "b")); - - assertEquals(2, helper.getItemCount()); - assertEquals("a", helper.getItem(0)); - assertEquals("b", helper.getItem(1)); - - verify(callback).onInserted(0, 2); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - } - - @Test - public void setListUpdate() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - ListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - // initial list (immediate) - helper.setList(Arrays.asList("a", "b")); - verify(callback).onInserted(0, 2); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - - // update (deferred) - helper.setList(Arrays.asList("a", "b", "c")); - verifyNoMoreInteractions(callback); - drain(); - verify(callback).onInserted(2, 1); - verifyNoMoreInteractions(callback); - - // clear (immediate) - helper.setList(null); - verify(callback).onRemoved(0, 3); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - - } - - private void drain() { - boolean executed; - do { - executed = mBackgroundThread.executeAll(); - executed |= mMainThread.executeAll(); - } while (executed); - } -} diff --git a/android/support/v7/util/DiffUtil.java b/android/support/v7/util/DiffUtil.java index 6302666f..ebc33f31 100644 --- a/android/support/v7/util/DiffUtil.java +++ b/android/support/v7/util/DiffUtil.java @@ -16,6 +16,7 @@ package android.support.v7.util; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; @@ -348,6 +349,72 @@ public class DiffUtil { } /** + * Callback for calculating the diff between two non-null items in a list. + * <p> + * {@link Callback} serves two roles - list indexing, and item diffing. ItemCallback handles + * just the second of these, which allows separation of code that indexes into an array or List + * from the presentation-layer and content specific diffing code. + * + * @param <T> Type of items to compare. + */ + public abstract static class ItemCallback<T> { + /** + * Called to check whether two objects represent the same item. + * <p> + * For example, if your items have unique ids, this method should check their id equality. + * + * @param oldItem The item in the old list. + * @param newItem The item in the new list. + * @return True if the two items represent the same object or false if they are different. + * + * @see Callback#areItemsTheSame(int, int) + */ + public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem); + + /** + * Called to check whether two items have the same data. + * <p> + * This information is used to detect if the contents of an item have changed. + * <p> + * This method to check equality instead of {@link Object#equals(Object)} so that you can + * change its behavior depending on your UI. + * <p> + * For example, if you are using DiffUtil with a + * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should + * return whether the items' visual representations are the same. + * <p> + * This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for + * these items. + * + * @param oldItem The item in the old list. + * @param newItem The item in the new list. + * @return True if the contents of the items are the same or false if they are different. + * + * @see Callback#areContentsTheSame(int, int) + */ + public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem); + + /** + * When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and + * {@link #areContentsTheSame(T, T)} returns false for them, this method is called to + * get a payload about the change. + * <p> + * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the + * particular field that changed in the item and your + * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that + * information to run the correct animation. + * <p> + * Default implementation returns {@code null}. + * + * @see Callback#getChangePayload(int, int) + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) { + return null; + } + } + + /** * Snakes represent a match between two lists. It is optionally prefixed or postfixed with an * add or remove operation. See the Myers' paper for details. */ diff --git a/android/support/v7/widget/AppCompatTextHelper.java b/android/support/v7/widget/AppCompatTextHelper.java index 51510aa2..fa6196f5 100644 --- a/android/support/v7/widget/AppCompatTextHelper.java +++ b/android/support/v7/widget/AppCompatTextHelper.java @@ -29,6 +29,7 @@ import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; +import android.support.v4.content.res.ResourcesCompat; import android.support.v4.widget.TextViewCompat; import android.support.v7.appcompat.R; import android.text.method.PasswordTransformationMethod; @@ -36,6 +37,8 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.widget.TextView; +import java.lang.ref.WeakReference; + @RequiresApi(9) class AppCompatTextHelper { @@ -63,6 +66,7 @@ class AppCompatTextHelper { private int mStyle = Typeface.NORMAL; private Typeface mFontTypeface; + private boolean mAsyncFontPending; AppCompatTextHelper(TextView view) { mView = view; @@ -213,8 +217,23 @@ class AppCompatTextHelper { ? R.styleable.TextAppearance_android_fontFamily : R.styleable.TextAppearance_fontFamily; if (!context.isRestricted()) { + final WeakReference<TextView> textViewWeak = new WeakReference<>(mView); + ResourcesCompat.FontCallback replyCallback = new ResourcesCompat.FontCallback() { + @Override + public void onFontRetrieved(@NonNull Typeface typeface) { + onAsyncTypefaceReceived(textViewWeak, typeface); + } + + @Override + public void onFontRetrievalFailed(int reason) { + // Do nothing. + } + }; try { - mFontTypeface = a.getFont(fontFamilyId, mStyle); + // Note the callback will be triggered on the UI thread. + mFontTypeface = a.getFont(fontFamilyId, mStyle, replyCallback); + // If this call gave us an immediate result, ignore any pending callbacks. + mAsyncFontPending = mFontTypeface == null; } catch (UnsupportedOperationException | Resources.NotFoundException e) { // Expected if it is not a font resource. } @@ -222,12 +241,16 @@ class AppCompatTextHelper { if (mFontTypeface == null) { // Try with String. This is done by TextView JB+, but fails in ICS String fontFamilyName = a.getString(fontFamilyId); - mFontTypeface = Typeface.create(fontFamilyName, mStyle); + if (fontFamilyName != null) { + mFontTypeface = Typeface.create(fontFamilyName, mStyle); + } } return; } if (a.hasValue(R.styleable.TextAppearance_android_typeface)) { + // Ignore previous pending fonts + mAsyncFontPending = false; int typefaceIndex = a.getInt(R.styleable.TextAppearance_android_typeface, SANS); switch (typefaceIndex) { case SANS: @@ -245,6 +268,16 @@ class AppCompatTextHelper { } } + private void onAsyncTypefaceReceived(WeakReference<TextView> textViewWeak, Typeface typeface) { + if (mAsyncFontPending) { + mFontTypeface = typeface; + final TextView textView = textViewWeak.get(); + if (textView != null) { + textView.setTypeface(typeface, mStyle); + } + } + } + void onSetTextAppearance(Context context, int resId) { final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, resId, R.styleable.TextAppearance); diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java index 7009733c..4bc17a86 100644 --- a/android/support/v7/widget/RecyclerView.java +++ b/android/support/v7/widget/RecyclerView.java @@ -6408,16 +6408,16 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } void markKnownViewsInvalid() { - if (mAdapter != null && mAdapter.hasStableIds()) { - final int cachedCount = mCachedViews.size(); - for (int i = 0; i < cachedCount; i++) { - final ViewHolder holder = mCachedViews.get(i); - if (holder != null) { - holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); - holder.addChangePayload(null); - } + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); + holder.addChangePayload(null); } - } else { + } + + if (mAdapter == null || !mAdapter.hasStableIds()) { // we cannot re-use cached views in this case. Recycle them all recycleAndClearCachedViews(); } diff --git a/android/support/v7/widget/TintTypedArray.java b/android/support/v7/widget/TintTypedArray.java index 22709551..384c4615 100644 --- a/android/support/v7/widget/TintTypedArray.java +++ b/android/support/v7/widget/TintTypedArray.java @@ -106,7 +106,8 @@ public class TintTypedArray { * not a font resource. */ @Nullable - public Typeface getFont(@StyleableRes int index, int style) { + public Typeface getFont(@StyleableRes int index, int style, + @Nullable ResourcesCompat.FontCallback fontCallback) { final int resourceId = mWrapped.getResourceId(index, 0); if (resourceId == 0) { return null; @@ -114,7 +115,7 @@ public class TintTypedArray { if (mTypedValue == null) { mTypedValue = new TypedValue(); } - return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style); + return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style, fontCallback); } public int length() { diff --git a/android/support/wear/ambient/AmbientMode.java b/android/support/wear/ambient/AmbientMode.java index 7fbbbb3f..db53dfc1 100644 --- a/android/support/wear/ambient/AmbientMode.java +++ b/android/support/wear/ambient/AmbientMode.java @@ -22,7 +22,6 @@ import android.content.Context; import android.os.Bundle; import android.support.annotation.CallSuper; import android.support.annotation.VisibleForTesting; -import android.util.Log; import com.google.android.wearable.compat.WearableActivityController; @@ -263,20 +262,6 @@ public final class AmbientMode extends Fragment { AmbientController() {} /** - * Sets whether this activity's task should be moved to the front when the system exits - * ambient mode. If true, the activity's task may be moved to the front if it was the last - * activity to be running when ambient started, depending on how much time the system spent - * in ambient mode. - */ - public void setAutoResumeEnabled(boolean enabled) { - if (mDelegate != null) { - mDelegate.setAutoResumeEnabled(enabled); - } else { - Log.w(TAG, "The fragment is not yet fully initialized, this call is a no-op"); - } - } - - /** * @return {@code true} if the activity is currently in ambient. */ public boolean isAmbient() { |