/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.ui.mediapicker; import android.app.Activity; import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import androidx.appcompat.app.ActionBar; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.LinearLayout; import com.android.messaging.Factory; import com.android.messaging.R; import com.android.messaging.datamodel.DataModel; import com.android.messaging.datamodel.binding.Binding; import com.android.messaging.datamodel.binding.BindingBase; import com.android.messaging.datamodel.binding.ImmutableBindingRef; import com.android.messaging.datamodel.data.DraftMessageData; import com.android.messaging.datamodel.data.MediaPickerData; import com.android.messaging.datamodel.data.MessagePartData; import com.android.messaging.datamodel.data.PendingAttachmentData; import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; import com.android.messaging.ui.BugleActionBarActivity; import com.android.messaging.ui.FixedViewPagerAdapter; import com.android.messaging.ui.mediapicker.DocumentImagePicker.SelectionListener; import com.android.messaging.util.AccessibilityUtil; import com.android.messaging.util.Assert; import com.android.messaging.util.UiUtils; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Fragment used to select or capture media to be added to the message */ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDataProvider { /** The listener interface for events from the media picker */ public interface MediaPickerListener { /** Called when the media picker is opened so the host can accommodate the UI */ void onOpened(); /** * Called when the media picker goes into or leaves full screen mode so the host can * accommodate the fullscreen UI */ void onFullScreenChanged(boolean fullScreen); /** * Called when the user selects one or more items * @param items The list of items which were selected */ void onItemsSelected(Collection items, boolean dismissMediaPicker); /** * Called when the user unselects one item. */ void onItemUnselected(MessagePartData item); /** * Called when the media picker is closed. Always called immediately after onItemsSelected */ void onDismissed(); /** * Called when media item selection is confirmed in a multi-select action. */ void onConfirmItemSelection(); /** * Called when a pending attachment is added. * @param pendingItem the pending attachment data being loaded. */ void onPendingItemAdded(PendingAttachmentData pendingItem); /** * Called when a new media chooser is selected. */ void onChooserSelected(final int chooserIndex); } /** The tag used when registering and finding this fragment */ public static final String FRAGMENT_TAG = "mediapicker"; // Media type constants that the media picker supports public static final int MEDIA_TYPE_DEFAULT = 0x0000; public static final int MEDIA_TYPE_NONE = 0x0000; public static final int MEDIA_TYPE_IMAGE = 0x0001; public static final int MEDIA_TYPE_VIDEO = 0x0002; public static final int MEDIA_TYPE_AUDIO = 0x0004; public static final int MEDIA_TYPE_VCARD = 0x0008; public static final int MEDIA_TYPE_LOCATION = 0x0010; private static final int MEDA_TYPE_INVALID = 0x0020; public static final int MEDIA_TYPE_ALL = 0xFFFF; /** The listener to call when events occur */ private MediaPickerListener mListener; /** The handler used to dispatch calls to the listener */ private Handler mListenerHandler; /** The bit flags of media types supported */ private int mSupportedMediaTypes; /** The list of choosers which could be within the media picker */ private final MediaChooser[] mChoosers; /** The list of currently enabled choosers */ private final ArrayList mEnabledChoosers; /** The currently selected chooser */ private MediaChooser mSelectedChooser; /** The main panel that controls the custom layout */ private MediaPickerPanel mMediaPickerPanel; /** The linear layout that holds the icons to select individual chooser tabs */ private LinearLayout mTabStrip; /** The view pager to swap between choosers */ private ViewPager mViewPager; /** The current pager adapter for the view pager */ private FixedViewPagerAdapter mPagerAdapter; /** True if the media picker is visible */ private boolean mOpen; /** The theme color to use to make the media picker match the rest of the UI */ private int mThemeColor; @VisibleForTesting final Binding mBinding = BindingBase.createBinding(this); /** Handles picking image from the document picker */ private DocumentImagePicker mDocumentImagePicker; /** Provides subscription-related data to access per-subscription configurations. */ private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider; /** Provides access to DraftMessageData associated with the current conversation */ private ImmutableBindingRef mDraftMessageDataModel; public MediaPicker() { this(Factory.get().getApplicationContext()); } public MediaPicker(final Context context) { mBinding.bind(DataModel.get().createMediaPickerData(context)); mEnabledChoosers = new ArrayList(); mChoosers = new MediaChooser[] { new CameraMediaChooser(this), new GalleryMediaChooser(this), new AudioMediaChooser(this), }; mOpen = false; setSupportedMediaTypes(MEDIA_TYPE_ALL); } private boolean mIsAttached; private int mStartingMediaTypeOnAttach = MEDA_TYPE_INVALID; private boolean mAnimateOnAttach; @Override public void onAttach (final Activity activity) { super.onAttach(activity); mIsAttached = true; if (mStartingMediaTypeOnAttach != MEDA_TYPE_INVALID) { // open() was previously called. Do the pending open now. doOpen(mStartingMediaTypeOnAttach, mAnimateOnAttach); } } @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mBinding.getData().init(getLoaderManager()); mDocumentImagePicker = new DocumentImagePicker(this, new SelectionListener() { @Override public void onDocumentSelected(final PendingAttachmentData data) { if (mBinding.isBound()) { dispatchPendingItemAdded(data); } } }); } @Override public View onCreateView( final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { mMediaPickerPanel = (MediaPickerPanel) inflater.inflate( R.layout.mediapicker_fragment, container, false); mMediaPickerPanel.setMediaPicker(this); mTabStrip = (LinearLayout) mMediaPickerPanel.findViewById(R.id.mediapicker_tabstrip); mTabStrip.setBackgroundColor(mThemeColor); for (final MediaChooser chooser : mChoosers) { chooser.onCreateTabButton(inflater, mTabStrip); final boolean enabled = (chooser.getSupportedMediaTypes() & mSupportedMediaTypes) != MEDIA_TYPE_NONE; final ImageButton tabButton = chooser.getTabButton(); if (tabButton != null) { tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE); mTabStrip.addView(tabButton); } } mViewPager = (ViewPager) mMediaPickerPanel.findViewById(R.id.mediapicker_view_pager); mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled( final int position, final float positionOffset, final int positionOffsetPixels) { } @Override public void onPageSelected(int position) { // The position returned is relative to if we are in RtL mode. This class never // switches the indices of the elements if we are in RtL mode so we need to // translate the index back. For example, if the user clicked the item most to the // right in RtL mode we would want the index to appear as 0 here, however the // position returned would the last possible index. if (UiUtils.isRtlMode()) { position = mEnabledChoosers.size() - 1 - position; } selectChooser(mEnabledChoosers.get(position)); } @Override public void onPageScrollStateChanged(final int state) { } }); // Camera initialization is expensive, so don't realize offscreen pages if not needed. mViewPager.setOffscreenPageLimit(0); mViewPager.setAdapter(mPagerAdapter); final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled( getActivity()); mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled); mMediaPickerPanel.setExpanded(mOpen, true, mEnabledChoosers.indexOf(mSelectedChooser)); return mMediaPickerPanel; } @Override public void onPause() { super.onPause(); CameraManager.get().onPause(); for (final MediaChooser chooser : mEnabledChoosers) { chooser.onPause(); } } @Override public void onResume() { super.onResume(); CameraManager.get().onResume(); for (final MediaChooser chooser : mEnabledChoosers) { chooser.onResume(); } } @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); mDocumentImagePicker.onActivityResult(requestCode, resultCode, data); } @Override public void onDestroy() { super.onDestroy(); mBinding.unbind(); } /** * Sets the theme color to make the media picker match the surrounding UI * @param themeColor The new theme color */ public void setConversationThemeColor(final int themeColor) { mThemeColor = themeColor; if (mTabStrip != null) { mTabStrip.setBackgroundColor(mThemeColor); } for (final MediaChooser chooser : mEnabledChoosers) { chooser.setThemeColor(mThemeColor); } } /** * Gets the current conversation theme color. */ public int getConversationThemeColor() { return mThemeColor; } public void setDraftMessageDataModel(final BindingBase draftBinding) { mDraftMessageDataModel = Binding.createBindingReference(draftBinding); } public ImmutableBindingRef getDraftMessageDataModel() { return mDraftMessageDataModel; } public void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) { mSubscriptionDataProvider = provider; } @Override public int getConversationSelfSubId() { return mSubscriptionDataProvider.getConversationSelfSubId(); } /** * Opens the media picker and optionally shows the chooser for the supplied media type * @param startingMediaType The media type of the chooser to open if {@link #MEDIA_TYPE_DEFAULT} * is used, then the default chooser from saved shared prefs is opened */ public void open(final int startingMediaType, final boolean animate) { mOpen = true; if (mIsAttached) { doOpen(startingMediaType, animate); } else { // open() can get called immediately after the MediaPicker is created. In that case, // we defer doing work as it may require an attached fragment (eg. calling // Fragment#requestPermission) mStartingMediaTypeOnAttach = startingMediaType; mAnimateOnAttach = animate; } } private void doOpen(int startingMediaType, final boolean animate) { final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled( // getActivity() will be null at this point Factory.get().getApplicationContext()); // If no specific starting type is specified (i.e. MEDIA_TYPE_DEFAULT), try to get the // last opened chooser index from shared prefs. if (startingMediaType == MEDIA_TYPE_DEFAULT) { final int selectedChooserIndex = mBinding.getData().getSelectedChooserIndex(); if (selectedChooserIndex >= 0 && selectedChooserIndex < mEnabledChoosers.size()) { selectChooser(mEnabledChoosers.get(selectedChooserIndex)); } else { // This is the first time the picker is being used if (isTouchExplorationEnabled) { // Accessibility defaults to audio attachment mode. startingMediaType = MEDIA_TYPE_AUDIO; } } } if (mSelectedChooser == null) { for (final MediaChooser chooser : mEnabledChoosers) { if (startingMediaType == MEDIA_TYPE_DEFAULT || (startingMediaType & chooser.getSupportedMediaTypes()) != MEDIA_TYPE_NONE) { selectChooser(chooser); break; } } } if (mSelectedChooser == null) { // Fall back to the first chooser. selectChooser(mEnabledChoosers.get(0)); } if (mMediaPickerPanel != null) { mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled); mMediaPickerPanel.setExpanded(true, animate, mEnabledChoosers.indexOf(mSelectedChooser)); } } /** @return True if the media picker is open */ public boolean isOpen() { return mOpen; } /** * Sets the list of media types to allow the user to select * @param mediaTypes The bit flags of media types to allow. Can be any combination of the * MEDIA_TYPE_* values */ void setSupportedMediaTypes(final int mediaTypes) { mSupportedMediaTypes = mediaTypes; mEnabledChoosers.clear(); boolean selectNextChooser = false; for (final MediaChooser chooser : mChoosers) { final boolean enabled = (chooser.getSupportedMediaTypes() & mSupportedMediaTypes) != MEDIA_TYPE_NONE; if (enabled) { // TODO Add a way to inform the chooser which media types are supported mEnabledChoosers.add(chooser); if (selectNextChooser) { selectChooser(chooser); selectNextChooser = false; } } else if (mSelectedChooser == chooser) { selectNextChooser = true; } final ImageButton tabButton = chooser.getTabButton(); if (tabButton != null) { tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE); } } if (selectNextChooser && mEnabledChoosers.size() > 0) { selectChooser(mEnabledChoosers.get(0)); } final MediaChooser[] enabledChoosers = new MediaChooser[mEnabledChoosers.size()]; mEnabledChoosers.toArray(enabledChoosers); mPagerAdapter = new FixedViewPagerAdapter(enabledChoosers); if (mViewPager != null) { mViewPager.setAdapter(mPagerAdapter); } // Only rebind data if we are currently bound. Otherwise, we must have not // bound to any data yet and should wait until onCreate() to bind data. if (mBinding.isBound() && getActivity() != null) { mBinding.unbind(); mBinding.bind(DataModel.get().createMediaPickerData(getActivity())); mBinding.getData().init(getLoaderManager()); } } ViewPager getViewPager() { return mViewPager; } /** Hides the media picker, and frees up any resources it’s using */ public void dismiss(final boolean animate) { mOpen = false; if (mMediaPickerPanel != null) { mMediaPickerPanel.setExpanded(false, animate, MediaPickerPanel.PAGE_NOT_SET); } mSelectedChooser = null; } /** * Sets the listener for the media picker events * @param listener The listener which will receive events */ public void setListener(final MediaPickerListener listener) { Assert.isMainThread(); mListener = listener; mListenerHandler = listener != null ? new Handler() : null; } /** @return True if the media picker is in full-screen mode */ public boolean isFullScreen() { return mMediaPickerPanel != null && mMediaPickerPanel.isFullScreen(); } public void setFullScreen(final boolean fullScreen) { mMediaPickerPanel.setFullScreenView(fullScreen, true); } public void updateActionBar(final ActionBar actionBar) { if (getActivity() == null) { return; } if (isFullScreen() && mSelectedChooser != null) { mSelectedChooser.updateActionBar(actionBar); } else { actionBar.hide(); } } /** * Selects a new chooser * @param newSelectedChooser The newly selected chooser */ void selectChooser(final MediaChooser newSelectedChooser) { if (mSelectedChooser == newSelectedChooser) { return; } if (mSelectedChooser != null) { mSelectedChooser.setSelected(false); } mSelectedChooser = newSelectedChooser; if (mSelectedChooser != null) { mSelectedChooser.setSelected(true); } final int chooserIndex = mEnabledChoosers.indexOf(mSelectedChooser); if (mViewPager != null) { mViewPager.setCurrentItem(chooserIndex, true /* smoothScroll */); } if (isFullScreen()) { invalidateOptionsMenu(); } // Save the newly selected chooser's index so we may directly switch to it the // next time user opens the media picker. mBinding.getData().saveSelectedChooserIndex(chooserIndex); if (mMediaPickerPanel != null) { mMediaPickerPanel.onChooserChanged(); } dispatchChooserSelected(chooserIndex); } public boolean canShowIme() { if (mSelectedChooser != null) { return mSelectedChooser.canShowIme(); } return false; } public boolean onBackPressed() { return mSelectedChooser != null && mSelectedChooser.onBackPressed(); } void invalidateOptionsMenu() { ((BugleActionBarActivity) getActivity()).supportInvalidateOptionsMenu(); } void dispatchOpened() { setHasOptionsMenu(false); mOpen = true; mPagerAdapter.notifyDataSetChanged(); if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onOpened(); } }); } if (mSelectedChooser != null) { mSelectedChooser.onFullScreenChanged(false); mSelectedChooser.onOpenedChanged(true); } } void dispatchDismissed() { setHasOptionsMenu(false); mOpen = false; if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onDismissed(); } }); } if (mSelectedChooser != null) { mSelectedChooser.onOpenedChanged(false); } } void dispatchFullScreen(final boolean fullScreen) { setHasOptionsMenu(fullScreen); if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onFullScreenChanged(fullScreen); } }); } if (mSelectedChooser != null) { mSelectedChooser.onFullScreenChanged(fullScreen); } } void dispatchItemsSelected(final MessagePartData item, final boolean dismissMediaPicker) { final List items = new ArrayList(1); items.add(item); dispatchItemsSelected(items, dismissMediaPicker); } void dispatchItemsSelected(final Collection items, final boolean dismissMediaPicker) { if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onItemsSelected(items, dismissMediaPicker); } }); } if (isFullScreen() && !dismissMediaPicker) { invalidateOptionsMenu(); } } void dispatchItemUnselected(final MessagePartData item) { if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onItemUnselected(item); } }); } if (isFullScreen()) { invalidateOptionsMenu(); } } void dispatchConfirmItemSelection() { if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onConfirmItemSelection(); } }); } } void dispatchPendingItemAdded(final PendingAttachmentData pendingItem) { if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onPendingItemAdded(pendingItem); } }); } if (isFullScreen()) { invalidateOptionsMenu(); } } void dispatchChooserSelected(final int chooserIndex) { if (mListener != null) { mListenerHandler.post(new Runnable() { @Override public void run() { mListener.onChooserSelected(chooserIndex); } }); } } public boolean canSwipeDownChooser() { return mSelectedChooser == null ? false : mSelectedChooser.canSwipeDown(); } public boolean isChooserHandlingTouch() { return mSelectedChooser == null ? false : mSelectedChooser.isHandlingTouch(); } public void stopChooserTouchHandling() { if (mSelectedChooser != null) { mSelectedChooser.stopTouchHandling(); } } boolean getChooserShowsActionBarInFullScreen() { return mSelectedChooser == null ? false : mSelectedChooser.getActionBarTitleResId() != 0; } @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { if (mSelectedChooser != null) { mSelectedChooser.onCreateOptionsMenu(inflater, menu); } } @Override public boolean onOptionsItemSelected(final MenuItem item) { return (mSelectedChooser != null && mSelectedChooser.onOptionsItemSelected(item)) || super.onOptionsItemSelected(item); } PagerAdapter getPagerAdapter() { return mPagerAdapter; } public void resetViewHolderState() { mPagerAdapter.resetState(); } /** * Launch an external picker to pick item from document picker as attachment. */ public void launchDocumentPicker() { mDocumentImagePicker.launchPicker(); } public ImmutableBindingRef getMediaPickerDataBinding() { return BindingBase.createBindingReference(mBinding); } protected static final int CAMERA_PERMISSION_REQUEST_CODE = 1; protected static final int LOCATION_PERMISSION_REQUEST_CODE = 2; protected static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 3; protected static final int GALLERY_PERMISSION_REQUEST_CODE = 4; @Override public void onRequestPermissionsResult( final int requestCode, final String permissions[], final int[] grantResults) { if (mSelectedChooser != null) { mSelectedChooser.onRequestPermissionsResult(requestCode, permissions, grantResults); } } }