blob: 8e5198b354cad2ffad7510e2bc8b37c0eeb6df27 [file] [log] [blame]
/*
* 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<MessagePartData> 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<MediaChooser> 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<MediaChooser> 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<MediaPickerData> 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<DraftMessageData> mDraftMessageDataModel;
public MediaPicker() {
this(Factory.get().getApplicationContext());
}
public MediaPicker(final Context context) {
mBinding.bind(DataModel.get().createMediaPickerData(context));
mEnabledChoosers = new ArrayList<MediaChooser>();
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<DraftMessageData> draftBinding) {
mDraftMessageDataModel = Binding.createBindingReference(draftBinding);
}
public ImmutableBindingRef<DraftMessageData> 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<MediaChooser>(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<MessagePartData> items = new ArrayList<MessagePartData>(1);
items.add(item);
dispatchItemsSelected(items, dismissMediaPicker);
}
void dispatchItemsSelected(final Collection<MessagePartData> 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<MediaPickerData> 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);
}
}
}