diff options
Diffstat (limited to 'src/com/android/messaging/ui')
154 files changed, 0 insertions, 33536 deletions
diff --git a/src/com/android/messaging/ui/AsyncImageView.java b/src/com/android/messaging/ui/AsyncImageView.java deleted file mode 100644 index 9aaf0b1..0000000 --- a/src/com/android/messaging/ui/AsyncImageView.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Path; -import android.graphics.RectF; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.support.annotation.Nullable; -import android.support.rastermill.FrameSequenceDrawable; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.widget.ImageView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.binding.Binding; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.media.BindableMediaRequest; -import com.android.messaging.datamodel.media.GifImageResource; -import com.android.messaging.datamodel.media.ImageRequest; -import com.android.messaging.datamodel.media.ImageRequestDescriptor; -import com.android.messaging.datamodel.media.ImageResource; -import com.android.messaging.datamodel.media.MediaRequest; -import com.android.messaging.datamodel.media.MediaResourceManager; -import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener; -import com.android.messaging.util.Assert; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.ThreadUtil; -import com.android.messaging.util.UiUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.util.HashSet; - -/** - * An ImageView used to asynchronously request an image from MediaResourceManager and render it. - */ -public class AsyncImageView extends ImageView implements MediaResourceLoadListener<ImageResource> { - private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; - // 100ms delay before disposing the image in case the AsyncImageView is re-added to the UI - private static final int DISPOSE_IMAGE_DELAY = 100; - - // AsyncImageView has a 1-1 binding relationship with an ImageRequest instance that requests - // the image from the MediaResourceManager. Since the request is done asynchronously, we - // want to make sure the image view is always bound to the latest image request that it - // issues, so that when the image is loaded, the ImageRequest (which extends BindableData) - // will be able to figure out whether the binding is still valid and whether the loaded image - // should be delivered to the AsyncImageView via onMediaResourceLoaded() callback. - @VisibleForTesting - public final Binding<BindableMediaRequest<ImageResource>> mImageRequestBinding; - - /** True if we want the image to fade in when it loads */ - private boolean mFadeIn; - - /** True if we want the image to reveal (scale) when it loads. When set to true, this - * will take precedence over {@link #mFadeIn} */ - private final boolean mReveal; - - // The corner radius for drawing rounded corners around bitmap. The default value is zero - // (no rounded corners) - private final int mCornerRadius; - private final Path mRoundedCornerClipPath; - private int mClipPathWidth; - private int mClipPathHeight; - - // A placeholder drawable that takes the spot of the image when it's loading. The default - // setting is null (no placeholder). - private final Drawable mPlaceholderDrawable; - protected ImageResource mImageResource; - private final Runnable mDisposeRunnable = new Runnable() { - @Override - public void run() { - if (mImageRequestBinding.isBound()) { - mDetachedRequestDescriptor = (ImageRequestDescriptor) - mImageRequestBinding.getData().getDescriptor(); - } - unbindView(); - releaseImageResource(); - } - }; - - private AsyncImageViewDelayLoader mDelayLoader; - private ImageRequestDescriptor mDetachedRequestDescriptor; - - public AsyncImageView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mImageRequestBinding = BindingBase.createBinding(this); - final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.AsyncImageView, - 0, 0); - mFadeIn = attr.getBoolean(R.styleable.AsyncImageView_fadeIn, true); - mReveal = attr.getBoolean(R.styleable.AsyncImageView_reveal, false); - mPlaceholderDrawable = attr.getDrawable(R.styleable.AsyncImageView_placeholderDrawable); - mCornerRadius = attr.getDimensionPixelSize(R.styleable.AsyncImageView_cornerRadius, 0); - mRoundedCornerClipPath = new Path(); - - attr.recycle(); - } - - /** - * The main entrypoint for AsyncImageView to load image resource given an ImageRequestDescriptor - * @param descriptor the request descriptor, or null if no image should be displayed - */ - public void setImageResourceId(@Nullable final ImageRequestDescriptor descriptor) { - final String requestKey = (descriptor == null) ? null : descriptor.getKey(); - if (mImageRequestBinding.isBound()) { - if (TextUtils.equals(mImageRequestBinding.getData().getKey(), requestKey)) { - // Don't re-request the bitmap if the new request is for the same resource. - return; - } - unbindView(); - } - setImage(null); - resetTransientViewStates(); - if (!TextUtils.isEmpty(requestKey)) { - maybeSetupPlaceholderDrawable(descriptor); - final BindableMediaRequest<ImageResource> imageRequest = - descriptor.buildAsyncMediaRequest(getContext(), this); - requestImage(imageRequest); - } - } - - /** - * Sets a delay loader that centrally manages image request delay loading logic. - */ - public void setDelayLoader(final AsyncImageViewDelayLoader delayLoader) { - Assert.isTrue(mDelayLoader == null); - mDelayLoader = delayLoader; - } - - /** - * Called by the delay loader when we can resume image loading. - */ - public void resumeLoading() { - Assert.notNull(mDelayLoader); - Assert.isTrue(mImageRequestBinding.isBound()); - MediaResourceManager.get().requestMediaResourceAsync(mImageRequestBinding.getData()); - } - - /** - * Setup the placeholder drawable if: - * 1. There's an image to be loaded AND - * 2. We are given a placeholder drawable AND - * 3. The descriptor provided us with source width and height. - */ - private void maybeSetupPlaceholderDrawable(final ImageRequestDescriptor descriptor) { - if (!TextUtils.isEmpty(descriptor.getKey()) && mPlaceholderDrawable != null) { - if (descriptor.sourceWidth != ImageRequest.UNSPECIFIED_SIZE && - descriptor.sourceHeight != ImageRequest.UNSPECIFIED_SIZE) { - // Set a transparent inset drawable to the foreground so it will mimick the final - // size of the image, and use the background to show the actual placeholder - // drawable. - setImageDrawable(PlaceholderInsetDrawable.fromDrawable( - new ColorDrawable(Color.TRANSPARENT), - descriptor.sourceWidth, descriptor.sourceHeight)); - } - setBackground(mPlaceholderDrawable); - } - } - - protected void setImage(final ImageResource resource) { - setImage(resource, false /* isCached */); - } - - protected void setImage(final ImageResource resource, final boolean isCached) { - // Switch reference to the new ImageResource. Make sure we release the current - // resource and addRef() on the new resource so that the underlying bitmaps don't - // get leaked or get recycled by the bitmap cache. - releaseImageResource(); - // Ensure that any pending dispose runnables get removed. - ThreadUtil.getMainThreadHandler().removeCallbacks(mDisposeRunnable); - // The drawable may require work to get if its a static object so try to only make this call - // once. - final Drawable drawable = (resource != null) ? resource.getDrawable(getResources()) : null; - if (drawable != null) { - mImageResource = resource; - mImageResource.addRef(); - setImageDrawable(drawable); - if (drawable instanceof FrameSequenceDrawable) { - ((FrameSequenceDrawable) drawable).start(); - } - - if (getVisibility() == VISIBLE) { - if (mReveal) { - setVisibility(INVISIBLE); - UiUtils.revealOrHideViewWithAnimation(this, VISIBLE, null); - } else if (mFadeIn && !isCached) { - // Hide initially to avoid flash. - setAlpha(0F); - animate().alpha(1F).start(); - } - } - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - if (mImageResource instanceof GifImageResource) { - LogUtil.v(TAG, "setImage size unknown -- it's a GIF"); - } else { - LogUtil.v(TAG, "setImage size: " + mImageResource.getMediaSize() + - " width: " + mImageResource.getBitmap().getWidth() + - " heigh: " + mImageResource.getBitmap().getHeight()); - } - } - } - invalidate(); - } - - private void requestImage(final BindableMediaRequest<ImageResource> request) { - mImageRequestBinding.bind(request); - if (mDelayLoader == null || !mDelayLoader.isDelayLoadingImage()) { - MediaResourceManager.get().requestMediaResourceAsync(request); - } else { - mDelayLoader.registerView(this); - } - } - - @Override - public void onMediaResourceLoaded(final MediaRequest<ImageResource> request, - final ImageResource resource, final boolean isCached) { - if (mImageResource != resource) { - setImage(resource, isCached); - } - } - - @Override - public void onMediaResourceLoadError( - final MediaRequest<ImageResource> request, final Exception exception) { - // Media load failed, unbind and reset bitmap to default. - unbindView(); - setImage(null); - } - - private void releaseImageResource() { - final Drawable drawable = getDrawable(); - if (drawable instanceof FrameSequenceDrawable) { - ((FrameSequenceDrawable) drawable).stop(); - ((FrameSequenceDrawable) drawable).destroy(); - } - if (mImageResource != null) { - mImageResource.release(); - mImageResource = null; - } - setImageDrawable(null); - setBackground(null); - } - - /** - * Resets transient view states (eg. alpha, animations) before rebinding/reusing the view. - */ - private void resetTransientViewStates() { - clearAnimation(); - setAlpha(1F); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - // If it was recently removed, then cancel disposing, we're still using it. - ThreadUtil.getMainThreadHandler().removeCallbacks(mDisposeRunnable); - - // When the image view gets detached and immediately re-attached, any fade-in animation - // will be terminated, leaving the view in a semi-transparent state. Make sure we restore - // alpha when the view is re-attached. - if (mFadeIn) { - setAlpha(1F); - } - - // Check whether we are in a simple reuse scenario: detached from window, and reattached - // later without rebinding. This may be done by containers such as the RecyclerView to - // reuse the views. In this case, we would like to rebind the original image request. - if (!mImageRequestBinding.isBound() && mDetachedRequestDescriptor != null) { - setImageResourceId(mDetachedRequestDescriptor); - } - mDetachedRequestDescriptor = null; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - // Dispose the bitmap, but if an AysncImageView is removed from the window, then quickly - // re-added, we shouldn't dispose, so wait a short time before disposing - ThreadUtil.getMainThreadHandler().postDelayed(mDisposeRunnable, DISPOSE_IMAGE_DELAY); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - // The base implementation does not honor the minimum sizes. We try to to honor it here. - - final int measuredWidth = getMeasuredWidth(); - final int measuredHeight = getMeasuredHeight(); - if (measuredWidth >= getMinimumWidth() || measuredHeight >= getMinimumHeight()) { - // We are ok if either of the minimum sizes is honored. Note that satisfying both the - // sizes may not be possible, depending on the aspect ratio of the image and whether - // a maximum size has been specified. This implementation only tries to handle the case - // where both the minimum sizes are not being satisfied. - return; - } - - if (!getAdjustViewBounds()) { - // The base implementation is reasonable in this case. If the view bounds cannot be - // changed, it is not possible to satisfy the minimum sizes anyway. - return; - } - - final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { - // The base implementation is reasonable in this case. - return; - } - - int width = measuredWidth; - int height = measuredHeight; - // Get the minimum sizes that will honor other constraints as well. - final int minimumWidth = resolveSize( - getMinimumWidth(), getMaxWidth(), widthMeasureSpec); - final int minimumHeight = resolveSize( - getMinimumHeight(), getMaxHeight(), heightMeasureSpec); - final float aspectRatio = measuredWidth / (float) measuredHeight; - if (aspectRatio == 0) { - // If the image is (close to) infinitely high, there is not much we can do. - return; - } - - if (width < minimumWidth) { - height = resolveSize((int) (minimumWidth / aspectRatio), - getMaxHeight(), heightMeasureSpec); - width = (int) (height * aspectRatio); - } - - if (height < minimumHeight) { - width = resolveSize((int) (minimumHeight * aspectRatio), - getMaxWidth(), widthMeasureSpec); - height = (int) (width / aspectRatio); - } - - setMeasuredDimension(width, height); - } - - private static int resolveSize(int desiredSize, int maxSize, int measureSpec) { - final int specMode = MeasureSpec.getMode(measureSpec); - final int specSize = MeasureSpec.getSize(measureSpec); - switch(specMode) { - case MeasureSpec.UNSPECIFIED: - return Math.min(desiredSize, maxSize); - - case MeasureSpec.AT_MOST: - return Math.min(Math.min(desiredSize, specSize), maxSize); - - default: - Assert.fail("Unreachable"); - return specSize; - } - } - - @Override - protected void onDraw(final Canvas canvas) { - if (mCornerRadius > 0) { - final int currentWidth = this.getWidth(); - final int currentHeight = this.getHeight(); - if (mClipPathWidth != currentWidth || mClipPathHeight != currentHeight) { - final RectF rect = new RectF(0, 0, currentWidth, currentHeight); - mRoundedCornerClipPath.reset(); - mRoundedCornerClipPath.addRoundRect(rect, mCornerRadius, mCornerRadius, - Path.Direction.CW); - mClipPathWidth = currentWidth; - mClipPathHeight = currentHeight; - } - - final int saveCount = canvas.getSaveCount(); - canvas.save(); - canvas.clipPath(mRoundedCornerClipPath); - super.onDraw(canvas); - canvas.restoreToCount(saveCount); - } else { - super.onDraw(canvas); - } - } - - private void unbindView() { - if (mImageRequestBinding.isBound()) { - mImageRequestBinding.unbind(); - if (mDelayLoader != null) { - mDelayLoader.unregisterView(this); - } - } - } - - /** - * As a performance optimization, the consumer of the AsyncImageView may opt to delay loading - * the image when it's busy doing other things (such as when a list view is scrolling). In - * order to do this, the consumer can create a new AsyncImageViewDelayLoader instance to be - * shared among all relevant AsyncImageViews (through setDelayLoader() method), and call - * onStartDelayLoading() and onStopDelayLoading() to start and stop delay loading, respectively. - */ - public static class AsyncImageViewDelayLoader { - private boolean mShouldDelayLoad; - private final HashSet<AsyncImageView> mAttachedViews; - - public AsyncImageViewDelayLoader() { - mAttachedViews = new HashSet<AsyncImageView>(); - } - - private void registerView(final AsyncImageView view) { - mAttachedViews.add(view); - } - - private void unregisterView(final AsyncImageView view) { - mAttachedViews.remove(view); - } - - public boolean isDelayLoadingImage() { - return mShouldDelayLoad; - } - - /** - * Called by the consumer of this view to delay loading images - */ - public void onDelayLoading() { - // Don't need to explicitly tell the AsyncImageView to stop loading since - // ImageRequests are not cancellable. - mShouldDelayLoad = true; - } - - /** - * Called by the consumer of this view to resume loading images - */ - public void onResumeLoading() { - if (mShouldDelayLoad) { - mShouldDelayLoad = false; - - // Notify all attached views to resume loading. - for (final AsyncImageView view : mAttachedViews) { - view.resumeLoading(); - } - mAttachedViews.clear(); - } - } - } -} diff --git a/src/com/android/messaging/ui/AttachmentPreview.java b/src/com/android/messaging/ui/AttachmentPreview.java deleted file mode 100644 index 7eea14b..0000000 --- a/src/com/android/messaging/ui/AttachmentPreview.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * 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; - -import android.animation.Animator; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ScrollView; - -import com.android.messaging.R; -import com.android.messaging.annotation.VisibleForAnimation; -import com.android.messaging.datamodel.data.DraftMessageData; -import com.android.messaging.datamodel.data.MediaPickerMessagePartData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.PendingAttachmentData; -import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener; -import com.android.messaging.ui.animation.PopupTransitionAnimation; -import com.android.messaging.ui.conversation.ComposeMessageView; -import com.android.messaging.ui.conversation.ConversationFragment; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ThreadUtil; -import com.android.messaging.util.UiUtils; - -import java.util.ArrayList; -import java.util.List; - -public class AttachmentPreview extends ScrollView implements OnAttachmentClickListener { - private FrameLayout mAttachmentView; - private ComposeMessageView mComposeMessageView; - private ImageButton mCloseButton; - private int mAnimatedHeight = -1; - private Animator mCloseGapAnimator; - private boolean mPendingFirstUpdate; - private Handler mHandler; - private Runnable mHideRunnable; - private boolean mPendingHideCanceled; - - private static final int CLOSE_BUTTON_REVEAL_STAGGER_MILLIS = 300; - - public AttachmentPreview(final Context context, final AttributeSet attrs) { - super(context, attrs); - mHandler = new Handler(Looper.getMainLooper()); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mCloseButton = (ImageButton) findViewById(R.id.close_button); - mCloseButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - mComposeMessageView.clearAttachments(); - } - }); - - mAttachmentView = (FrameLayout) findViewById(R.id.attachment_view); - - // The attachment preview is a scroll view so that it can show the bottom portion of the - // attachment whenever the space is tight (e.g. when in landscape mode). Per design - // request we'd like to make the attachment view always scrolled to the bottom. - addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(final View v, final int left, final int top, final int right, - final int bottom, final int oldLeft, final int oldTop, final int oldRight, - final int oldBottom) { - post(new Runnable() { - @Override - public void run() { - final int childCount = getChildCount(); - if (childCount > 0) { - final View lastChild = getChildAt(childCount - 1); - scrollTo(getScrollX(), lastChild.getBottom() - getHeight()); - } - } - }); - } - }); - mPendingFirstUpdate = true; - } - - public void setComposeMessageView(final ComposeMessageView composeMessageView) { - mComposeMessageView = composeMessageView; - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mAnimatedHeight >= 0) { - setMeasuredDimension(getMeasuredWidth(), mAnimatedHeight); - } - } - - private void cancelPendingHide() { - mPendingHideCanceled = true; - } - - public void hideAttachmentPreview() { - if (getVisibility() != GONE) { - UiUtils.revealOrHideViewWithAnimation(mCloseButton, GONE, - null /* onFinishRunnable */); - startCloseGapAnimationOnAttachmentClear(); - - if (mAttachmentView.getChildCount() > 0) { - mPendingHideCanceled = false; - final View viewToHide = mAttachmentView.getChildCount() > 1 ? - mAttachmentView : mAttachmentView.getChildAt(0); - UiUtils.revealOrHideViewWithAnimation(viewToHide, INVISIBLE, - new Runnable() { - @Override - public void run() { - // Only hide if we are didn't get overruled by showing - if (!mPendingHideCanceled) { - mAttachmentView.removeAllViews(); - setVisibility(GONE); - } - } - }); - } else { - mAttachmentView.removeAllViews(); - setVisibility(GONE); - } - } - } - - // returns true if we have attachments - public boolean onAttachmentsChanged(final DraftMessageData draftMessageData) { - final boolean isFirstUpdate = mPendingFirstUpdate; - final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments(); - final List<PendingAttachmentData> pendingAttachments = - draftMessageData.getReadOnlyPendingAttachments(); - - // Any change in attachments would invalidate the animated height animation. - cancelCloseGapAnimation(); - mPendingFirstUpdate = false; - - final int combinedAttachmentCount = attachments.size() + pendingAttachments.size(); - mCloseButton.setContentDescription(getResources() - .getQuantityString(R.plurals.attachment_preview_close_content_description, - combinedAttachmentCount)); - if (combinedAttachmentCount == 0) { - mHideRunnable = new Runnable() { - @Override - public void run() { - mHideRunnable = null; - // Only start the hiding if there are still no attachments - if (attachments.size() + pendingAttachments.size() == 0) { - hideAttachmentPreview(); - } - } - }; - if (draftMessageData.isSending()) { - // Wait to hide until the message is ready to start animating - // We'll execute immediately when the animation triggers - mHandler.postDelayed(mHideRunnable, - ConversationFragment.MESSAGE_ANIMATION_MAX_WAIT); - } else { - // Run immediately when clearing attachments - mHideRunnable.run(); - } - return false; - } - - cancelPendingHide(); // We're showing - if (getVisibility() != VISIBLE) { - setVisibility(VISIBLE); - mAttachmentView.setVisibility(VISIBLE); - - // Don't animate in the close button if this is the first update after view creation. - // This is the initial draft load from database for pre-existing drafts. - if (!isFirstUpdate) { - // Reveal the close button after the view animates in. - mCloseButton.setVisibility(INVISIBLE); - ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() { - @Override - public void run() { - UiUtils.revealOrHideViewWithAnimation(mCloseButton, VISIBLE, - null /* onFinishRunnable */); - } - }, UiUtils.MEDIAPICKER_TRANSITION_DURATION + CLOSE_BUTTON_REVEAL_STAGGER_MILLIS); - } - } - - // Merge the pending attachment list with real attachment. Design would prefer these be - // in LIFO order user can see added images past the 5th one but we also want them to be in - // order and we want it to be WYSIWYG. - final List<MessagePartData> combinedAttachments = new ArrayList<>(); - combinedAttachments.addAll(attachments); - combinedAttachments.addAll(pendingAttachments); - - final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); - if (combinedAttachmentCount > 1) { - MultiAttachmentLayout multiAttachmentLayout = null; - Rect transitionRect = null; - if (mAttachmentView.getChildCount() > 0) { - final View firstChild = mAttachmentView.getChildAt(0); - if (firstChild instanceof MultiAttachmentLayout) { - Assert.equals(1, mAttachmentView.getChildCount()); - multiAttachmentLayout = (MultiAttachmentLayout) firstChild; - multiAttachmentLayout.bindAttachments(combinedAttachments, - null /* transitionRect */, combinedAttachmentCount); - } else { - transitionRect = new Rect(firstChild.getLeft(), firstChild.getTop(), - firstChild.getRight(), firstChild.getBottom()); - } - } - if (multiAttachmentLayout == null) { - multiAttachmentLayout = AttachmentPreviewFactory.createMultiplePreview( - getContext(), this); - multiAttachmentLayout.bindAttachments(combinedAttachments, transitionRect, - combinedAttachmentCount); - mAttachmentView.removeAllViews(); - mAttachmentView.addView(multiAttachmentLayout); - } - } else { - final MessagePartData attachment = combinedAttachments.get(0); - boolean shouldAnimate = true; - if (mAttachmentView.getChildCount() > 0) { - // If we are going from N->1 attachments, try to use the current bounds - // bounds as the starting rect. - shouldAnimate = false; - final View firstChild = mAttachmentView.getChildAt(0); - if (firstChild instanceof MultiAttachmentLayout && - attachment instanceof MediaPickerMessagePartData) { - final View leftoverView = ((MultiAttachmentLayout) firstChild) - .findViewForAttachment(attachment); - if (leftoverView != null) { - final Rect currentRect = UiUtils.getMeasuredBoundsOnScreen(leftoverView); - if (!currentRect.isEmpty() && - attachment instanceof MediaPickerMessagePartData) { - ((MediaPickerMessagePartData) attachment).setStartRect(currentRect); - shouldAnimate = true; - } - } - } - } - mAttachmentView.removeAllViews(); - final View attachmentView = AttachmentPreviewFactory.createAttachmentPreview( - layoutInflater, attachment, mAttachmentView, - AttachmentPreviewFactory.TYPE_SINGLE, true /* startImageRequest */, this); - if (attachmentView != null) { - mAttachmentView.addView(attachmentView); - if (shouldAnimate) { - tryAnimateViewIn(attachment, attachmentView); - } - } - } - return true; - } - - public void onMessageAnimationStart() { - if (mHideRunnable == null) { - return; - } - - // Run the hide animation at the same time as the message animation - mHandler.removeCallbacks(mHideRunnable); - setVisibility(View.INVISIBLE); - mHideRunnable.run(); - } - - static void tryAnimateViewIn(final MessagePartData attachmentData, final View view) { - if (attachmentData instanceof MediaPickerMessagePartData) { - final Rect startRect = ((MediaPickerMessagePartData) attachmentData).getStartRect(); - new PopupTransitionAnimation(startRect, view).startAfterLayoutComplete(); - } - } - - @VisibleForAnimation - public void setAnimatedHeight(final int animatedHeight) { - if (mAnimatedHeight != animatedHeight) { - mAnimatedHeight = animatedHeight; - requestLayout(); - } - } - - /** - * Kicks off an animation to animate the layout change for closing the gap between the - * message list and the compose message box when the attachments are cleared. - */ - private void startCloseGapAnimationOnAttachmentClear() { - // Cancel existing animation. - cancelCloseGapAnimation(); - mCloseGapAnimator = ObjectAnimator.ofInt(this, "animatedHeight", getHeight(), 0); - mCloseGapAnimator.start(); - } - - private void cancelCloseGapAnimation() { - if (mCloseGapAnimator != null) { - mCloseGapAnimator.cancel(); - mCloseGapAnimator = null; - } - mAnimatedHeight = -1; - } - - @Override - public boolean onAttachmentClick(final MessagePartData attachment, - final Rect viewBoundsOnScreen, final boolean longPress) { - if (longPress) { - mComposeMessageView.onAttachmentPreviewLongClicked(); - return true; - } - - if (!(attachment instanceof PendingAttachmentData) && attachment.isImage()) { - mComposeMessageView.displayPhoto(attachment.getContentUri(), viewBoundsOnScreen); - return true; - } - return false; - } -} diff --git a/src/com/android/messaging/ui/AttachmentPreviewFactory.java b/src/com/android/messaging/ui/AttachmentPreviewFactory.java deleted file mode 100644 index ed5d4d7..0000000 --- a/src/com/android/messaging/ui/AttachmentPreviewFactory.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Rect; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnLongClickListener; -import android.view.ViewGroup; -import android.widget.FrameLayout.LayoutParams; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.PendingAttachmentData; -import com.android.messaging.datamodel.data.PersonItemData; -import com.android.messaging.datamodel.data.VCardContactItemData; -import com.android.messaging.datamodel.media.FileImageRequestDescriptor; -import com.android.messaging.datamodel.media.ImageRequest; -import com.android.messaging.datamodel.media.ImageRequestDescriptor; -import com.android.messaging.datamodel.media.UriImageRequestDescriptor; -import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener; -import com.android.messaging.ui.PersonItemView.PersonItemViewListener; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.ImageUtils; -import com.android.messaging.util.UiUtils; -import com.android.messaging.util.UriUtil; - -/** - * A view factory that creates previews for single/multiple attachments. - */ -public class AttachmentPreviewFactory { - /** Standalone attachment preview */ - public static final int TYPE_SINGLE = 1; - - /** Attachment preview displayed in a multi-attachment layout */ - public static final int TYPE_MULTIPLE = 2; - - /** Attachment preview displayed in the attachment chooser grid view */ - public static final int TYPE_CHOOSER_GRID = 3; - - public static View createAttachmentPreview(final LayoutInflater layoutInflater, - final MessagePartData attachmentData, final ViewGroup parent, - final int viewType, final boolean startImageRequest, - @Nullable final OnAttachmentClickListener clickListener) { - final String contentType = attachmentData.getContentType(); - View attachmentView = null; - if (attachmentData instanceof PendingAttachmentData) { - attachmentView = createPendingAttachmentPreview(layoutInflater, parent, - (PendingAttachmentData) attachmentData); - } else if (ContentType.isImageType(contentType)) { - attachmentView = createImagePreview(layoutInflater, attachmentData, parent, viewType, - startImageRequest); - } else if (ContentType.isAudioType(contentType)) { - attachmentView = createAudioPreview(layoutInflater, attachmentData, parent, viewType); - } else if (ContentType.isVideoType(contentType)) { - attachmentView = createVideoPreview(layoutInflater, attachmentData, parent, viewType); - } else if (ContentType.isVCardType(contentType)) { - attachmentView = createVCardPreview(layoutInflater, attachmentData, parent, viewType); - } else { - Assert.fail("unsupported attachment type: " + contentType); - return null; - } - - // Some views have a caption, set the text/visibility if one exists - final TextView captionView = (TextView) attachmentView.findViewById(R.id.caption); - if (captionView != null) { - final String caption = attachmentData.getText(); - captionView.setVisibility(TextUtils.isEmpty(caption) ? View.GONE : View.VISIBLE); - captionView.setText(caption); - } - - if (attachmentView != null && clickListener != null) { - attachmentView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); - clickListener.onAttachmentClick(attachmentData, bounds, - false /* longPress */); - } - }); - attachmentView.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(final View view) { - final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); - return clickListener.onAttachmentClick(attachmentData, bounds, - true /* longPress */); - } - }); - } - return attachmentView; - } - - public static MultiAttachmentLayout createMultiplePreview(final Context context, - final OnAttachmentClickListener listener) { - final MultiAttachmentLayout multiAttachmentLayout = - new MultiAttachmentLayout(context, null); - final ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - multiAttachmentLayout.setLayoutParams(layoutParams); - multiAttachmentLayout.setOnAttachmentClickListener(listener); - return multiAttachmentLayout; - } - - public static ImageRequestDescriptor getImageRequestDescriptorForAttachment( - final MessagePartData attachmentData, final int desiredWidth, final int desiredHeight) { - final Uri uri = attachmentData.getContentUri(); - final String contentType = attachmentData.getContentType(); - if (ContentType.isImageType(contentType)) { - final String filePath = UriUtil.getFilePathFromUri(uri); - if (filePath != null) { - return new FileImageRequestDescriptor(filePath, desiredWidth, desiredHeight, - attachmentData.getWidth(), attachmentData.getHeight(), - false /* canUseThumbnail */, true /* allowCompression */, - false /* isStatic */); - } else { - return new UriImageRequestDescriptor(uri, desiredWidth, desiredHeight, - attachmentData.getWidth(), attachmentData.getHeight(), - true /* allowCompression */, false /* isStatic */, false /*cropToCircle*/, - ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */, - ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */); - } - } - return null; - } - - private static View createImagePreview(final LayoutInflater layoutInflater, - final MessagePartData attachmentData, final ViewGroup parent, - final int viewType, final boolean startImageRequest) { - int layoutId = R.layout.attachment_single_image; - switch (viewType) { - case AttachmentPreviewFactory.TYPE_SINGLE: - layoutId = R.layout.attachment_single_image; - break; - case AttachmentPreviewFactory.TYPE_MULTIPLE: - layoutId = R.layout.attachment_multiple_image; - break; - case AttachmentPreviewFactory.TYPE_CHOOSER_GRID: - layoutId = R.layout.attachment_chooser_image; - break; - default: - Assert.fail("unsupported attachment view type!"); - break; - } - final View view = layoutInflater.inflate(layoutId, parent, false /* attachToRoot */); - final AsyncImageView imageView = (AsyncImageView) view.findViewById( - R.id.attachment_image_view); - int maxWidth = imageView.getMaxWidth(); - int maxHeight = imageView.getMaxHeight(); - if (viewType == TYPE_CHOOSER_GRID) { - final Resources resources = layoutInflater.getContext().getResources(); - maxWidth = maxHeight = resources.getDimensionPixelSize( - R.dimen.attachment_grid_image_cell_size); - } - if (maxWidth <= 0 || maxWidth == Integer.MAX_VALUE) { - maxWidth = ImageRequest.UNSPECIFIED_SIZE; - } - if (maxHeight <= 0 || maxHeight == Integer.MAX_VALUE) { - maxHeight = ImageRequest.UNSPECIFIED_SIZE; - } - if (startImageRequest) { - imageView.setImageResourceId(getImageRequestDescriptorForAttachment(attachmentData, - maxWidth, maxHeight)); - } - imageView.setContentDescription( - parent.getResources().getString(R.string.message_image_content_description)); - return view; - } - - private static View createPendingAttachmentPreview(final LayoutInflater layoutInflater, - final ViewGroup parent, final PendingAttachmentData attachmentData) { - final View pendingItemView = layoutInflater.inflate(R.layout.attachment_pending_item, - parent, false); - final ImageView imageView = (ImageView) - pendingItemView.findViewById(R.id.pending_item_view); - final ViewGroup.LayoutParams layoutParams = imageView.getLayoutParams(); - final int defaultSize = layoutInflater.getContext().getResources().getDimensionPixelSize( - R.dimen.pending_attachment_size); - layoutParams.width = attachmentData.getWidth() == MessagePartData.UNSPECIFIED_SIZE ? - defaultSize : attachmentData.getWidth(); - layoutParams.height = attachmentData.getHeight() == MessagePartData.UNSPECIFIED_SIZE ? - defaultSize : attachmentData.getHeight(); - return pendingItemView; - } - - private static View createVCardPreview(final LayoutInflater layoutInflater, - final MessagePartData attachmentData, final ViewGroup parent, - final int viewType) { - int layoutId = R.layout.attachment_single_vcard; - switch (viewType) { - case AttachmentPreviewFactory.TYPE_SINGLE: - layoutId = R.layout.attachment_single_vcard; - break; - case AttachmentPreviewFactory.TYPE_MULTIPLE: - layoutId = R.layout.attachment_multiple_vcard; - break; - case AttachmentPreviewFactory.TYPE_CHOOSER_GRID: - layoutId = R.layout.attachment_chooser_vcard; - break; - default: - Assert.fail("unsupported attachment view type!"); - break; - } - final View view = layoutInflater.inflate(layoutId, parent, false /* attachToRoot */); - final PersonItemView vcardPreview = (PersonItemView) view.findViewById( - R.id.vcard_attachment_view); - vcardPreview.setAvatarOnly(viewType != AttachmentPreviewFactory.TYPE_SINGLE); - vcardPreview.bind(DataModel.get().createVCardContactItemData(layoutInflater.getContext(), - attachmentData)); - vcardPreview.setListener(new PersonItemViewListener() { - @Override - public void onPersonClicked(final PersonItemData data) { - Assert.isTrue(data instanceof VCardContactItemData); - final VCardContactItemData vCardData = (VCardContactItemData) data; - if (vCardData.hasValidVCard()) { - final Uri vCardUri = vCardData.getVCardUri(); - UIIntents.get().launchVCardDetailActivity(vcardPreview.getContext(), vCardUri); - } - } - - @Override - public boolean onPersonLongClicked(final PersonItemData data) { - return false; - } - }); - return view; - } - - private static View createAudioPreview(final LayoutInflater layoutInflater, - final MessagePartData attachmentData, final ViewGroup parent, - final int viewType) { - int layoutId = R.layout.attachment_single_audio; - switch (viewType) { - case AttachmentPreviewFactory.TYPE_SINGLE: - layoutId = R.layout.attachment_single_audio; - break; - case AttachmentPreviewFactory.TYPE_MULTIPLE: - layoutId = R.layout.attachment_multiple_audio; - break; - case AttachmentPreviewFactory.TYPE_CHOOSER_GRID: - layoutId = R.layout.attachment_chooser_audio; - break; - default: - Assert.fail("unsupported attachment view type!"); - break; - } - final View view = layoutInflater.inflate(layoutId, parent, false /* attachToRoot */); - final AudioAttachmentView audioView = (AudioAttachmentView) - view.findViewById(R.id.audio_attachment_view); - audioView.bindMessagePartData(attachmentData, false /* incoming */); - return view; - } - - private static View createVideoPreview(final LayoutInflater layoutInflater, - final MessagePartData attachmentData, final ViewGroup parent, - final int viewType) { - int layoutId = R.layout.attachment_single_video; - switch (viewType) { - case AttachmentPreviewFactory.TYPE_SINGLE: - layoutId = R.layout.attachment_single_video; - break; - case AttachmentPreviewFactory.TYPE_MULTIPLE: - layoutId = R.layout.attachment_multiple_video; - break; - case AttachmentPreviewFactory.TYPE_CHOOSER_GRID: - layoutId = R.layout.attachment_chooser_video; - break; - default: - Assert.fail("unsupported attachment view type!"); - break; - } - final VideoThumbnailView videoThumbnail = (VideoThumbnailView) layoutInflater.inflate( - layoutId, parent, false /* attachToRoot */); - videoThumbnail.setSource(attachmentData, false /* incomingMessage */); - return videoThumbnail; - } -} diff --git a/src/com/android/messaging/ui/AudioAttachmentPlayPauseButton.java b/src/com/android/messaging/ui/AudioAttachmentPlayPauseButton.java deleted file mode 100644 index 724c4fe..0000000 --- a/src/com/android/messaging/ui/AudioAttachmentPlayPauseButton.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.ImageView; -import android.widget.ViewSwitcher; - -import com.android.messaging.R; - -/** - * Shows a tinted play pause button. - */ -public class AudioAttachmentPlayPauseButton extends ViewSwitcher { - private ImageView mPlayButton; - private ImageView mPauseButton; - - private boolean mShowAsIncoming; - - public AudioAttachmentPlayPauseButton(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mPlayButton = (ImageView) findViewById(R.id.play_button); - mPauseButton = (ImageView) findViewById(R.id.pause_button); - updateAppearance(); - } - - public void setVisualStyle(final boolean showAsIncoming) { - if (mShowAsIncoming != showAsIncoming) { - mShowAsIncoming = showAsIncoming; - updateAppearance(); - } - } - - private void updateAppearance() { - mPlayButton.setImageDrawable(ConversationDrawables.get() - .getPlayButtonDrawable(mShowAsIncoming)); - mPauseButton.setImageDrawable(ConversationDrawables.get() - .getPauseButtonDrawable(mShowAsIncoming)); - } -} diff --git a/src/com/android/messaging/ui/AudioAttachmentView.java b/src/com/android/messaging/ui/AudioAttachmentView.java deleted file mode 100644 index bb649b0..0000000 --- a/src/com/android/messaging/ui/AudioAttachmentView.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Path; -import android.graphics.RectF; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.media.MediaPlayer.OnCompletionListener; -import android.media.MediaPlayer.OnErrorListener; -import android.media.MediaPlayer.OnPreparedListener; -import android.net.Uri; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.ui.mediapicker.PausableChronometer; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.UiUtils; - -/** - * A reusable widget that hosts an audio player for audio attachment playback. This widget is used - * by both the media picker and the conversation message view to show audio attachments. - */ -public class AudioAttachmentView extends LinearLayout { - /** The normal layout mode where we have the play button, timer and progress bar */ - private static final int LAYOUT_MODE_NORMAL = 0; - - /** The compact layout mode with only the play button and the timer beneath it. Suitable - * for displaying in limited space such as multi-attachment layout */ - private static final int LAYOUT_MODE_COMPACT = 1; - - /** The sub-compact layout mode with only the play button. */ - private static final int LAYOUT_MODE_SUB_COMPACT = 2; - - private static final int PLAY_BUTTON = 0; - private static final int PAUSE_BUTTON = 1; - - private AudioAttachmentPlayPauseButton mPlayPauseButton; - private PausableChronometer mChronometer; - private AudioPlaybackProgressBar mProgressBar; - private MediaPlayer mMediaPlayer; - - private Uri mDataSourceUri; - - // The corner radius for drawing rounded corners. The default value is zero (no rounded corners) - private final int mCornerRadius; - private final Path mRoundedCornerClipPath; - private int mClipPathWidth; - private int mClipPathHeight; - - // Indicates whether the attachment view is to be styled as a part of an incoming message. - private boolean mShowAsIncoming; - - private boolean mPrepared; - private boolean mPlaybackFinished; - private final int mMode; - - public AudioAttachmentView(final Context context, final AttributeSet attrs) { - super(context, attrs); - final TypedArray typedAttributes = - context.obtainStyledAttributes(attrs, R.styleable.AudioAttachmentView); - mMode = typedAttributes.getInt(R.styleable.AudioAttachmentView_layoutMode, - LAYOUT_MODE_NORMAL); - final LayoutInflater inflater = LayoutInflater.from(getContext()); - inflater.inflate(R.layout.audio_attachment_view, this, true); - typedAttributes.recycle(); - - setWillNotDraw(mMode != LAYOUT_MODE_SUB_COMPACT); - mRoundedCornerClipPath = new Path(); - mCornerRadius = context.getResources().getDimensionPixelSize( - R.dimen.conversation_list_image_preview_corner_radius); - setContentDescription(context.getString(R.string.audio_attachment_content_description)); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mPlayPauseButton = (AudioAttachmentPlayPauseButton) findViewById(R.id.play_pause_button); - mChronometer = (PausableChronometer) findViewById(R.id.timer); - mProgressBar = (AudioPlaybackProgressBar) findViewById(R.id.progress); - mPlayPauseButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - setupMediaPlayer(); - if (mMediaPlayer != null && mPrepared) { - if (mMediaPlayer.isPlaying()) { - mMediaPlayer.pause(); - mChronometer.pause(); - mProgressBar.pause(); - } else { - playAudio(); - } - } - updatePlayPauseButtonState(); - } - }); - updatePlayPauseButtonState(); - initializeViewsForMode(); - } - - /** - * Bind the audio attachment view with a MessagePartData. - * @param incoming indicates whether the attachment view is to be styled as a part of an - * incoming message. - */ - public void bindMessagePartData(final MessagePartData messagePartData, - final boolean incoming) { - Assert.isTrue(messagePartData == null || - ContentType.isAudioType(messagePartData.getContentType())); - final Uri contentUri = (messagePartData == null) ? null : messagePartData.getContentUri(); - bind(contentUri, incoming); - } - - public void bind(final Uri dataSourceUri, final boolean incoming) { - final String currentUriString = (mDataSourceUri == null) ? "" : mDataSourceUri.toString(); - final String newUriString = (dataSourceUri == null) ? "" : dataSourceUri.toString(); - mShowAsIncoming = incoming; - if (!TextUtils.equals(currentUriString, newUriString)) { - mDataSourceUri = dataSourceUri; - resetToZeroState(); - } - } - - private void playAudio() { - Assert.notNull(mMediaPlayer); - if (mPlaybackFinished) { - mMediaPlayer.seekTo(0); - mChronometer.restart(); - mProgressBar.restart(); - mPlaybackFinished = false; - } else { - mChronometer.resume(); - mProgressBar.resume(); - } - mMediaPlayer.start(); - } - - private void onAudioReplayError(final int what, final int extra, final Exception exception) { - if (exception == null) { - LogUtil.e(LogUtil.BUGLE_TAG, "audio replay failed, what=" + what + - ", extra=" + extra); - } else { - LogUtil.e(LogUtil.BUGLE_TAG, "audio replay failed, exception=" + exception); - } - UiUtils.showToastAtBottom(R.string.audio_recording_replay_failed); - releaseMediaPlayer(); - } - - private void setupMediaPlayer() { - Assert.notNull(mDataSourceUri); - if (mMediaPlayer == null) { - Assert.isTrue(!mPrepared); - mMediaPlayer = new MediaPlayer(); - try { - mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mMediaPlayer.setDataSource(Factory.get().getApplicationContext(), mDataSourceUri); - mMediaPlayer.setOnCompletionListener(new OnCompletionListener() { - @Override - public void onCompletion(final MediaPlayer mp) { - updatePlayPauseButtonState(); - mChronometer.reset(); - mChronometer.setBase(SystemClock.elapsedRealtime() - - mMediaPlayer.getDuration()); - mProgressBar.reset(); - - mPlaybackFinished = true; - } - }); - - mMediaPlayer.setOnPreparedListener(new OnPreparedListener() { - @Override - public void onPrepared(final MediaPlayer mp) { - // Set base on the chronometer so we can show the full length of the audio. - mChronometer.setBase(SystemClock.elapsedRealtime() - - mMediaPlayer.getDuration()); - mProgressBar.setDuration(mMediaPlayer.getDuration()); - mMediaPlayer.seekTo(0); - mPrepared = true; - } - }); - - mMediaPlayer.setOnErrorListener(new OnErrorListener() { - @Override - public boolean onError(final MediaPlayer mp, final int what, final int extra) { - onAudioReplayError(what, extra, null); - return true; - } - }); - mMediaPlayer.prepareAsync(); - } catch (final Exception exception) { - onAudioReplayError(0, 0, exception); - releaseMediaPlayer(); - } - } - } - - private void releaseMediaPlayer() { - if (mMediaPlayer != null) { - mMediaPlayer.release(); - mMediaPlayer = null; - mPrepared = false; - mPlaybackFinished = false; - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - releaseMediaPlayer(); - } - - @Override - protected void onDraw(final Canvas canvas) { - if (mMode != LAYOUT_MODE_SUB_COMPACT) { - return; - } - - final int currentWidth = this.getWidth(); - final int currentHeight = this.getHeight(); - if (mClipPathWidth != currentWidth || mClipPathHeight != currentHeight) { - final RectF rect = new RectF(0, 0, currentWidth, currentHeight); - mRoundedCornerClipPath.reset(); - mRoundedCornerClipPath.addRoundRect(rect, mCornerRadius, mCornerRadius, - Path.Direction.CW); - mClipPathWidth = currentWidth; - mClipPathHeight = currentHeight; - } - - canvas.clipPath(mRoundedCornerClipPath); - super.onDraw(canvas); - } - - private void updatePlayPauseButtonState() { - if (mMediaPlayer == null || !mMediaPlayer.isPlaying()) { - mPlayPauseButton.setDisplayedChild(PLAY_BUTTON); - } else { - mPlayPauseButton.setDisplayedChild(PAUSE_BUTTON); - } - } - - private void resetToZeroState() { - // Release the media player so it may be set up with the new audio source. - releaseMediaPlayer(); - mChronometer.reset(); - mProgressBar.reset(); - updateVisualStyle(); - - if (mDataSourceUri != null) { - // Re-ensure the media player, so we can read the duration of the audio. - setupMediaPlayer(); - } - } - - private void updateVisualStyle() { - if (mMode == LAYOUT_MODE_SUB_COMPACT) { - // Sub-compact mode has static visual appearance already set up during initialization. - return; - } - - if (mShowAsIncoming) { - mChronometer.setTextColor(getResources().getColor(R.color.message_text_color_incoming)); - } else { - mChronometer.setTextColor(getResources().getColor(R.color.message_text_color_outgoing)); - } - mProgressBar.setVisualStyle(mShowAsIncoming); - mPlayPauseButton.setVisualStyle(mShowAsIncoming); - updatePlayPauseButtonState(); - } - - private void initializeViewsForMode() { - switch (mMode) { - case LAYOUT_MODE_NORMAL: - setOrientation(HORIZONTAL); - mProgressBar.setVisibility(VISIBLE); - break; - - case LAYOUT_MODE_COMPACT: - setOrientation(VERTICAL); - mProgressBar.setVisibility(GONE); - ((MarginLayoutParams) mPlayPauseButton.getLayoutParams()).setMargins(0, 0, 0, 0); - ((MarginLayoutParams) mChronometer.getLayoutParams()).setMargins(0, 0, 0, 0); - break; - - case LAYOUT_MODE_SUB_COMPACT: - setOrientation(VERTICAL); - mProgressBar.setVisibility(GONE); - mChronometer.setVisibility(GONE); - ((MarginLayoutParams) mPlayPauseButton.getLayoutParams()).setMargins(0, 0, 0, 0); - final ImageView playButton = (ImageView) findViewById(R.id.play_button); - playButton.setImageDrawable( - getResources().getDrawable(R.drawable.ic_preview_play)); - final ImageView pauseButton = (ImageView) findViewById(R.id.pause_button); - pauseButton.setImageDrawable( - getResources().getDrawable(R.drawable.ic_preview_pause)); - break; - - default: - Assert.fail("Unsupported mode for AudioAttachmentView!"); - break; - } - } -} diff --git a/src/com/android/messaging/ui/AudioPlaybackProgressBar.java b/src/com/android/messaging/ui/AudioPlaybackProgressBar.java deleted file mode 100644 index a5b3a57..0000000 --- a/src/com/android/messaging/ui/AudioPlaybackProgressBar.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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; - -import android.animation.ObjectAnimator; -import android.animation.TimeAnimator; -import android.animation.TimeAnimator.TimeListener; -import android.content.Context; -import android.graphics.drawable.ClipDrawable; -import android.graphics.drawable.Drawable; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.view.Gravity; -import android.widget.ProgressBar; - -/** - * Shows a styled progress bar that is synchronized with the playback state of an audio attachment. - */ -public class AudioPlaybackProgressBar extends ProgressBar implements PlaybackStateView { - private long mDurationInMillis; - private final TimeAnimator mUpdateAnimator; - private long mCumulativeTime = 0; - private long mCurrentPlayStartTime = 0; - private boolean mIncoming = false; - - public AudioPlaybackProgressBar(final Context context, final AttributeSet attrs) { - super(context, attrs); - - mUpdateAnimator = new TimeAnimator(); - mUpdateAnimator.setRepeatCount(ObjectAnimator.INFINITE); - mUpdateAnimator.setTimeListener(new TimeListener() { - @Override - public void onTimeUpdate(final TimeAnimator animation, final long totalTime, - final long deltaTime) { - int progress = 0; - if (mDurationInMillis > 0) { - progress = (int) (((mCumulativeTime + SystemClock.elapsedRealtime() - - mCurrentPlayStartTime) * 1.0f / mDurationInMillis) * 100); - progress = Math.max(Math.min(progress, 100), 0); - } - setProgress(progress); - } - }); - updateAppearance(); - } - - /** - * Sets the duration of the audio that's being played, in milliseconds. - */ - public void setDuration(final long durationInMillis) { - mDurationInMillis = durationInMillis; - } - - @Override - public void restart() { - reset(); - resume(); - } - - @Override - public void reset() { - stopUpdateTicks(); - setProgress(0); - mCumulativeTime = 0; - mCurrentPlayStartTime = 0; - } - - @Override - public void resume() { - mCurrentPlayStartTime = SystemClock.elapsedRealtime(); - startUpdateTicks(); - } - - @Override - public void pause() { - mCumulativeTime += SystemClock.elapsedRealtime() - mCurrentPlayStartTime; - stopUpdateTicks(); - } - - private void startUpdateTicks() { - if (!mUpdateAnimator.isStarted()) { - mUpdateAnimator.start(); - } - } - - private void stopUpdateTicks() { - if (mUpdateAnimator.isStarted()) { - mUpdateAnimator.end(); - } - } - - private void updateAppearance() { - final Drawable drawable = - ConversationDrawables.get().getAudioProgressDrawable(mIncoming); - final ClipDrawable clipDrawable = new ClipDrawable(drawable, Gravity.START, - ClipDrawable.HORIZONTAL); - setProgressDrawable(clipDrawable); - setBackground(ConversationDrawables.get() - .getAudioProgressBackgroundDrawable(mIncoming)); - } - - public void setVisualStyle(final boolean incoming) { - if (mIncoming != incoming) { - mIncoming = incoming; - updateAppearance(); - } - } -} diff --git a/src/com/android/messaging/ui/BaseBugleActivity.java b/src/com/android/messaging/ui/BaseBugleActivity.java deleted file mode 100644 index 1236282..0000000 --- a/src/com/android/messaging/ui/BaseBugleActivity.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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; - -import android.app.Activity; -import android.os.Bundle; - -import com.android.messaging.util.BugleActivityUtil; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.UiUtils; - -/** - * Base class for app activities that would normally derive from Activity. Responsible for - * ensuring app requirements are met during onResume() - */ -public class BaseBugleActivity extends Activity { - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (UiUtils.redirectToPermissionCheckIfNeeded(this)) { - return; - } - } - - @Override - protected void onResume() { - super.onResume(); - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onResume"); - BugleActivityUtil.onActivityResume(this, BaseBugleActivity.this); - } - - @Override - protected void onPause() { - super.onPause(); - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onPause"); - } -} diff --git a/src/com/android/messaging/ui/BaseBugleFragmentActivity.java b/src/com/android/messaging/ui/BaseBugleFragmentActivity.java deleted file mode 100644 index 947970f..0000000 --- a/src/com/android/messaging/ui/BaseBugleFragmentActivity.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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; - -import android.app.Activity; - -import com.android.messaging.util.BugleActivityUtil; -import com.android.messaging.util.LogUtil; - -/** - * Base class for app activities that would normally derive from FragmentActivity. Responsible for - * ensuring app requirements are met during onResume() - */ -public class BaseBugleFragmentActivity extends Activity { - @Override - protected void onResume() { - super.onResume(); - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onResume"); - // Ensure we have a sufficient version of Google Play Services, prompting for upgrade and - // disabling the data updates if we don't have the correct version. - BugleActivityUtil.onActivityResume(this, BaseBugleFragmentActivity.this); - } - - @Override - protected void onPause() { - super.onPause(); - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onPause"); - } -} diff --git a/src/com/android/messaging/ui/BasePagerViewHolder.java b/src/com/android/messaging/ui/BasePagerViewHolder.java deleted file mode 100644 index a82ecce..0000000 --- a/src/com/android/messaging/ui/BasePagerViewHolder.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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; - -import android.os.Parcelable; -import android.view.View; -import android.view.ViewGroup; - -/** - * The base pager view holder implementation that handles basic view creation/destruction logic, - * as well as logic to save/restore instance state that's persisted not only for activity - * reconstruction (e.g. during a configuration change), but also cases where the individual - * page view gets destroyed and recreated. - * - * To opt into saving/restoring instance state behavior for a particular page view, let the - * PageView implement PersistentInstanceState to save and restore instance states to/from a - * Parcelable. - */ -public abstract class BasePagerViewHolder implements PagerViewHolder { - protected View mView; - protected Parcelable mSavedState; - - /** - * This is called when the entire view pager is being torn down (due to configuration change - * for example) that will be restored later. - */ - @Override - public Parcelable saveState() { - savePendingState(); - return mSavedState; - } - - /** - * This is called when the view pager is being restored. - */ - @Override - public void restoreState(final Parcelable restoredState) { - if (restoredState != null) { - mSavedState = restoredState; - // If the view is already there, let it restore the state. Otherwise, it will be - // restored the next time the view gets created. - restorePendingState(); - } - } - - @Override - public void resetState() { - mSavedState = null; - if (mView != null && (mView instanceof PersistentInstanceState)) { - ((PersistentInstanceState) mView).resetState(); - } - } - - /** - * This is called when the view itself is being torn down. This may happen when the user - * has flipped to another page in the view pager, so we want to save the current state if - * possible. - */ - @Override - public View destroyView() { - savePendingState(); - final View retView = mView; - mView = null; - return retView; - } - - @Override - public View getView(ViewGroup container) { - if (mView == null) { - mView = createView(container); - // When initially created, check if the view has any saved state. If so restore it. - restorePendingState(); - } - return mView; - } - - private void savePendingState() { - if (mView != null && (mView instanceof PersistentInstanceState)) { - mSavedState = ((PersistentInstanceState) mView).saveState(); - } - } - - private void restorePendingState() { - if (mView != null && (mView instanceof PersistentInstanceState) && (mSavedState != null)) { - ((PersistentInstanceState) mView).restoreState(mSavedState); - } - } - - /** - * Create and initialize a new page view. - */ - protected abstract View createView(ViewGroup container); -} diff --git a/src/com/android/messaging/ui/BlockedParticipantListItemView.java b/src/com/android/messaging/ui/BlockedParticipantListItemView.java deleted file mode 100644 index 2ccdebf..0000000 --- a/src/com/android/messaging/ui/BlockedParticipantListItemView.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.support.v4.text.BidiFormatter; -import android.support.v4.text.TextDirectionHeuristicsCompat; -import android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantListItemData; - -/** - * View for individual participant in blocked participants list. - * - * Unblocks participant when clicked. - */ -public class BlockedParticipantListItemView extends LinearLayout { - private TextView mNameTextView; - private ContactIconView mContactIconView; - private ParticipantListItemData mData; - - public BlockedParticipantListItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - mNameTextView = (TextView) findViewById(R.id.name); - mContactIconView = (ContactIconView) findViewById(R.id.contact_icon); - setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - mData.unblock(getContext()); - } - }); - } - - public void bind(final ParticipantListItemData data) { - mData = data; - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - mNameTextView.setText(bidiFormatter.unicodeWrap( - data.getDisplayName(), TextDirectionHeuristicsCompat.LTR)); - mContactIconView.setImageResourceUri(data.getAvatarUri(), data.getContactId(), - data.getLookupKey(), data.getNormalizedDestination()); - mNameTextView.setText(data.getDisplayName()); - } -} diff --git a/src/com/android/messaging/ui/BlockedParticipantsActivity.java b/src/com/android/messaging/ui/BlockedParticipantsActivity.java deleted file mode 100644 index b740264..0000000 --- a/src/com/android/messaging/ui/BlockedParticipantsActivity.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.util.Assert; - -/** - * Show a list of currently blocked participants. - */ -public class BlockedParticipantsActivity extends BugleActionBarActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.blocked_participants_activity); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - Assert.isTrue(fragment instanceof BlockedParticipantsFragment); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - // Treat the home press as back press so that when we go back to - // ConversationActivity, it doesn't lose its original intent (conversation id etc.) - onBackPressed(); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/com/android/messaging/ui/BlockedParticipantsFragment.java b/src/com/android/messaging/ui/BlockedParticipantsFragment.java deleted file mode 100644 index 3ab54ab..0000000 --- a/src/com/android/messaging/ui/BlockedParticipantsFragment.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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; - -import android.app.Fragment; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; -import android.support.v4.widget.CursorAdapter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.binding.Binding; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.data.BlockedParticipantsData; -import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.util.Assert; - -/** - * Show a list of currently blocked participants. - */ -public class BlockedParticipantsFragment extends Fragment - implements BlockedParticipantsDataListener { - private ListView mListView; - private BlockedParticipantListAdapter mAdapter; - private final Binding<BlockedParticipantsData> mBinding = - BindingBase.createBinding(this); - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View view = - inflater.inflate(R.layout.blocked_participants_fragment, container, false); - mListView = (ListView) view.findViewById(android.R.id.list); - mAdapter = new BlockedParticipantListAdapter(getActivity(), null); - mListView.setAdapter(mAdapter); - mBinding.bind(DataModel.get().createBlockedParticipantsData(getActivity(), this)); - mBinding.getData().init(getLoaderManager(), mBinding); - return view; - } - - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - } - - @Override - public void onDestroy() { - super.onDestroy(); - mBinding.unbind(); - } - - /** - * An adapter to display ParticipantListItemView based on ParticipantData. - */ - private class BlockedParticipantListAdapter extends CursorAdapter { - public BlockedParticipantListAdapter(final Context context, final Cursor cursor) { - super(context, cursor, 0); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return LayoutInflater.from(context) - .inflate(R.layout.blocked_participant_list_item_view, parent, false); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - Assert.isTrue(view instanceof BlockedParticipantListItemView); - ((BlockedParticipantListItemView) view).bind( - mBinding.getData().createParticipantListItemData(cursor)); - } - } - - @Override - public void onBlockedParticipantsCursorUpdated(Cursor cursor) { - mAdapter.swapCursor(cursor); - } -} diff --git a/src/com/android/messaging/ui/BugleActionBarActivity.java b/src/com/android/messaging/ui/BugleActionBarActivity.java deleted file mode 100644 index 80dabb8..0000000 --- a/src/com/android/messaging/ui/BugleActionBarActivity.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * 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; - -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarActivity; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; - -import com.android.messaging.R; -import com.android.messaging.util.BugleActivityUtil; -import com.android.messaging.util.ImeUtil; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.UiUtils; - -import java.util.HashSet; -import java.util.Set; - -/** - * Base class for app activities that use an action bar. Responsible for logging/telemetry/other - * needs that will be common for all activities. We can break out the common code if/when we need - * a version that doesn't use an actionbar. - */ -public class BugleActionBarActivity extends ActionBarActivity implements ImeUtil.ImeStateHost { - // Tracks the list of observers opting in for IME state change. - private final Set<ImeUtil.ImeStateObserver> mImeStateObservers = new HashSet<>(); - - // Tracks the soft keyboard display state - private boolean mImeOpen; - - // The ActionMode that represents the modal contextual action bar, using our own implementation - // rather than the built in contextual action bar to reduce jank - private CustomActionMode mActionMode; - - // The menu for the action bar - private Menu mActionBarMenu; - - // Used to determine if a onDisplayHeightChanged was due to the IME opening or rotation of the - // device - private int mLastScreenHeight; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (UiUtils.redirectToPermissionCheckIfNeeded(this)) { - return; - } - - mLastScreenHeight = getResources().getDisplayMetrics().heightPixels; - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onCreate"); - } - } - - @Override - protected void onStart() { - super.onStart(); - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onStart"); - } - } - - @Override - protected void onRestart() { - super.onStop(); - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onRestart"); - } - } - - @Override - protected void onResume() { - super.onResume(); - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onResume"); - } - BugleActivityUtil.onActivityResume(this, BugleActionBarActivity.this); - } - - @Override - protected void onPause() { - super.onPause(); - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onPause"); - } - } - - @Override - protected void onStop() { - super.onStop(); - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onStop"); - } - } - - private boolean mDestroyed; - - @Override - protected void onDestroy() { - super.onDestroy(); - mDestroyed = true; - } - - public boolean getIsDestroyed() { - return mDestroyed; - } - - @Override - public void onDisplayHeightChanged(final int heightSpecification) { - int screenHeight = getResources().getDisplayMetrics().heightPixels; - - if (screenHeight != mLastScreenHeight) { - // Appears to be an orientation change, don't fire ime updates - mLastScreenHeight = screenHeight; - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onDisplayHeightChanged " + - " screenHeight: " + screenHeight + " lastScreenHeight: " + mLastScreenHeight + - " Skipped, appears to be orientation change."); - return; - } - final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null && actionBar.isShowing()) { - screenHeight -= actionBar.getHeight(); - } - final int height = View.MeasureSpec.getSize(heightSpecification); - - final boolean imeWasOpen = mImeOpen; - mImeOpen = screenHeight - height > 100; - - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onDisplayHeightChanged " + - "imeWasOpen: " + imeWasOpen + " mImeOpen: " + mImeOpen + " screenHeight: " + - screenHeight + " height: " + height); - } - - if (imeWasOpen != mImeOpen) { - for (final ImeUtil.ImeStateObserver observer : mImeStateObservers) { - observer.onImeStateChanged(mImeOpen); - } - } - } - - @Override - public void registerImeStateObserver(final ImeUtil.ImeStateObserver observer) { - mImeStateObservers.add(observer); - } - - @Override - public void unregisterImeStateObserver(final ImeUtil.ImeStateObserver observer) { - mImeStateObservers.remove(observer); - } - - @Override - public boolean isImeOpen() { - return mImeOpen; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - mActionBarMenu = menu; - if (mActionMode != null && - mActionMode.getCallback().onCreateActionMode(mActionMode, menu)) { - return true; - } - return false; - } - - @Override - public boolean onPrepareOptionsMenu(final Menu menu) { - mActionBarMenu = menu; - if (mActionMode != null && - mActionMode.getCallback().onPrepareActionMode(mActionMode, menu)) { - return true; - } - return super.onPrepareOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem menuItem) { - if (mActionMode != null && - mActionMode.getCallback().onActionItemClicked(mActionMode, menuItem)) { - return true; - } - - switch (menuItem.getItemId()) { - case android.R.id.home: - if (mActionMode != null) { - dismissActionMode(); - return true; - } - } - return super.onOptionsItemSelected(menuItem); - } - - @Override - public ActionMode startActionMode(final ActionMode.Callback callback) { - mActionMode = new CustomActionMode(callback); - supportInvalidateOptionsMenu(); - invalidateActionBar(); - return mActionMode; - } - - public void dismissActionMode() { - if (mActionMode != null) { - mActionMode.finish(); - mActionMode = null; - invalidateActionBar(); - } - } - - public ActionMode getActionMode() { - return mActionMode; - } - - protected ActionMode.Callback getActionModeCallback() { - if (mActionMode == null) { - return null; - } - - return mActionMode.getCallback(); - } - - /** - * Receives and handles action bar invalidation request from sub-components of this activity. - * - * <p>Normally actions have sole control over the action bar, but in order to support seamless - * transitions for components such as the full screen media picker, we have to let it take over - * the action bar and then restore its state afterwards</p> - * - * <p>If a fragment does anything that may change the action bar, it should call this method - * and then it is this method's responsibility to figure out which component "controls" the - * action bar and delegate the updating of the action bar to that component</p> - */ - public final void invalidateActionBar() { - if (mActionMode != null) { - mActionMode.updateActionBar(getSupportActionBar()); - } else { - updateActionBar(getSupportActionBar()); - } - } - - protected void updateActionBar(final ActionBar actionBar) { - actionBar.setHomeAsUpIndicator(null); - } - - /** - * Custom ActionMode implementation which allows us to just replace the contents of the main - * action bar rather than overlay over it - */ - private class CustomActionMode extends ActionMode { - private CharSequence mTitle; - private CharSequence mSubtitle; - private View mCustomView; - private final Callback mCallback; - - public CustomActionMode(final Callback callback) { - mCallback = callback; - } - - @Override - public void setTitle(final CharSequence title) { - mTitle = title; - } - - @Override - public void setTitle(final int resId) { - mTitle = getResources().getString(resId); - } - - @Override - public void setSubtitle(final CharSequence subtitle) { - mSubtitle = subtitle; - } - - @Override - public void setSubtitle(final int resId) { - mSubtitle = getResources().getString(resId); - } - - @Override - public void setCustomView(final View view) { - mCustomView = view; - } - - @Override - public void invalidate() { - invalidateActionBar(); - } - - @Override - public void finish() { - mActionMode = null; - mCallback.onDestroyActionMode(this); - supportInvalidateOptionsMenu(); - invalidateActionBar(); - } - - @Override - public Menu getMenu() { - return mActionBarMenu; - } - - @Override - public CharSequence getTitle() { - return mTitle; - } - - @Override - public CharSequence getSubtitle() { - return mSubtitle; - } - - @Override - public View getCustomView() { - return mCustomView; - } - - @Override - public MenuInflater getMenuInflater() { - return BugleActionBarActivity.this.getMenuInflater(); - } - - public Callback getCallback() { - return mCallback; - } - - public void updateActionBar(final ActionBar actionBar) { - actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP); - actionBar.setDisplayShowTitleEnabled(false); - actionBar.setDisplayShowCustomEnabled(false); - mActionMode.getCallback().onPrepareActionMode(mActionMode, mActionBarMenu); - actionBar.setBackgroundDrawable(new ColorDrawable( - getResources().getColor(R.color.contextual_action_bar_background_color))); - actionBar.setHomeAsUpIndicator(R.drawable.ic_cancel_small_dark); - actionBar.show(); - } - } -} diff --git a/src/com/android/messaging/ui/BugleAnimationTags.java b/src/com/android/messaging/ui/BugleAnimationTags.java deleted file mode 100644 index b141f5b..0000000 --- a/src/com/android/messaging/ui/BugleAnimationTags.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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; - - -/** - * Defines a list of animation tags used as android:transitionName properties used for L's - * hero transitions. - */ -public class BugleAnimationTags { - /** - * The tag for the FAB in the conversation list activity. - */ - public static final String TAG_FABICON = "bugle:fabicon"; - - /** - * The tag for the content view of a conversation list item view. - */ - public static final String TAG_CLIVCONTENT = "bugle:clivcontent"; - - /** - * The tag for the action bar. - */ - public static final String TAG_ACTIONBAR = "bugle:actionbar"; -} diff --git a/src/com/android/messaging/ui/ClassZeroActivity.java b/src/com/android/messaging/ui/ClassZeroActivity.java deleted file mode 100644 index 129ec19..0000000 --- a/src/com/android/messaging/ui/ClassZeroActivity.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.ContentValues; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.SystemClock; -import android.provider.Telephony.Sms; -import android.text.TextUtils; -import android.util.Log; -import android.view.Window; - -import com.android.messaging.R; -import com.android.messaging.datamodel.action.ReceiveSmsMessageAction; -import com.android.messaging.util.Assert; - -import java.util.ArrayList; - -/** - * Display a class-zero SMS message to the user. Wait for the user to dismiss - * it. - */ -public class ClassZeroActivity extends Activity { - private static final boolean VERBOSE = false; - private static final String TAG = "display_00"; - private static final int ON_AUTO_SAVE = 1; - - /** Default timer to dismiss the dialog. */ - private static final long DEFAULT_TIMER = 5 * 60 * 1000; - - /** To remember the exact time when the timer should fire. */ - private static final String TIMER_FIRE = "timer_fire"; - - private ContentValues mMessageValues = null; - - /** Is the message read. */ - private boolean mRead = false; - - /** The timer to dismiss the dialog automatically. */ - private long mTimerSet = 0; - private AlertDialog mDialog = null; - - private ArrayList<ContentValues> mMessageQueue = null; - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(final Message msg) { - // Do not handle an invalid message. - if (msg.what == ON_AUTO_SAVE) { - mRead = false; - mDialog.dismiss(); - saveMessage(); - processNextMessage(); - } - } - }; - - private boolean queueMsgFromIntent(final Intent msgIntent) { - final ContentValues messageValues = - msgIntent.getParcelableExtra(UIIntents.UI_INTENT_EXTRA_MESSAGE_VALUES); - // that takes the format argument is a hidden API right now. - final String message = messageValues.getAsString(Sms.BODY); - if (TextUtils.isEmpty(message)) { - if (mMessageQueue.size() == 0) { - finish(); - } - return false; - } - mMessageQueue.add(messageValues); - return true; - } - - private void processNextMessage() { - if (mMessageQueue.size() > 0) { - mMessageQueue.remove(0); - } - if (mMessageQueue.size() == 0) { - finish(); - } else { - displayZeroMessage(mMessageQueue.get(0)); - } - } - - private void saveMessage() { - mMessageValues.put(Sms.Inbox.READ, mRead ? Integer.valueOf(1) : Integer.valueOf(0)); - final ReceiveSmsMessageAction action = new ReceiveSmsMessageAction(mMessageValues); - action.start(); - } - - @Override - protected void onCreate(final Bundle icicle) { - super.onCreate(icicle); - requestWindowFeature(Window.FEATURE_NO_TITLE); - - if (mMessageQueue == null) { - mMessageQueue = new ArrayList<ContentValues>(); - } - if (!queueMsgFromIntent(getIntent())) { - return; - } - Assert.isTrue(mMessageQueue.size() == 1); - if (mMessageQueue.size() == 1) { - displayZeroMessage(mMessageQueue.get(0)); - } - - if (icicle != null) { - mTimerSet = icicle.getLong(TIMER_FIRE, mTimerSet); - } - } - - private void displayZeroMessage(final ContentValues messageValues) { - /* This'll be used by the save action */ - mMessageValues = messageValues; - final String message = messageValues.getAsString(Sms.BODY);; - - mDialog = new AlertDialog.Builder(this).setMessage(message) - .setPositiveButton(R.string.save, mSaveListener) - .setNegativeButton(android.R.string.cancel, mCancelListener) - .setTitle(R.string.class_0_message_activity) - .setCancelable(false).show(); - final long now = SystemClock.uptimeMillis(); - mTimerSet = now + DEFAULT_TIMER; - } - - @Override - protected void onNewIntent(final Intent msgIntent) { - // Running with another visible message, queue this one - queueMsgFromIntent(msgIntent); - } - - @Override - protected void onStart() { - super.onStart(); - final long now = SystemClock.uptimeMillis(); - if (mTimerSet <= now) { - // Save the message if the timer already expired. - mHandler.sendEmptyMessage(ON_AUTO_SAVE); - } else { - mHandler.sendEmptyMessageAtTime(ON_AUTO_SAVE, mTimerSet); - if (VERBOSE) { - Log.d(TAG, "onRestart time = " + Long.toString(mTimerSet) + " " - + this.toString()); - } - } - } - - @Override - protected void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - outState.putLong(TIMER_FIRE, mTimerSet); - if (VERBOSE) { - Log.d(TAG, "onSaveInstanceState time = " + Long.toString(mTimerSet) - + " " + this.toString()); - } - } - - @Override - protected void onStop() { - super.onStop(); - mHandler.removeMessages(ON_AUTO_SAVE); - if (VERBOSE) { - Log.d(TAG, "onStop time = " + Long.toString(mTimerSet) - + " " + this.toString()); - } - } - - private final OnClickListener mCancelListener = new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int whichButton) { - dialog.dismiss(); - processNextMessage(); - } - }; - - private final OnClickListener mSaveListener = new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int whichButton) { - mRead = true; - saveMessage(); - dialog.dismiss(); - processNextMessage(); - } - }; -} diff --git a/src/com/android/messaging/ui/CompositeAdapter.java b/src/com/android/messaging/ui/CompositeAdapter.java deleted file mode 100644 index 620e511..0000000 --- a/src/com/android/messaging/ui/CompositeAdapter.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.database.DataSetObserver; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -/** - * A general purpose adapter that composes one or more other adapters. It - * appends them in the order they are added. - */ -public class CompositeAdapter extends BaseAdapter { - - private static final int INITIAL_CAPACITY = 2; - - public static class Partition { - boolean mShowIfEmpty; - boolean mHasHeader; - BaseAdapter mAdapter; - - public Partition(final boolean showIfEmpty, final boolean hasHeader, - final BaseAdapter adapter) { - this.mShowIfEmpty = showIfEmpty; - this.mHasHeader = hasHeader; - this.mAdapter = adapter; - } - - /** - * True if the directory should be shown even if no contacts are found. - */ - public boolean showIfEmpty() { - return mShowIfEmpty; - } - - public boolean hasHeader() { - return mHasHeader; - } - - public int getCount() { - int count = mAdapter.getCount(); - if (mHasHeader && (count != 0 || mShowIfEmpty)) { - count++; - } - return count; - } - - public BaseAdapter getAdapter() { - return mAdapter; - } - - public View getHeaderView(final View convertView, final ViewGroup parentView) { - return null; - } - - public void close() { - // do nothing in base class. - } - } - - private class Observer extends DataSetObserver { - @Override - public void onChanged() { - CompositeAdapter.this.notifyDataSetChanged(); - } - - @Override - public void onInvalidated() { - CompositeAdapter.this.notifyDataSetInvalidated(); - } - }; - - protected final Context mContext; - private Partition[] mPartitions; - private int mSize = 0; - private int mCount = 0; - private boolean mCacheValid = true; - private final Observer mObserver; - - public CompositeAdapter(final Context context) { - mContext = context; - mObserver = new Observer(); - mPartitions = new Partition[INITIAL_CAPACITY]; - } - - public Context getContext() { - return mContext; - } - - public void addPartition(final Partition partition) { - if (mSize >= mPartitions.length) { - final int newCapacity = mSize + 2; - final Partition[] newAdapters = new Partition[newCapacity]; - System.arraycopy(mPartitions, 0, newAdapters, 0, mSize); - mPartitions = newAdapters; - } - mPartitions[mSize++] = partition; - partition.getAdapter().registerDataSetObserver(mObserver); - invalidate(); - notifyDataSetChanged(); - } - - public void removePartition(final int index) { - final Partition partition = mPartitions[index]; - partition.close(); - System.arraycopy(mPartitions, index + 1, mPartitions, index, - mSize - index - 1); - mSize--; - partition.getAdapter().unregisterDataSetObserver(mObserver); - invalidate(); - notifyDataSetChanged(); - } - - public void clearPartitions() { - for (int i = 0; i < mSize; i++) { - final Partition partition = mPartitions[i]; - partition.close(); - partition.getAdapter().unregisterDataSetObserver(mObserver); - } - invalidate(); - notifyDataSetChanged(); - } - - public Partition getPartition(final int index) { - return mPartitions[index]; - } - - public int getPartitionAtPosition(final int position) { - ensureCacheValid(); - int start = 0; - for (int i = 0; i < mSize; i++) { - final int end = start + mPartitions[i].getCount(); - if (position >= start && position < end) { - int offset = position - start; - if (mPartitions[i].hasHeader() && - (mPartitions[i].getCount() > 0 || mPartitions[i].showIfEmpty())) { - offset--; - } - if (offset == -1) { - return -1; - } - return i; - } - start = end; - } - return mSize - 1; - } - - public int getPartitionCount() { - return mSize; - } - - public void invalidate() { - mCacheValid = false; - } - - private void ensureCacheValid() { - if (mCacheValid) { - return; - } - mCount = 0; - for (int i = 0; i < mSize; i++) { - mCount += mPartitions[i].getCount(); - } - } - - @Override - public int getCount() { - ensureCacheValid(); - return mCount; - } - - public int getCount(final int index) { - ensureCacheValid(); - return mPartitions[index].getCount(); - } - - @Override - public Object getItem(final int position) { - ensureCacheValid(); - int start = 0; - for (int i = 0; i < mSize; i++) { - final int end = start + mPartitions[i].getCount(); - if (position >= start && position < end) { - final int offset = position - start; - final Partition partition = mPartitions[i]; - if (partition.hasHeader() && offset == 0 && - (partition.getCount() > 0 || partition.showIfEmpty())) { - // This is the header - return null; - } - return mPartitions[i].getAdapter().getItem(offset); - } - start = end; - } - - return null; - } - - @Override - public long getItemId(final int position) { - ensureCacheValid(); - int start = 0; - for (int i = 0; i < mSize; i++) { - final int end = start + mPartitions[i].getCount(); - if (position >= start && position < end) { - final int offset = position - start; - final Partition partition = mPartitions[i]; - if (partition.hasHeader() && offset == 0 && - (partition.getCount() > 0 || partition.showIfEmpty())) { - // Header - return 0; - } - return mPartitions[i].getAdapter().getItemId(offset); - } - start = end; - } - - return 0; - } - - @Override - public boolean isEnabled(int position) { - ensureCacheValid(); - int start = 0; - for (int i = 0; i < mSize; i++) { - final int end = start + mPartitions[i].getCount(); - if (position >= start && position < end) { - final int offset = position - start; - final Partition partition = mPartitions[i]; - if (partition.hasHeader() && offset == 0 && - (partition.getCount() > 0 || partition.showIfEmpty())) { - // This is the header - return false; - } - return true; - } - start = end; - } - return true; - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parentView) { - ensureCacheValid(); - int start = 0; - for (int i = 0; i < mSize; i++) { - final Partition partition = mPartitions[i]; - final int end = start + partition.getCount(); - if (position >= start && position < end) { - int offset = position - start; - View view; - if (partition.hasHeader() && - (partition.getCount() > 0 || partition.showIfEmpty())) { - offset = offset - 1; - } - if (offset == -1) { - view = partition.getHeaderView(convertView, parentView); - } else { - view = partition.getAdapter().getView(offset, convertView, parentView); - } - if (view == null) { - throw new NullPointerException("View should not be null, partition: " + i - + " position: " + offset); - } - return view; - } - start = end; - } - - throw new ArrayIndexOutOfBoundsException(position); - } -} diff --git a/src/com/android/messaging/ui/ContactIconView.java b/src/com/android/messaging/ui/ContactIconView.java deleted file mode 100644 index 44983ab..0000000 --- a/src/com/android/messaging/ui/ContactIconView.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.net.Uri; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.datamodel.media.AvatarGroupRequestDescriptor; -import com.android.messaging.datamodel.media.AvatarRequestDescriptor; -import com.android.messaging.util.Assert; -import com.android.messaging.util.AvatarUriUtil; -import com.android.messaging.util.ContactUtil; - -/** - * A view used to render contact icons. This class derives from AsyncImageView, so it loads contact - * icons from MediaResourceManager, and it handles more rendering logic than an AsyncImageView - * (draws a circular bitmap). - */ -public class ContactIconView extends AsyncImageView { - private static final int NORMAL_ICON_SIZE_ID = 0; - private static final int LARGE_ICON_SIZE_ID = 1; - private static final int SMALL_ICON_SIZE_ID = 2; - - protected final int mIconSize; - private final int mColorPressedId; - - private long mContactId; - private String mContactLookupKey; - private String mNormalizedDestination; - private Uri mAvatarUri; - private boolean mDisableClickHandler; - - public ContactIconView(final Context context, final AttributeSet attrs) { - super(context, attrs); - - final Resources resources = context.getResources(); - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactIconView); - - final int iconSizeId = a.getInt(R.styleable.ContactIconView_iconSize, 0); - switch (iconSizeId) { - case NORMAL_ICON_SIZE_ID: - mIconSize = (int) resources.getDimension( - R.dimen.contact_icon_view_normal_size); - break; - case LARGE_ICON_SIZE_ID: - mIconSize = (int) resources.getDimension( - R.dimen.contact_icon_view_large_size); - break; - case SMALL_ICON_SIZE_ID: - mIconSize = (int) resources.getDimension( - R.dimen.contact_icon_view_small_size); - break; - default: - // For the compiler, something has to be set even with the assert. - mIconSize = 0; - Assert.fail("Unsupported ContactIconView icon size attribute"); - } - mColorPressedId = resources.getColor(R.color.contact_avatar_pressed_color); - - setImage(null); - a.recycle(); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - setColorFilter(mColorPressedId); - } else { - clearColorFilter(); - } - return super.onTouchEvent(event); - } - - /** - * Method which allows the automatic hookup of a click handler when the Uri is changed - */ - public void setImageClickHandlerDisabled(final boolean isHandlerDisabled) { - mDisableClickHandler = isHandlerDisabled; - setOnClickListener(null); - setClickable(false); - } - - /** - * A convenience method that sets the URI of the contact icon by creating a new image request. - */ - public void setImageResourceUri(final Uri uri) { - setImageResourceUri(uri, 0, null, null); - } - - public void setImageResourceUri(final Uri uri, final long contactId, - final String contactLookupKey, final String normalizedDestination) { - if (uri == null) { - setImageResourceId(null); - } else { - final String avatarType = AvatarUriUtil.getAvatarType(uri); - if (AvatarUriUtil.TYPE_GROUP_URI.equals(avatarType)) { - setImageResourceId(new AvatarGroupRequestDescriptor(uri, mIconSize, mIconSize)); - } else { - setImageResourceId(new AvatarRequestDescriptor(uri, mIconSize, mIconSize)); - } - } - - mContactId = contactId; - mContactLookupKey = contactLookupKey; - mNormalizedDestination = normalizedDestination; - mAvatarUri = uri; - - maybeInitializeOnClickListener(); - } - - protected void maybeInitializeOnClickListener() { - if ((mContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED - && !TextUtils.isEmpty(mContactLookupKey)) || - !TextUtils.isEmpty(mNormalizedDestination)) { - if (!mDisableClickHandler) { - setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - ContactUtil.showOrAddContact(view, mContactId, mContactLookupKey, - mAvatarUri, mNormalizedDestination); - } - }); - } - } else { - // This should happen when the phone number is not in the user's contacts or it is a - // group conversation, group conversations don't have contact phone numbers. If this - // is the case then absorb the click to prevent propagation. - setOnClickListener(null); - } - } -} diff --git a/src/com/android/messaging/ui/ConversationDrawables.java b/src/com/android/messaging/ui/ConversationDrawables.java deleted file mode 100644 index cf858e2..0000000 --- a/src/com/android/messaging/ui/ConversationDrawables.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.util.ImageUtils; - -/** - * A singleton cache that holds tinted drawable resources for displaying messages, such as - * message bubbles, audio attachments etc. - */ -public class ConversationDrawables { - private static ConversationDrawables sInstance; - - // Cache the color filtered bubble drawables so that we don't need to create a - // new one for each ConversationMessageView. - private Drawable mIncomingBubbleDrawable; - private Drawable mOutgoingBubbleDrawable; - private Drawable mIncomingErrorBubbleDrawable; - private Drawable mIncomingBubbleNoArrowDrawable; - private Drawable mOutgoingBubbleNoArrowDrawable; - private Drawable mAudioPlayButtonDrawable; - private Drawable mAudioPauseButtonDrawable; - private Drawable mIncomingAudioProgressBackgroundDrawable; - private Drawable mOutgoingAudioProgressBackgroundDrawable; - private Drawable mAudioProgressForegroundDrawable; - private Drawable mFastScrollThumbDrawable; - private Drawable mFastScrollThumbPressedDrawable; - private Drawable mFastScrollPreviewDrawableLeft; - private Drawable mFastScrollPreviewDrawableRight; - private final Context mContext; - private int mOutgoingBubbleColor; - private int mIncomingErrorBubbleColor; - private int mIncomingAudioButtonColor; - private int mSelectedBubbleColor; - private int mThemeColor; - - public static ConversationDrawables get() { - if (sInstance == null) { - sInstance = new ConversationDrawables(Factory.get().getApplicationContext()); - } - return sInstance; - } - - private ConversationDrawables(final Context context) { - mContext = context; - // Pre-create all the drawables. - updateDrawables(); - } - - public int getConversationThemeColor() { - return mThemeColor; - } - - public void updateDrawables() { - final Resources resources = mContext.getResources(); - - mIncomingBubbleDrawable = resources.getDrawable(R.drawable.msg_bubble_incoming); - mIncomingBubbleNoArrowDrawable = - resources.getDrawable(R.drawable.message_bubble_incoming_no_arrow); - mIncomingErrorBubbleDrawable = resources.getDrawable(R.drawable.msg_bubble_error); - mOutgoingBubbleDrawable = resources.getDrawable(R.drawable.msg_bubble_outgoing); - mOutgoingBubbleNoArrowDrawable = - resources.getDrawable(R.drawable.message_bubble_outgoing_no_arrow); - mAudioPlayButtonDrawable = resources.getDrawable(R.drawable.ic_audio_play); - mAudioPauseButtonDrawable = resources.getDrawable(R.drawable.ic_audio_pause); - mIncomingAudioProgressBackgroundDrawable = - resources.getDrawable(R.drawable.audio_progress_bar_background_incoming); - mOutgoingAudioProgressBackgroundDrawable = - resources.getDrawable(R.drawable.audio_progress_bar_background_outgoing); - mAudioProgressForegroundDrawable = - resources.getDrawable(R.drawable.audio_progress_bar_progress); - mFastScrollThumbDrawable = resources.getDrawable(R.drawable.fastscroll_thumb); - mFastScrollThumbPressedDrawable = - resources.getDrawable(R.drawable.fastscroll_thumb_pressed); - mFastScrollPreviewDrawableLeft = - resources.getDrawable(R.drawable.fastscroll_preview_left); - mFastScrollPreviewDrawableRight = - resources.getDrawable(R.drawable.fastscroll_preview_right); - mOutgoingBubbleColor = resources.getColor(R.color.message_bubble_color_outgoing); - mIncomingErrorBubbleColor = - resources.getColor(R.color.message_error_bubble_color_incoming); - mIncomingAudioButtonColor = - resources.getColor(R.color.message_audio_button_color_incoming); - mSelectedBubbleColor = resources.getColor(R.color.message_bubble_color_selected); - mThemeColor = resources.getColor(R.color.primary_color); - } - - public Drawable getBubbleDrawable(final boolean selected, final boolean incoming, - final boolean needArrow, final boolean isError) { - final Drawable protoDrawable; - if (needArrow) { - if (incoming) { - protoDrawable = isError && !selected ? - mIncomingErrorBubbleDrawable : mIncomingBubbleDrawable; - } else { - protoDrawable = mOutgoingBubbleDrawable; - } - } else if (incoming) { - protoDrawable = mIncomingBubbleNoArrowDrawable; - } else { - protoDrawable = mOutgoingBubbleNoArrowDrawable; - } - - int color; - if (selected) { - color = mSelectedBubbleColor; - } else if (incoming) { - if (isError) { - color = mIncomingErrorBubbleColor; - } else { - color = mThemeColor; - } - } else { - color = mOutgoingBubbleColor; - } - - return ImageUtils.getTintedDrawable(mContext, protoDrawable, color); - } - - private int getAudioButtonColor(final boolean incoming) { - return incoming ? mIncomingAudioButtonColor : mThemeColor; - } - - public Drawable getPlayButtonDrawable(final boolean incoming) { - return ImageUtils.getTintedDrawable( - mContext, mAudioPlayButtonDrawable, getAudioButtonColor(incoming)); - } - - public Drawable getPauseButtonDrawable(final boolean incoming) { - return ImageUtils.getTintedDrawable( - mContext, mAudioPauseButtonDrawable, getAudioButtonColor(incoming)); - } - - public Drawable getAudioProgressDrawable(final boolean incoming) { - return ImageUtils.getTintedDrawable( - mContext, mAudioProgressForegroundDrawable, getAudioButtonColor(incoming)); - } - - public Drawable getAudioProgressBackgroundDrawable(final boolean incoming) { - return incoming ? mIncomingAudioProgressBackgroundDrawable : - mOutgoingAudioProgressBackgroundDrawable; - } - - public Drawable getFastScrollThumbDrawable(final boolean pressed) { - if (pressed) { - return ImageUtils.getTintedDrawable(mContext, mFastScrollThumbPressedDrawable, - mThemeColor); - } else { - return mFastScrollThumbDrawable; - } - } - - public Drawable getFastScrollPreviewDrawable(boolean positionRight) { - Drawable protoDrawable = positionRight ? mFastScrollPreviewDrawableRight : - mFastScrollPreviewDrawableLeft; - return ImageUtils.getTintedDrawable(mContext, protoDrawable, mThemeColor); - } -} diff --git a/src/com/android/messaging/ui/CursorRecyclerAdapter.java b/src/com/android/messaging/ui/CursorRecyclerAdapter.java deleted file mode 100644 index f1a7b7d..0000000 --- a/src/com/android/messaging/ui/CursorRecyclerAdapter.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.DataSetObserver; -import android.os.Handler; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.ViewGroup; -import android.widget.FilterQueryProvider; - -/** - * Copy of CursorAdapter suited for RecyclerView. - * - * TODO: BUG 16327984. Replace this with a framework supported CursorAdapter for - * RecyclerView when one is available. - */ -public abstract class CursorRecyclerAdapter<VH extends RecyclerView.ViewHolder> - extends RecyclerView.Adapter<VH> { - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected boolean mDataValid; - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected boolean mAutoRequery; - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected Cursor mCursor; - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected Context mContext; - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected int mRowIDColumn; - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected ChangeObserver mChangeObserver; - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected DataSetObserver mDataSetObserver; - /** - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected FilterQueryProvider mFilterQueryProvider; - - /** - * If set the adapter will call requery() on the cursor whenever a content change - * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}. - * - * @deprecated This option is discouraged, as it results in Cursor queries - * being performed on the application's UI thread and thus can cause poor - * responsiveness or even Application Not Responding errors. As an alternative, - * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. - */ - @Deprecated - public static final int FLAG_AUTO_REQUERY = 0x01; - - /** - * If set the adapter will register a content observer on the cursor and will call - * {@link #onContentChanged()} when a notification comes in. Be careful when - * using this flag: you will need to unset the current Cursor from the adapter - * to avoid leaks due to its registered observers. This flag is not needed - * when using a CursorAdapter with a - * {@link android.content.CursorLoader}. - */ - public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; - - /** - * Recommended constructor. - * - * @param c The cursor from which to get the data. - * @param context The context - * @param flags Flags used to determine the behavior of the adapter; may - * be any combination of {@link #FLAG_AUTO_REQUERY} and - * {@link #FLAG_REGISTER_CONTENT_OBSERVER}. - */ - public CursorRecyclerAdapter(final Context context, final Cursor c, final int flags) { - init(context, c, flags); - } - - void init(final Context context, final Cursor c, int flags) { - if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) { - flags |= FLAG_REGISTER_CONTENT_OBSERVER; - mAutoRequery = true; - } else { - mAutoRequery = false; - } - final boolean cursorPresent = c != null; - mCursor = c; - mDataValid = cursorPresent; - mContext = context; - mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; - if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { - mChangeObserver = new ChangeObserver(); - mDataSetObserver = new MyDataSetObserver(); - } else { - mChangeObserver = null; - mDataSetObserver = null; - } - - if (cursorPresent) { - if (mChangeObserver != null) { - c.registerContentObserver(mChangeObserver); - } - if (mDataSetObserver != null) { - c.registerDataSetObserver(mDataSetObserver); - } - } - } - - /** - * Returns the cursor. - * @return the cursor. - */ - public Cursor getCursor() { - return mCursor; - } - - @Override - public int getItemCount() { - if (mDataValid && mCursor != null) { - return mCursor.getCount(); - } else { - return 0; - } - } - - /** - * @see android.support.v7.widget.RecyclerView.Adapter#getItem(int) - */ - public Object getItem(final int position) { - if (mDataValid && mCursor != null) { - mCursor.moveToPosition(position); - return mCursor; - } else { - return null; - } - } - - /** - * @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int) - */ - @Override - public long getItemId(final int position) { - if (mDataValid && mCursor != null) { - if (mCursor.moveToPosition(position)) { - return mCursor.getLong(mRowIDColumn); - } else { - return 0; - } - } else { - return 0; - } - } - - @Override - public VH onCreateViewHolder(final ViewGroup parent, final int viewType) { - return createViewHolder(mContext, parent, viewType); - } - - @Override - public void onBindViewHolder(final VH holder, final int position) { - if (!mDataValid) { - throw new IllegalStateException("this should only be called when the cursor is valid"); - } - if (!mCursor.moveToPosition(position)) { - throw new IllegalStateException("couldn't move cursor to position " + position); - } - bindViewHolder(holder, mContext, mCursor); - } - /** - * Bind an existing view to the data pointed to by cursor - * @param view Existing view, returned earlier by newView - * @param context Interface to application's global information - * @param cursor The cursor from which to get the data. The cursor is already - * moved to the correct position. - */ - public abstract void bindViewHolder(VH holder, Context context, Cursor cursor); - - /** - * @see android.support.v7.widget.RecyclerView.Adapter#createViewHolder(Context, ViewGroup, int) - */ - public abstract VH createViewHolder(Context context, ViewGroup parent, int viewType); - - /** - * Change the underlying cursor to a new cursor. If there is an existing cursor it will be - * closed. - * - * @param cursor The new cursor to be used - */ - public void changeCursor(final Cursor cursor) { - final Cursor old = swapCursor(cursor); - if (old != null) { - old.close(); - } - } - - /** - * Swap in a new Cursor, returning the old Cursor. Unlike - * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em> - * closed. - * - * @param newCursor The new cursor to be used. - * @return Returns the previously set Cursor, or null if there wasa not one. - * If the given new Cursor is the same instance is the previously set - * Cursor, null is also returned. - */ - public Cursor swapCursor(final Cursor newCursor) { - if (newCursor == mCursor) { - return null; - } - final Cursor oldCursor = mCursor; - if (oldCursor != null) { - if (mChangeObserver != null) { - oldCursor.unregisterContentObserver(mChangeObserver); - } - if (mDataSetObserver != null) { - oldCursor.unregisterDataSetObserver(mDataSetObserver); - } - } - mCursor = newCursor; - if (newCursor != null) { - if (mChangeObserver != null) { - newCursor.registerContentObserver(mChangeObserver); - } - if (mDataSetObserver != null) { - newCursor.registerDataSetObserver(mDataSetObserver); - } - mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); - mDataValid = true; - // notify the observers about the new cursor - notifyDataSetChanged(); - } else { - mRowIDColumn = -1; - mDataValid = false; - // notify the observers about the lack of a data set - notifyDataSetChanged(); - } - return oldCursor; - } - - /** - * <p>Converts the cursor into a CharSequence. Subclasses should override this - * method to convert their results. The default implementation returns an - * empty String for null values or the default String representation of - * the value.</p> - * - * @param cursor the cursor to convert to a CharSequence - * @return a CharSequence representing the value - */ - public CharSequence convertToString(final Cursor cursor) { - return cursor == null ? "" : cursor.toString(); - } - - /** - * Called when the {@link ContentObserver} on the cursor receives a change notification. - * The default implementation provides the auto-requery logic, but may be overridden by - * sub classes. - * - * @see ContentObserver#onChange(boolean) - */ - protected void onContentChanged() { - if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { - if (false) { - Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); - } - mDataValid = mCursor.requery(); - } - } - - private class ChangeObserver extends ContentObserver { - public ChangeObserver() { - super(new Handler()); - } - - @Override - public boolean deliverSelfNotifications() { - return true; - } - - @Override - public void onChange(final boolean selfChange) { - onContentChanged(); - } - } - - private class MyDataSetObserver extends DataSetObserver { - @Override - public void onChanged() { - mDataValid = true; - notifyDataSetChanged(); - } - - @Override - public void onInvalidated() { - mDataValid = false; - notifyDataSetChanged(); - } - } - -} diff --git a/src/com/android/messaging/ui/CustomHeaderPagerListViewHolder.java b/src/com/android/messaging/ui/CustomHeaderPagerListViewHolder.java deleted file mode 100644 index 1268c53..0000000 --- a/src/com/android/messaging/ui/CustomHeaderPagerListViewHolder.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.database.Cursor; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.CursorAdapter; -import android.widget.ListView; - -import com.android.messaging.util.ImeUtil; - -/** - * Produces and holds a list view and its tab header to be displayed in a ViewPager. - */ -public abstract class CustomHeaderPagerListViewHolder extends BasePagerViewHolder - implements CustomHeaderPagerViewHolder { - private final Context mContext; - private final CursorAdapter mListAdapter; - private boolean mListCursorInitialized; - private ListView mListView; - - public CustomHeaderPagerListViewHolder(final Context context, - final CursorAdapter adapter) { - mContext = context; - mListAdapter = adapter; - } - - @Override - protected View createView(ViewGroup container) { - final LayoutInflater inflater = (LayoutInflater) - mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - final View view = inflater.inflate( - getLayoutResId(), - null /* root */, - false /* attachToRoot */); - final ListView listView = (ListView) view.findViewById(getListViewResId()); - listView.setAdapter(mListAdapter); - listView.setOnScrollListener(new OnScrollListener() { - @Override - public void onScrollStateChanged(final AbsListView view, final int scrollState) { - if (scrollState != SCROLL_STATE_IDLE) { - ImeUtil.get().hideImeKeyboard(mContext, view); - } - } - - @Override - public void onScroll(final AbsListView view, final int firstVisibleItem, - final int visibleItemCount, final int totalItemCount) { - } - }); - mListView = listView; - maybeSetEmptyView(); - return view; - } - - public void onContactsCursorUpdated(final Cursor data) { - mListAdapter.swapCursor(data); - if (!mListCursorInitialized) { - // We set the emptyView here instead of in create so that the initial load won't show - // the empty UI - the system handles this and doesn't do what we would like. - mListCursorInitialized = true; - maybeSetEmptyView(); - } - } - - /** - * We don't want to show the empty view hint until BOTH conditions are met: - * 1. The view has been created. - * 2. Cursor data has been loaded once. - * Due to timing when data is loaded, the view may not be ready (and vice versa). So we - * are calling this method from both onContactsCursorUpdated & createView. - */ - private void maybeSetEmptyView() { - if (mView != null && mListCursorInitialized) { - final ListEmptyView emptyView = (ListEmptyView) mView.findViewById(getEmptyViewResId()); - if (emptyView != null) { - emptyView.setTextHint(getEmptyViewTitleResId()); - emptyView.setImageHint(getEmptyViewImageResId()); - final ListView listView = (ListView) mView.findViewById(getListViewResId()); - listView.setEmptyView(emptyView); - } - } - } - - public void invalidateList() { - mListAdapter.notifyDataSetChanged(); - } - - /** - * In order for scene transition to work, we toggle the visibility for each individual list - * view items so that they can be properly tracked by the scene transition manager. - * @param show whether the pending transition is to show or hide the list. - */ - public void toggleVisibilityForPendingTransition(final boolean show, final View epicenterView) { - if (mListView == null) { - return; - } - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View childView = mListView.getChildAt(i); - if (childView != epicenterView) { - childView.setVisibility(show ? View.VISIBLE : View.INVISIBLE); - } - } - } - - @Override - public CharSequence getPageTitle(Context context) { - return context.getString(getPageTitleResId()); - } - - protected abstract int getLayoutResId(); - protected abstract int getPageTitleResId(); - protected abstract int getEmptyViewResId(); - protected abstract int getEmptyViewTitleResId(); - protected abstract int getEmptyViewImageResId(); - protected abstract int getListViewResId(); -} diff --git a/src/com/android/messaging/ui/CustomHeaderPagerViewHolder.java b/src/com/android/messaging/ui/CustomHeaderPagerViewHolder.java deleted file mode 100644 index 43802cd..0000000 --- a/src/com/android/messaging/ui/CustomHeaderPagerViewHolder.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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; - -import android.content.Context; - -/** - * An extension on the standard PagerViewHolder to return a custom header view to be used by - * CustomHeaderViewPager - */ -public interface CustomHeaderPagerViewHolder extends PagerViewHolder { - CharSequence getPageTitle(Context context); -} diff --git a/src/com/android/messaging/ui/CustomHeaderViewPager.java b/src/com/android/messaging/ui/CustomHeaderViewPager.java deleted file mode 100644 index 2e8df42..0000000 --- a/src/com/android/messaging/ui/CustomHeaderViewPager.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.support.v4.view.ViewPager.OnPageChangeListener; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.widget.LinearLayout; - -import com.android.messaging.R; -import com.android.messaging.util.Assert; - -/** - * A view that contains both a view pager and a tab strip wrapped in a linear layout. - */ -public class CustomHeaderViewPager extends LinearLayout { - public final static int DEFAULT_TAB_STRIP_SIZE = -1; - private final int mDefaultTabStripSize; - private ViewPager mViewPager; - private ViewPagerTabs mTabstrip; - - public CustomHeaderViewPager(final Context context, final AttributeSet attrs) { - super(context, attrs); - - final LayoutInflater inflater = LayoutInflater.from(context); - inflater.inflate(R.layout.custom_header_view_pager, this, true); - setOrientation(LinearLayout.VERTICAL); - - mTabstrip = (ViewPagerTabs) findViewById(R.id.tab_strip); - mViewPager = (ViewPager) findViewById(R.id.pager); - - TypedValue tv = new TypedValue(); - context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true); - mDefaultTabStripSize = context.getResources().getDimensionPixelSize(tv.resourceId); - } - - public void setCurrentItem(final int position) { - mViewPager.setCurrentItem(position); - } - - public void setViewPagerTabHeight(final int tabHeight) { - mTabstrip.getLayoutParams().height = tabHeight == DEFAULT_TAB_STRIP_SIZE ? - mDefaultTabStripSize : tabHeight; - } - - public void setViewHolders(final CustomHeaderPagerViewHolder[] viewHolders) { - Assert.notNull(mViewPager); - final PagerAdapter adapter = new CustomHeaderViewPagerAdapter(viewHolders); - mViewPager.setAdapter(adapter); - mTabstrip.setViewPager(mViewPager); - mViewPager.setOnPageChangeListener(new OnPageChangeListener() { - - @Override - public void onPageScrollStateChanged(int state) { - mTabstrip.onPageScrollStateChanged(state); - } - - @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { - mTabstrip.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - - @Override - public void onPageSelected(int position) { - mTabstrip.onPageSelected(position); - } - }); - } - - public int getSelectedItemPosition() { - return mTabstrip.getSelectedItemPosition(); - } -} diff --git a/src/com/android/messaging/ui/CustomHeaderViewPagerAdapter.java b/src/com/android/messaging/ui/CustomHeaderViewPagerAdapter.java deleted file mode 100644 index 1a5cf6a..0000000 --- a/src/com/android/messaging/ui/CustomHeaderViewPagerAdapter.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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; - -import com.android.messaging.Factory; - -public class CustomHeaderViewPagerAdapter extends - FixedViewPagerAdapter<CustomHeaderPagerViewHolder> { - - public CustomHeaderViewPagerAdapter(final CustomHeaderPagerViewHolder[] viewHolders) { - super(viewHolders); - } - - @Override - public CharSequence getPageTitle(int position) { - // The tab strip will handle RTL internally so we should use raw position. - return getViewHolder(position, false /* rtlAware */) - .getPageTitle(Factory.get().getApplicationContext()); - } -} diff --git a/src/com/android/messaging/ui/FixedViewPagerAdapter.java b/src/com/android/messaging/ui/FixedViewPagerAdapter.java deleted file mode 100644 index bd73b71..0000000 --- a/src/com/android/messaging/ui/FixedViewPagerAdapter.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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; - -import android.os.Bundle; -import android.os.Parcelable; -import android.support.v4.view.PagerAdapter; -import android.view.View; -import android.view.ViewGroup; - -import com.android.messaging.Factory; -import com.android.messaging.util.Assert; -import com.android.messaging.util.UiUtils; -import com.google.common.annotations.VisibleForTesting; - -/** - * A PagerAdapter that provides a fixed number of paged Views provided by a fixed set of - * {@link PagerViewHolder}'s. This allows us to put a fixed number of Views, instead of fragments, - * into a given ViewPager. - */ -public class FixedViewPagerAdapter<T extends PagerViewHolder> extends PagerAdapter { - private final T[] mViewHolders; - - public FixedViewPagerAdapter(final T[] viewHolders) { - Assert.notNull(viewHolders); - mViewHolders = viewHolders; - } - - @Override - public Object instantiateItem(final ViewGroup container, final int position) { - final PagerViewHolder viewHolder = getViewHolder(position); - final View view = viewHolder.getView(container); - if (view == null) { - return null; - } - view.setTag(viewHolder); - container.addView(view); - return viewHolder; - } - - @Override - public void destroyItem(final ViewGroup container, final int position, final Object object) { - final PagerViewHolder viewHolder = getViewHolder(position); - final View destroyedView = viewHolder.destroyView(); - if (destroyedView != null) { - container.removeView(destroyedView); - } - } - - @Override - public int getCount() { - return mViewHolders.length; - } - - @Override - public boolean isViewFromObject(final View view, final Object object) { - return view.getTag() == object; - } - - public T getViewHolder(final int i) { - return getViewHolder(i, true /* rtlAware */); - } - - @VisibleForTesting - public T getViewHolder(final int i, final boolean rtlAware) { - return mViewHolders[rtlAware ? getRtlPosition(i) : i]; - } - - @Override - public Parcelable saveState() { - // The paged views in the view pager gets created and destroyed as the user scrolls through - // them. By default, only the pages to the immediate left and right of the current visible - // page are realized. Moreover, if the activity gets destroyed and recreated, the pages are - // automatically destroyed. Therefore, in order to preserve transient page UI states that - // are not persisted in the DB we'd like to store them in a Bundle when views get - // destroyed. When the views get recreated, we rehydrate them by passing them the saved - // data. When the activity gets destroyed, it invokes saveState() on this adapter to - // add this saved Bundle to the overall saved instance state. - final Bundle savedViewHolderState = new Bundle(Factory.get().getApplicationContext() - .getClassLoader()); - for (int i = 0; i < mViewHolders.length; i++) { - final Parcelable pageState = getViewHolder(i).saveState(); - savedViewHolderState.putParcelable(getInstanceStateKeyForPage(i), pageState); - } - return savedViewHolderState; - } - - @Override - public void restoreState(final Parcelable state, final ClassLoader loader) { - if (state instanceof Bundle) { - final Bundle restoredViewHolderState = (Bundle) state; - ((Bundle) state).setClassLoader(Factory.get().getApplicationContext().getClassLoader()); - for (int i = 0; i < mViewHolders.length; i++) { - final Parcelable pageState = restoredViewHolderState - .getParcelable(getInstanceStateKeyForPage(i)); - getViewHolder(i).restoreState(pageState); - } - } else { - super.restoreState(state, loader); - } - } - - public void resetState() { - for (int i = 0; i < mViewHolders.length; i++) { - getViewHolder(i).resetState(); - } - } - - private String getInstanceStateKeyForPage(final int i) { - return getViewHolder(i).getClass().getCanonicalName() + "_savedstate_" + i; - } - - protected int getRtlPosition(final int position) { - if (UiUtils.isRtlMode()) { - return mViewHolders.length - 1 - position; - } - return position; - } -} diff --git a/src/com/android/messaging/ui/ImeDetectFrameLayout.java b/src/com/android/messaging/ui/ImeDetectFrameLayout.java deleted file mode 100644 index 32564ea..0000000 --- a/src/com/android/messaging/ui/ImeDetectFrameLayout.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -import com.android.messaging.util.ImeUtil; -import com.android.messaging.util.LogUtil; - -public class ImeDetectFrameLayout extends FrameLayout { - public ImeDetectFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int measuredHeight = getMeasuredHeight(); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, "ImeDetectFrameLayout " + - "measuredHeight: " + measuredHeight + " getMeasuredHeight(): " + - getMeasuredHeight()); - } - - if (measuredHeight != getMeasuredHeight() && getContext() instanceof ImeUtil.ImeStateHost) { - ((ImeUtil.ImeStateHost) getContext()).onDisplayHeightChanged(heightMeasureSpec); - } - } -} diff --git a/src/com/android/messaging/ui/LicenseActivity.java b/src/com/android/messaging/ui/LicenseActivity.java deleted file mode 100644 index a28da81..0000000 --- a/src/com/android/messaging/ui/LicenseActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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; - -import android.app.Activity; -import android.os.Bundle; -import android.webkit.WebView; - -import com.android.messaging.R; - -public class LicenseActivity extends Activity { - private final String LICENSE_URL = "file:///android_asset/licenses.html"; - - @Override - public void onCreate(final Bundle bundle) { - super.onCreate(bundle); - setContentView(R.layout.license_activity); - final WebView webView = (WebView) findViewById(R.id.content); - webView.loadUrl(LICENSE_URL); - } -} diff --git a/src/com/android/messaging/ui/LineWrapLayout.java b/src/com/android/messaging/ui/LineWrapLayout.java deleted file mode 100644 index 5219811..0000000 --- a/src/com/android/messaging/ui/LineWrapLayout.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; - -import java.util.ArrayList; - -/** -* A line-wrapping flow layout. Arranges children in horizontal flow, packing as many -* child views as possible on each line. When the current line does not -* have enough horizontal space, the layout continues on the next line. -*/ -public class LineWrapLayout extends ViewGroup { - public LineWrapLayout(Context context) { - this(context, null); - } - - public LineWrapLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int startPadding = UiUtils.getPaddingStart(this); - final int endPadding = UiUtils.getPaddingEnd(this); - final int widthMode = MeasureSpec.getMode(widthMeasureSpec); - final int widthSize = MeasureSpec.getSize(widthMeasureSpec) - startPadding - endPadding; - final boolean isFixedSize = (widthMode == MeasureSpec.EXACTLY); - - int height = 0; - - int childCount = getChildCount(); - int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); - - int x = startPadding; - int currLineWidth = 0; - int currLineHeight = 0; - int maxLineWidth = 0; - - for (int i = 0; i < childCount; i++) { - View currChild = getChildAt(i); - if (currChild.getVisibility() == GONE) { - continue; - } - LayoutParams layoutParams = (LayoutParams) currChild.getLayoutParams(); - int startMargin = layoutParams.getStartMargin(); - int endMargin = layoutParams.getEndMargin(); - currChild.measure(childWidthSpec, MeasureSpec.UNSPECIFIED); - int childMeasuredWidth = currChild.getMeasuredWidth() + startMargin + endMargin; - int childMeasuredHeight = currChild.getMeasuredHeight() + layoutParams.topMargin + - layoutParams.bottomMargin; - - if ((x + childMeasuredWidth) > widthSize) { - // New line. Update the overall height and reset trackers. - height += currLineHeight; - currLineHeight = 0; - x = startPadding; - currLineWidth = 0; - startMargin = 0; - } - - x += childMeasuredWidth; - currLineWidth += childMeasuredWidth; - currLineHeight = Math.max(currLineHeight, childMeasuredHeight); - maxLineWidth = Math.max(currLineWidth, maxLineWidth); - } - // And account for the height of the last line. - height += currLineHeight; - - int width = isFixedSize ? widthSize : maxLineWidth; - setMeasuredDimension(width + startPadding + endPadding, - height + getPaddingTop() + getPaddingBottom()); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - final int startPadding = UiUtils.getPaddingStart(this); - final int endPadding = UiUtils.getPaddingEnd(this); - int width = getWidth() - startPadding - endPadding; - int y = getPaddingTop(); - int x = startPadding; - int childCount = getChildCount(); - - int currLineHeight = 0; - - // Do a dry-run first to get the line heights. - final ArrayList<Integer> lineHeights = new ArrayList<Integer>(); - for (int i = 0; i < childCount; i++) { - View currChild = getChildAt(i); - if (currChild.getVisibility() == GONE) { - continue; - } - LayoutParams layoutParams = (LayoutParams) currChild.getLayoutParams(); - int childWidth = currChild.getMeasuredWidth(); - int childHeight = currChild.getMeasuredHeight(); - int startMargin = layoutParams.getStartMargin(); - int endMargin = layoutParams.getEndMargin(); - - if ((x + childWidth + startMargin + endMargin) > width) { - // new line - lineHeights.add(currLineHeight); - currLineHeight = 0; - x = startPadding; - startMargin = 0; - } - currLineHeight = Math.max(currLineHeight, childHeight + layoutParams.topMargin + - layoutParams.bottomMargin); - x += childWidth + startMargin + endMargin; - } - // Add the last line height. - lineHeights.add(currLineHeight); - - // Now perform the actual layout. - x = startPadding; - currLineHeight = 0; - int lineIndex = 0; - for (int i = 0; i < childCount; i++) { - View currChild = getChildAt(i); - if (currChild.getVisibility() == GONE) { - continue; - } - LayoutParams layoutParams = (LayoutParams) currChild.getLayoutParams(); - int childWidth = currChild.getMeasuredWidth(); - int childHeight = currChild.getMeasuredHeight(); - int startMargin = layoutParams.getStartMargin(); - int endMargin = layoutParams.getEndMargin(); - - if ((x + childWidth + startMargin + endMargin) > width) { - // new line - y += currLineHeight; - currLineHeight = 0; - x = startPadding; - startMargin = 0; - lineIndex++; - } - final int startPositionX = x + startMargin; - int startPositionY = y + layoutParams.topMargin; // default to top gravity - final int majorGravity = layoutParams.gravity & Gravity.VERTICAL_GRAVITY_MASK; - if (majorGravity != Gravity.TOP && lineHeights.size() > lineIndex) { - final int lineHeight = lineHeights.get(lineIndex); - switch (majorGravity) { - case Gravity.BOTTOM: - startPositionY = y + lineHeight - childHeight - layoutParams.bottomMargin; - break; - - case Gravity.CENTER_VERTICAL: - startPositionY = y + (lineHeight - childHeight) / 2; - break; - } - } - - if (OsUtil.isAtLeastJB_MR2() && getResources().getConfiguration() - .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - currChild.layout(width - startPositionX - childWidth, startPositionY, - width - startPositionX, startPositionY + childHeight); - } else { - currChild.layout(startPositionX, startPositionY, startPositionX + childWidth, - startPositionY + childHeight); - } - currLineHeight = Math.max(currLineHeight, childHeight + layoutParams.topMargin + - layoutParams.bottomMargin); - x += childWidth + startMargin + endMargin; - } - } - - @Override - protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return new LayoutParams(p); - } - - @Override - public LayoutParams generateLayoutParams(AttributeSet attrs) { - return new LayoutParams(getContext(), attrs); - } - - @Override - protected LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - } - - public static final class LayoutParams extends FrameLayout.LayoutParams { - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - } - - public LayoutParams(int width, int height) { - super(width, height); - } - - public LayoutParams(ViewGroup.LayoutParams source) { - super(source); - } - - public int getStartMargin() { - if (OsUtil.isAtLeastJB_MR2()) { - return getMarginStart(); - } else { - return leftMargin; - } - } - - public int getEndMargin() { - if (OsUtil.isAtLeastJB_MR2()) { - return getMarginEnd(); - } else { - return rightMargin; - } - } - } -} diff --git a/src/com/android/messaging/ui/ListEmptyView.java b/src/com/android/messaging/ui/ListEmptyView.java deleted file mode 100644 index 8cf3049..0000000 --- a/src/com/android/messaging/ui/ListEmptyView.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.Gravity; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.LinearLayout.LayoutParams; -import android.widget.TextView; - -import com.android.messaging.R; - -/** - * A common reusable view that shows a hint image and text for an empty list view. - */ -public class ListEmptyView extends LinearLayout { - private ImageView mEmptyImageHint; - private TextView mEmptyTextHint; - - public ListEmptyView(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mEmptyImageHint = (ImageView) findViewById(R.id.empty_image_hint); - mEmptyTextHint = (TextView) findViewById(R.id.empty_text_hint); - } - - public void setImageHint(final int resId) { - mEmptyImageHint.setImageResource(resId); - } - - public void setTextHint(final int resId) { - mEmptyTextHint.setText(getResources().getText(resId)); - } - - public void setTextHint(final CharSequence hintText) { - mEmptyTextHint.setText(hintText); - } - - public void setIsImageVisible(final boolean isImageVisible) { - mEmptyImageHint.setVisibility(isImageVisible ? VISIBLE : GONE); - } - - public void setIsVerticallyCentered(final boolean isVerticallyCentered) { - int gravity = - isVerticallyCentered ? Gravity.CENTER : Gravity.TOP | Gravity.CENTER_HORIZONTAL; - ((LinearLayout.LayoutParams) mEmptyImageHint.getLayoutParams()).gravity = gravity; - ((LinearLayout.LayoutParams) mEmptyTextHint.getLayoutParams()).gravity = gravity; - getLayoutParams().height = - isVerticallyCentered ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; - requestLayout(); - } -} diff --git a/src/com/android/messaging/ui/MaxHeightScrollView.java b/src/com/android/messaging/ui/MaxHeightScrollView.java deleted file mode 100644 index a000cd1..0000000 --- a/src/com/android/messaging/ui/MaxHeightScrollView.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.widget.ScrollView; - -import com.android.messaging.R; - -/** - * A ScrollView that limits the maximum height that it can take. This is to work around android - * layout's limitation of not having android:maxHeight. - */ -public class MaxHeightScrollView extends ScrollView { - private static final int NO_MAX_HEIGHT = -1; - - private final int mMaxHeight; - - public MaxHeightScrollView(final Context context, final AttributeSet attrs) { - super(context, attrs); - final TypedArray attr = context.obtainStyledAttributes(attrs, - R.styleable.MaxHeightScrollView, 0, 0); - mMaxHeight = attr.getDimensionPixelSize(R.styleable.MaxHeightScrollView_android_maxHeight, - NO_MAX_HEIGHT); - attr.recycle(); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mMaxHeight != NO_MAX_HEIGHT) { - setMeasuredDimension(getMeasuredWidth(), Math.min(getMeasuredHeight(), mMaxHeight)); - } - } -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/MultiAttachmentLayout.java b/src/com/android/messaging/ui/MultiAttachmentLayout.java deleted file mode 100644 index f620245..0000000 --- a/src/com/android/messaging/ui/MultiAttachmentLayout.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.animation.AnimationSet; -import android.view.animation.ScaleAnimation; -import android.view.animation.TranslateAnimation; -import android.widget.FrameLayout; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.MediaPickerMessagePartData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.PendingAttachmentData; -import com.android.messaging.datamodel.media.ImageRequestDescriptor; -import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.Assert; -import com.android.messaging.util.UiUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -/** - * Holds and displays multiple attachments in a 4x2 grid. Each preview image "tile" can take - * one of three sizes - small (1x1), wide (2x1) and large (2x2). We have a number of predefined - * layout settings designed for holding 2, 3, 4+ attachments (these layout settings are - * tweakable by design request to allow for max flexibility). For a visual example, consider the - * following attachment layout: - * - * +---------------+----------------+ - * | | | - * | | B | - * | | | - * | A |-------+--------| - * | | | | - * | | C | D | - * | | | | - * +---------------+-------+--------+ - * - * In the above example, the layout consists of four tiles, A-D. A is a large tile, B is a - * wide tile and C & D are both small tiles. A starts at (0,0) and ends at (1,1), B starts at - * (2,0) and ends at (3,0), and so on. In our layout class we'd have these tiles in the order - * of A-D, so that we make sure the last tile is always the one where we can put the overflow - * indicator (e.g. "+2"). - */ -public class MultiAttachmentLayout extends FrameLayout { - - public interface OnAttachmentClickListener { - boolean onAttachmentClick(MessagePartData attachment, Rect viewBoundsOnScreen, - boolean longPress); - } - - private static final int GRID_WIDTH = 4; // in # of cells - private static final int GRID_HEIGHT = 2; // in # of cells - - /** - * Represents a preview image tile in the layout - */ - private static class Tile { - public final int startX; - public final int startY; - public final int endX; - public final int endY; - - private Tile(final int startX, final int startY, final int endX, final int endY) { - this.startX = startX; - this.startY = startY; - this.endX = endX; - this.endY = endY; - } - - public int getWidthMeasureSpec(final int cellWidth, final int padding) { - return MeasureSpec.makeMeasureSpec((endX - startX + 1) * cellWidth - padding * 2, - MeasureSpec.EXACTLY); - } - - public int getHeightMeasureSpec(final int cellHeight, final int padding) { - return MeasureSpec.makeMeasureSpec((endY - startY + 1) * cellHeight - padding * 2, - MeasureSpec.EXACTLY); - } - - public static Tile large(final int startX, final int startY) { - return new Tile(startX, startY, startX + 1, startY + 1); - } - - public static Tile wide(final int startX, final int startY) { - return new Tile(startX, startY, startX + 1, startY); - } - - public static Tile small(final int startX, final int startY) { - return new Tile(startX, startY, startX, startY); - } - } - - /** - * A layout simply contains a list of tiles, in the order of top-left -> bottom-right. - */ - private static class Layout { - public final List<Tile> tiles; - public Layout(final Tile[] tilesArray) { - tiles = Arrays.asList(tilesArray); - } - } - - /** - * List of predefined layout configurations w.r.t no. of attachments. - */ - private static final Layout[] ATTACHMENT_LAYOUTS_BY_COUNT = { - null, // Doesn't support zero attachments. - null, // Doesn't support one attachment. Single attachment preview is used instead. - new Layout(new Tile[] { Tile.large(0, 0), Tile.large(2, 0) }), // 2 items - new Layout(new Tile[] { Tile.large(0, 0), Tile.wide(2, 0), Tile.wide(2, 1) }), // 3 items - new Layout(new Tile[] { Tile.large(0, 0), Tile.wide(2, 0), Tile.small(2, 1), // 4+ items - Tile.small(3, 1) }), - }; - - /** - * List of predefined RTL layout configurations w.r.t no. of attachments. - */ - private static final Layout[] ATTACHMENT_RTL_LAYOUTS_BY_COUNT = { - null, // Doesn't support zero attachments. - null, // Doesn't support one attachment. Single attachment preview is used instead. - new Layout(new Tile[] { Tile.large(2, 0), Tile.large(0, 0)}), // 2 items - new Layout(new Tile[] { Tile.large(2, 0), Tile.wide(0, 0), Tile.wide(0, 1) }), // 3 items - new Layout(new Tile[] { Tile.large(2, 0), Tile.wide(0, 0), Tile.small(1, 1), // 4+ items - Tile.small(0, 1) }), - }; - - private Layout mCurrentLayout; - private ArrayList<ViewWrapper> mPreviewViews; - private int mPlusNumber; - private TextView mPlusTextView; - private OnAttachmentClickListener mAttachmentClickListener; - private AsyncImageViewDelayLoader mImageViewDelayLoader; - - public MultiAttachmentLayout(final Context context, final AttributeSet attrs) { - super(context, attrs); - mPreviewViews = new ArrayList<ViewWrapper>(); - } - - public void bindAttachments(final Iterable<MessagePartData> attachments, - final Rect transitionRect, final int count) { - final ArrayList<ViewWrapper> previousViews = mPreviewViews; - mPreviewViews = new ArrayList<ViewWrapper>(); - removeView(mPlusTextView); - mPlusTextView = null; - - determineLayout(attachments, count); - buildViews(attachments, previousViews, transitionRect); - - // Remove all previous views that couldn't be recycled. - for (final ViewWrapper viewWrapper : previousViews) { - removeView(viewWrapper.view); - } - requestLayout(); - } - - public OnAttachmentClickListener getOnAttachmentClickListener() { - return mAttachmentClickListener; - } - - public void setOnAttachmentClickListener(final OnAttachmentClickListener listener) { - mAttachmentClickListener = listener; - } - - public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) { - mImageViewDelayLoader = delayLoader; - } - - public void setColorFilter(int color) { - for (ViewWrapper viewWrapper : mPreviewViews) { - if (viewWrapper.view instanceof AsyncImageView) { - ((AsyncImageView) viewWrapper.view).setColorFilter(color); - } - } - } - - public void clearColorFilter() { - for (ViewWrapper viewWrapper : mPreviewViews) { - if (viewWrapper.view instanceof AsyncImageView) { - ((AsyncImageView) viewWrapper.view).clearColorFilter(); - } - } - } - - private void determineLayout(final Iterable<MessagePartData> attachments, final int count) { - Assert.isTrue(attachments != null); - final boolean isRtl = AccessibilityUtil.isLayoutRtl(getRootView()); - if (isRtl) { - mCurrentLayout = ATTACHMENT_RTL_LAYOUTS_BY_COUNT[Math.min(count, - ATTACHMENT_RTL_LAYOUTS_BY_COUNT.length - 1)]; - } else { - mCurrentLayout = ATTACHMENT_LAYOUTS_BY_COUNT[Math.min(count, - ATTACHMENT_LAYOUTS_BY_COUNT.length - 1)]; - } - - // We must have a valid layout for the current configuration. - Assert.notNull(mCurrentLayout); - - mPlusNumber = count - mCurrentLayout.tiles.size(); - Assert.isTrue(mPlusNumber >= 0); - } - - private void buildViews(final Iterable<MessagePartData> attachments, - final ArrayList<ViewWrapper> previousViews, final Rect transitionRect) { - final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); - final int count = mCurrentLayout.tiles.size(); - int i = 0; - final Iterator<MessagePartData> iterator = attachments.iterator(); - while (iterator.hasNext() && i < count) { - final MessagePartData attachment = iterator.next(); - ViewWrapper attachmentWrapper = null; - // Try to recycle a previous view first - for (int j = 0; j < previousViews.size(); j++) { - final ViewWrapper previousView = previousViews.get(j); - if (previousView.attachment.equals(attachment) && - !(previousView.attachment instanceof PendingAttachmentData)) { - attachmentWrapper = previousView; - previousViews.remove(j); - break; - } - } - - if (attachmentWrapper == null) { - final View view = AttachmentPreviewFactory.createAttachmentPreview(layoutInflater, - attachment, this, AttachmentPreviewFactory.TYPE_MULTIPLE, - false /* startImageRequest */, mAttachmentClickListener); - - if (view == null) { - // createAttachmentPreview can return null if something goes wrong (e.g. - // attachment has unsupported contentType) - continue; - } - if (view instanceof AsyncImageView && mImageViewDelayLoader != null) { - AsyncImageView asyncImageView = (AsyncImageView) view; - asyncImageView.setDelayLoader(mImageViewDelayLoader); - } - addView(view); - attachmentWrapper = new ViewWrapper(view, attachment); - // Help animate from single to multi by copying over the prev location - if (count == 2 && i == 1 && transitionRect != null) { - attachmentWrapper.prevLeft = transitionRect.left; - attachmentWrapper.prevTop = transitionRect.top; - attachmentWrapper.prevWidth = transitionRect.width(); - attachmentWrapper.prevHeight = transitionRect.height(); - } - } - i++; - Assert.notNull(attachmentWrapper); - mPreviewViews.add(attachmentWrapper); - - // The first view will animate in using PopupTransitionAnimation, but the remaining - // views will slide from their previous position to their new position within the - // layout - if (i == 0) { - AttachmentPreview.tryAnimateViewIn(attachment, attachmentWrapper.view); - } - attachmentWrapper.needsSlideAnimation = i > 0; - } - - // Build the plus text view (e.g. "+2") for when there are more attachments than what - // this layout can display. - if (mPlusNumber > 0) { - mPlusTextView = (TextView) layoutInflater.inflate(R.layout.attachment_more_text_view, - null /* parent */); - mPlusTextView.setText(getResources().getString(R.string.attachment_more_items, - mPlusNumber)); - addView(mPlusTextView); - } - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - final int maxWidth = getResources().getDimensionPixelSize( - R.dimen.multiple_attachment_preview_width); - final int maxHeight = getResources().getDimensionPixelSize( - R.dimen.multiple_attachment_preview_height); - final int width = Math.min(MeasureSpec.getSize(widthMeasureSpec), maxWidth); - final int height = maxHeight; - final int cellWidth = width / GRID_WIDTH; - final int cellHeight = height / GRID_HEIGHT; - final int count = mPreviewViews.size(); - final int padding = getResources().getDimensionPixelOffset( - R.dimen.multiple_attachment_preview_padding); - for (int i = 0; i < count; i++) { - final View view = mPreviewViews.get(i).view; - final Tile imageTile = mCurrentLayout.tiles.get(i); - view.measure(imageTile.getWidthMeasureSpec(cellWidth, padding), - imageTile.getHeightMeasureSpec(cellHeight, padding)); - - // Now that we know the size, we can request an appropriately-sized image. - if (view instanceof AsyncImageView) { - final ImageRequestDescriptor imageRequest = - AttachmentPreviewFactory.getImageRequestDescriptorForAttachment( - mPreviewViews.get(i).attachment, - view.getMeasuredWidth(), - view.getMeasuredHeight()); - ((AsyncImageView) view).setImageResourceId(imageRequest); - } - - if (i == count - 1 && mPlusTextView != null) { - // The plus text view always covers the last attachment. - mPlusTextView.measure(imageTile.getWidthMeasureSpec(cellWidth, padding), - imageTile.getHeightMeasureSpec(cellHeight, padding)); - } - } - setMeasuredDimension(width, height); - } - - @Override - protected void onLayout(final boolean changed, final int left, final int top, final int right, - final int bottom) { - final int cellWidth = getMeasuredWidth() / GRID_WIDTH; - final int cellHeight = getMeasuredHeight() / GRID_HEIGHT; - final int padding = getResources().getDimensionPixelOffset( - R.dimen.multiple_attachment_preview_padding); - final int count = mPreviewViews.size(); - for (int i = 0; i < count; i++) { - final ViewWrapper viewWrapper = mPreviewViews.get(i); - final View view = viewWrapper.view; - final Tile imageTile = mCurrentLayout.tiles.get(i); - final int tileLeft = imageTile.startX * cellWidth; - final int tileTop = imageTile.startY * cellHeight; - view.layout(tileLeft + padding, tileTop + padding, - tileLeft + view.getMeasuredWidth(), - tileTop + view.getMeasuredHeight()); - if (viewWrapper.needsSlideAnimation) { - trySlideAttachmentView(viewWrapper); - viewWrapper.needsSlideAnimation = false; - } else { - viewWrapper.prevLeft = view.getLeft(); - viewWrapper.prevTop = view.getTop(); - viewWrapper.prevWidth = view.getWidth(); - viewWrapper.prevHeight = view.getHeight(); - } - - if (i == count - 1 && mPlusTextView != null) { - // The plus text view always covers the last attachment. - mPlusTextView.layout(tileLeft + padding, tileTop + padding, - tileLeft + mPlusTextView.getMeasuredWidth(), - tileTop + mPlusTextView.getMeasuredHeight()); - } - } - } - - private void trySlideAttachmentView(final ViewWrapper viewWrapper) { - if (!(viewWrapper.attachment instanceof MediaPickerMessagePartData)) { - return; - } - final View view = viewWrapper.view; - - - final int xOffset = viewWrapper.prevLeft - view.getLeft(); - final int yOffset = viewWrapper.prevTop - view.getTop(); - final float scaleX = viewWrapper.prevWidth / (float) view.getWidth(); - final float scaleY = viewWrapper.prevHeight / (float) view.getHeight(); - - if (xOffset == 0 && yOffset == 0 && scaleX == 1 && scaleY == 1) { - // Layout hasn't changed - return; - } - - final AnimationSet animationSet = new AnimationSet( - true /* shareInterpolator */); - animationSet.addAnimation(new TranslateAnimation(xOffset, 0, yOffset, 0)); - animationSet.addAnimation(new ScaleAnimation(scaleX, 1, scaleY, 1)); - animationSet.setDuration( - UiUtils.MEDIAPICKER_TRANSITION_DURATION); - animationSet.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR); - view.startAnimation(animationSet); - view.invalidate(); - viewWrapper.prevLeft = view.getLeft(); - viewWrapper.prevTop = view.getTop(); - viewWrapper.prevWidth = view.getWidth(); - viewWrapper.prevHeight = view.getHeight(); - } - - public View findViewForAttachment(final MessagePartData attachment) { - for (ViewWrapper wrapper : mPreviewViews) { - if (wrapper.attachment.equals(attachment) && - !(wrapper.attachment instanceof PendingAttachmentData)) { - return wrapper.view; - } - } - return null; - } - - private static class ViewWrapper { - final View view; - final MessagePartData attachment; - boolean needsSlideAnimation; - int prevLeft; - int prevTop; - int prevWidth; - int prevHeight; - - ViewWrapper(final View view, final MessagePartData attachment) { - this.view = view; - this.attachment = attachment; - } - } -} diff --git a/src/com/android/messaging/ui/OrientedBitmapDrawable.java b/src/com/android/messaging/ui/OrientedBitmapDrawable.java deleted file mode 100644 index 9242668..0000000 --- a/src/com/android/messaging/ui/OrientedBitmapDrawable.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.BitmapDrawable; -import android.view.Gravity; - -import com.android.messaging.util.exif.ExifInterface; - -/** - * A drawable that draws a bitmap in a flipped or rotated orientation without having to adjust the - * bitmap - */ -public class OrientedBitmapDrawable extends BitmapDrawable { - private final ExifInterface.OrientationParams mOrientationParams; - private final Rect mDstRect; - private int mCenterX; - private int mCenterY; - private boolean mApplyGravity; - - public static BitmapDrawable create(final int orientation, Resources res, Bitmap bitmap) { - if (orientation <= ExifInterface.Orientation.TOP_LEFT) { - // No need to adjust the bitmap, so just use a regular BitmapDrawable - return new BitmapDrawable(res, bitmap); - } else { - // Create an oriented bitmap drawable - return new OrientedBitmapDrawable(orientation, res, bitmap); - } - } - - private OrientedBitmapDrawable(final int orientation, Resources res, Bitmap bitmap) { - super(res, bitmap); - mOrientationParams = ExifInterface.getOrientationParams(orientation); - mApplyGravity = true; - mDstRect = new Rect(); - } - - @Override - public int getIntrinsicWidth() { - if (mOrientationParams.invertDimensions) { - return super.getIntrinsicHeight(); - } - return super.getIntrinsicWidth(); - } - - @Override - public int getIntrinsicHeight() { - if (mOrientationParams.invertDimensions) { - return super.getIntrinsicWidth(); - } - return super.getIntrinsicHeight(); - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - mApplyGravity = true; - } - - @Override - public void draw(Canvas canvas) { - if (mApplyGravity) { - Gravity.apply(getGravity(), getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), - mDstRect); - mCenterX = mDstRect.centerX(); - mCenterY = mDstRect.centerY(); - if (mOrientationParams.invertDimensions) { - final Matrix matrix = new Matrix(); - matrix.setRotate(mOrientationParams.rotation, mCenterX, mCenterY); - final RectF rotatedRect = new RectF(mDstRect); - matrix.mapRect(rotatedRect); - mDstRect.set((int) rotatedRect.left, (int) rotatedRect.top, (int) rotatedRect.right, - (int) rotatedRect.bottom); - } - - mApplyGravity = false; - } - canvas.save(); - canvas.scale(mOrientationParams.scaleX, mOrientationParams.scaleY, mCenterX, mCenterY); - canvas.rotate(mOrientationParams.rotation, mCenterX, mCenterY); - canvas.drawBitmap(getBitmap(), (Rect) null, mDstRect, getPaint()); - canvas.restore(); - } -} diff --git a/src/com/android/messaging/ui/PagerViewHolder.java b/src/com/android/messaging/ui/PagerViewHolder.java deleted file mode 100644 index 2f33a0f..0000000 --- a/src/com/android/messaging/ui/PagerViewHolder.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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; - -import android.view.View; -import android.view.ViewGroup; - -/** - * Holds reusable View(s) for a {@link FixedViewPagerAdapter} to display a page. By using - * reusable Views inside ViewPagers this allows us to get rid of nested fragments and the messy - * activity lifecycle problems they entail. - */ -public interface PagerViewHolder extends PersistentInstanceState { - /** Instructs the pager to clean up any view related resources - * @return the destroyed View so that the adapter may remove it from the container, or - * null if no View has been created. */ - View destroyView(); - - /** @return The view that presents the page view to the user */ - View getView(ViewGroup container); -} diff --git a/src/com/android/messaging/ui/PagingAwareViewPager.java b/src/com/android/messaging/ui/PagingAwareViewPager.java deleted file mode 100644 index cc7b2cd..0000000 --- a/src/com/android/messaging/ui/PagingAwareViewPager.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.view.MotionEvent; - -import com.android.messaging.util.UiUtils; - -/** - * A simple extension on the standard ViewPager which lets you turn paging on/off. - */ -public class PagingAwareViewPager extends ViewPager { - private boolean mPagingEnabled = true; - - public PagingAwareViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setCurrentItem(int item, boolean smoothScroll) { - super.setCurrentItem(getRtlPosition(item), smoothScroll); - } - - @Override - public void setCurrentItem(int item) { - super.setCurrentItem(getRtlPosition(item)); - } - - @Override - public int getCurrentItem() { - int position = super.getCurrentItem(); - return getRtlPosition(position); - } - - /** - * Switches position in pager to be adjusted for if we are in RtL mode - * - * @param position - * @return position adjusted if in rtl mode - */ - protected int getRtlPosition(final int position) { - final PagerAdapter adapter = getAdapter(); - if (adapter != null && UiUtils.isRtlMode()) { - return adapter.getCount() - 1 - position; - } - return position; - } - - @Override - public boolean onTouchEvent(final MotionEvent event) { - if (!mPagingEnabled) { - return false; - } - return super.onTouchEvent(event); - } - - @Override - public boolean onInterceptTouchEvent(final MotionEvent event) { - if (!mPagingEnabled) { - return false; - } - return super.onInterceptTouchEvent(event); - } - - public void setPagingEnabled(final boolean enabled) { - this.mPagingEnabled = enabled; - } - - /** This prevents touch-less scrolling eg. while doing accessibility navigation. */ - @Override - public boolean canScrollHorizontally(int direction) { - if (mPagingEnabled) { - return super.canScrollHorizontally(direction); - } else { - return false; - } - } -} diff --git a/src/com/android/messaging/ui/PermissionCheckActivity.java b/src/com/android/messaging/ui/PermissionCheckActivity.java deleted file mode 100644 index e992a10..0000000 --- a/src/com/android/messaging/ui/PermissionCheckActivity.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.SystemClock; -import android.provider.Settings; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.TextView; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; - -/** - * Activity to check if the user has required permissions. If not, it will try to prompt the user - * to grant permissions. However, the OS may not actually prompt the user if the user had - * previously checked the "Never ask again" checkbox while denying the required permissions. - */ -public class PermissionCheckActivity extends Activity { - private static final int REQUIRED_PERMISSIONS_REQUEST_CODE = 1; - private static final long AUTOMATED_RESULT_THRESHOLD_MILLLIS = 250; - private static final String PACKAGE_URI_PREFIX = "package:"; - private long mRequestTimeMillis; - private TextView mNextView; - private TextView mSettingsView; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (redirectIfNeeded()) { - return; - } - - setContentView(R.layout.permission_check_activity); - UiUtils.setStatusBarColor(this, getColor(R.color.permission_check_activity_background)); - - findViewById(R.id.exit).setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - finish(); - } - }); - - mNextView = (TextView) findViewById(R.id.next); - mNextView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - tryRequestPermission(); - } - }); - - mSettingsView = (TextView) findViewById(R.id.settings); - mSettingsView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse(PACKAGE_URI_PREFIX + getPackageName())); - startActivity(intent); - } - }); - } - - @Override - public void onResume() { - super.onResume(); - - if (redirectIfNeeded()) { - return; - } - } - - private void tryRequestPermission() { - final String[] missingPermissions = OsUtil.getMissingRequiredPermissions(); - if (missingPermissions.length == 0) { - redirect(); - return; - } - - mRequestTimeMillis = SystemClock.elapsedRealtime(); - requestPermissions(missingPermissions, REQUIRED_PERMISSIONS_REQUEST_CODE); - } - - @Override - public void onRequestPermissionsResult( - final int requestCode, final String permissions[], final int[] grantResults) { - if (requestCode == REQUIRED_PERMISSIONS_REQUEST_CODE) { - // We do not use grantResults as some of the granted permissions might have been - // revoked while the permissions dialog box was being shown for the missing permissions. - if (OsUtil.hasRequiredPermissions()) { - Factory.get().onRequiredPermissionsAcquired(); - redirect(); - } else { - final long currentTimeMillis = SystemClock.elapsedRealtime(); - // If the permission request completes very quickly, it must be because the system - // automatically denied. This can happen if the user had previously denied it - // and checked the "Never ask again" check box. - if ((currentTimeMillis - mRequestTimeMillis) < AUTOMATED_RESULT_THRESHOLD_MILLLIS) { - mNextView.setVisibility(View.GONE); - - mSettingsView.setVisibility(View.VISIBLE); - findViewById(R.id.enable_permission_procedure).setVisibility(View.VISIBLE); - } - } - } - } - - /** Returns true if the redirecting was performed */ - private boolean redirectIfNeeded() { - if (!OsUtil.hasRequiredPermissions()) { - return false; - } - - redirect(); - return true; - } - - private void redirect() { - UIIntents.get().launchConversationListActivity(this); - finish(); - } -} diff --git a/src/com/android/messaging/ui/PersistentInstanceState.java b/src/com/android/messaging/ui/PersistentInstanceState.java deleted file mode 100644 index 3cc1856..0000000 --- a/src/com/android/messaging/ui/PersistentInstanceState.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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; - -import android.os.Parcelable; - -/** - * Wraps around functionality to save, restore and reset a particular UI component's state. - */ -public interface PersistentInstanceState { - /** - * Saves necessary information about the current state of the instance to later restore - * the instance to its original state. - */ - Parcelable saveState(); - - /** - * Given a previously saved instance state, attempt to restore to its original view state. - */ - void restoreState(Parcelable restoredState); - - /** - * Called when any current/preserved state should be reset to zero state. - */ - void resetState(); -} diff --git a/src/com/android/messaging/ui/PersonItemView.java b/src/com/android/messaging/ui/PersonItemView.java deleted file mode 100644 index afd1a99..0000000 --- a/src/com/android/messaging/ui/PersonItemView.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.Intent; -import android.support.v4.text.BidiFormatter; -import android.support.v4.text.TextDirectionHeuristicsCompat; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.binding.DetachableBinding; -import com.android.messaging.datamodel.data.PersonItemData; -import com.android.messaging.datamodel.data.PersonItemData.PersonItemDataListener; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.UiUtils; - -/** - * Shows a view for a "person" - could be a contact or a participant. This always shows a - * contact icon on the left, and the person's display name on the right. - * - * This view is always bound to an abstract PersonItemData class, so to use it for a specific - * scenario, all you need to do is to create a concrete PersonItemData subclass that bridges - * between the underlying data (e.g. ParticipantData) and what the UI wants (e.g. display name). - */ -public class PersonItemView extends LinearLayout implements PersonItemDataListener, - OnLayoutChangeListener { - public interface PersonItemViewListener { - void onPersonClicked(PersonItemData data); - boolean onPersonLongClicked(PersonItemData data); - } - - protected final DetachableBinding<PersonItemData> mBinding; - private TextView mNameTextView; - private TextView mDetailsTextView; - private ContactIconView mContactIconView; - private View mDetailsContainer; - private PersonItemViewListener mListener; - private boolean mAvatarOnly; - - public PersonItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mBinding = BindingBase.createDetachableBinding(this); - LayoutInflater.from(getContext()).inflate(R.layout.person_item_view, this, true); - } - - @Override - protected void onFinishInflate() { - mNameTextView = (TextView) findViewById(R.id.name); - mDetailsTextView = (TextView) findViewById(R.id.details); - mContactIconView = (ContactIconView) findViewById(R.id.contact_icon); - mDetailsContainer = findViewById(R.id.details_container); - mNameTextView.addOnLayoutChangeListener(this); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (mBinding.isBound()) { - mBinding.detach(); - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mBinding.reAttachIfPossible(); - } - - /** - * Binds to a person item data which will provide us info to be displayed. - * @param personData the PersonItemData to be bound to. - */ - public void bind(final PersonItemData personData) { - if (mBinding.isBound()) { - if (mBinding.getData().equals(personData)) { - // Don't rebind if we are requesting the same data. - return; - } - mBinding.unbind(); - } - - if (personData != null) { - mBinding.bind(personData); - mBinding.getData().setListener(this); - - // Accessibility reason : in case phone numbers are mixed in the display name, - // we need to vocalize it for talkback. - final String vocalizedDisplayName = AccessibilityUtil.getVocalizedPhoneNumber( - getResources(), getDisplayName()); - mNameTextView.setContentDescription(vocalizedDisplayName); - } - updateViewAppearance(); - } - - /** - * @return Display name, possibly comma-ellipsized. - */ - private String getDisplayName() { - final int width = mNameTextView.getMeasuredWidth(); - final String displayName = mBinding.getData().getDisplayName(); - if (width == 0 || TextUtils.isEmpty(displayName) || !displayName.contains(",")) { - return displayName; - } - final String plusOneString = getContext().getString(R.string.plus_one); - final String plusNString = getContext().getString(R.string.plus_n); - return BidiFormatter.getInstance().unicodeWrap( - UiUtils.commaEllipsize( - displayName, - mNameTextView.getPaint(), - width, - plusOneString, - plusNString).toString(), - TextDirectionHeuristicsCompat.LTR); - } - - @Override - public void onLayoutChange(final View v, final int left, final int top, final int right, - final int bottom, final int oldLeft, final int oldTop, final int oldRight, - final int oldBottom) { - if (mBinding.isBound() && v == mNameTextView) { - setNameTextView(); - } - } - - /** - * When set to true, we display only the avatar of the person and hide everything else. - */ - public void setAvatarOnly(final boolean avatarOnly) { - mAvatarOnly = avatarOnly; - mDetailsContainer.setVisibility(avatarOnly ? GONE : VISIBLE); - } - - public boolean isAvatarOnly() { - return mAvatarOnly; - } - - public void setNameTextColor(final int color) { - mNameTextView.setTextColor(color); - } - - public void setDetailsTextColor(final int color) { - mDetailsTextView.setTextColor(color); - } - - public void setListener(final PersonItemViewListener listener) { - mListener = listener; - if (mListener == null) { - return; - } - setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - if (mListener != null && mBinding.isBound()) { - mListener.onPersonClicked(mBinding.getData()); - } - } - }); - final OnLongClickListener onLongClickListener = new OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - if (mListener != null && mBinding.isBound()) { - return mListener.onPersonLongClicked(mBinding.getData()); - } - return false; - } - }; - setOnLongClickListener(onLongClickListener); - mContactIconView.setOnLongClickListener(onLongClickListener); - } - - public void performClickOnAvatar() { - mContactIconView.performClick(); - } - - protected void updateViewAppearance() { - if (mBinding.isBound()) { - setNameTextView(); - - final String details = mBinding.getData().getDetails(); - if (TextUtils.isEmpty(details)) { - mDetailsTextView.setVisibility(GONE); - } else { - mDetailsTextView.setVisibility(VISIBLE); - mDetailsTextView.setText(details); - } - - mContactIconView.setImageResourceUri(mBinding.getData().getAvatarUri(), - mBinding.getData().getContactId(), mBinding.getData().getLookupKey(), - mBinding.getData().getNormalizedDestination()); - } else { - mNameTextView.setText(""); - mContactIconView.setImageResourceUri(null); - } - } - - private void setNameTextView() { - final String displayName = getDisplayName(); - if (TextUtils.isEmpty(displayName)) { - mNameTextView.setVisibility(GONE); - } else { - mNameTextView.setVisibility(VISIBLE); - mNameTextView.setText(displayName); - } - } - - @Override - public void onPersonDataUpdated(final PersonItemData data) { - mBinding.ensureBound(data); - updateViewAppearance(); - } - - @Override - public void onPersonDataFailed(final PersonItemData data, final Exception exception) { - mBinding.ensureBound(data); - updateViewAppearance(); - } - - public Intent getClickIntent() { - return mBinding.getData().getClickIntent(); - } -} diff --git a/src/com/android/messaging/ui/PlaceholderInsetDrawable.java b/src/com/android/messaging/ui/PlaceholderInsetDrawable.java deleted file mode 100644 index dda2e7b..0000000 --- a/src/com/android/messaging/ui/PlaceholderInsetDrawable.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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; - -import android.graphics.drawable.Drawable; -import android.graphics.drawable.InsetDrawable; - -/** - * A "placeholder" drawable that has the same sizing properties as the real UI element it - * replaces. - * - * This is an InsetDrawable that takes a placeholder drawable (an animation list, or simply - * a color drawable) and place it in the center of the inset drawable that's sized to the - * requested source width and height of the image that it replaces. Unlike the base - * InsetDrawable, this implementation returns the true width and height of the real image - * that it's placeholding, instead of the intrinsic size of the contained drawable, so that - * when used in an ImageView, it may be positioned/scaled/cropped the same way the real - * image is. - */ -public class PlaceholderInsetDrawable extends InsetDrawable { - // The dimensions of the real image that this drawable is replacing. - private final int mSourceWidth; - private final int mSourceHeight; - - /** - * Given a source drawable, wraps it around in this placeholder drawable by placing the - * drawable at the center of the container if possible (or fill the container if the - * drawable doesn't have intrinsic size such as color drawable). - */ - public static PlaceholderInsetDrawable fromDrawable(final Drawable drawable, - final int sourceWidth, final int sourceHeight) { - final int drawableWidth = drawable.getIntrinsicWidth(); - final int drawableHeight = drawable.getIntrinsicHeight(); - final int insetHorizontal = drawableWidth < 0 || drawableWidth > sourceWidth ? - 0 : (sourceWidth - drawableWidth) / 2; - final int insetVertical = drawableHeight < 0 || drawableHeight > sourceHeight ? - 0 : (sourceHeight - drawableHeight) / 2; - return new PlaceholderInsetDrawable(drawable, insetHorizontal, insetVertical, - insetHorizontal, insetVertical, sourceWidth, sourceHeight); - } - - private PlaceholderInsetDrawable(final Drawable drawable, final int insetLeft, - final int insetTop, final int insetRight, final int insetBottom, - final int sourceWidth, final int sourceHeight) { - super(drawable, insetLeft, insetTop, insetRight, insetBottom); - mSourceWidth = sourceWidth; - mSourceHeight = sourceHeight; - } - - @Override - public int getIntrinsicWidth() { - return mSourceWidth; - } - - @Override - public int getIntrinsicHeight() { - return mSourceHeight; - } -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/PlainTextEditText.java b/src/com/android/messaging/ui/PlainTextEditText.java deleted file mode 100644 index 8d6e784..0000000 --- a/src/com/android/messaging/ui/PlainTextEditText.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.util.AttributeSet; -import android.widget.EditText; - -/** - * We want the EditText used in Conversations to convert text to plain text on paste. This - * conversion would happen anyway on send, so without this class it could appear to the user - * that we would send e.g. bold or italic formatting, but in the sent message it would just be - * plain text. - */ -public class PlainTextEditText extends EditText { - private static final char OBJECT_UNICODE = '\uFFFC'; - - public PlainTextEditText(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - // Intercept and modify the paste event. Let everything else through unchanged. - @Override - public boolean onTextContextMenuItem(final int id) { - if (id == android.R.id.paste) { - // We can use this to know where the text position was originally before we pasted - final int selectionStartPrePaste = getSelectionStart(); - - // Let the EditText's normal paste routine fire, then modify the content after. - // This is simpler than re-implementing the paste logic, which we'd have to do - // if we want to get the text from the clipboard ourselves and then modify it. - - final boolean result = super.onTextContextMenuItem(id); - CharSequence text = getText(); - int selectionStart = getSelectionStart(); - int selectionEnd = getSelectionEnd(); - - // There is an option in the Chrome mobile app to copy image; however, instead of the - // image in the form of the uri, Chrome gives us the html source for the image, which - // the platform paste code turns into the unicode object character. The below section - // of code looks for that edge case and replaces it with the url for the image. - final int startIndex = selectionStart - 1; - final int pasteStringLength = selectionStart - selectionStartPrePaste; - // Only going to handle the case where the pasted object is the image - if (pasteStringLength == 1 && text.charAt(startIndex) == OBJECT_UNICODE) { - final ClipboardManager clipboard = - (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); - final ClipData clip = clipboard.getPrimaryClip(); - if (clip != null) { - ClipData.Item item = clip.getItemAt(0); - StringBuilder sb = new StringBuilder(text); - final String url = item.getText().toString(); - sb.replace(selectionStartPrePaste, selectionStart, url); - text = sb.toString(); - selectionStart = selectionStartPrePaste + url.length(); - selectionEnd = selectionStart; - } - } - - // This removes the formatting due to the conversion to string. - setText(text.toString(), BufferType.EDITABLE); - - // Restore the cursor selection state. - setSelection(selectionStart, selectionEnd); - return result; - } else { - return super.onTextContextMenuItem(id); - } - } -} diff --git a/src/com/android/messaging/ui/PlaybackStateView.java b/src/com/android/messaging/ui/PlaybackStateView.java deleted file mode 100644 index 8d9aac7..0000000 --- a/src/com/android/messaging/ui/PlaybackStateView.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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; - -/** - * An interface for a UI element ("View") that reflects the playback state of a piece of media - * content. It needs to support the ability to take common playback commands (play, pause, stop, - * restart) and reflect the state in UI (through timer or progress bar etc.) - */ -public interface PlaybackStateView { - /** - * Restart the playback. - */ - void restart(); - - /** - * Reset ("stop") the playback to the starting position. - */ - void reset(); - - /** - * Resume the playback, or start it if it hasn't been started yet. - */ - void resume(); - - /** - * Pause the playback. - */ - void pause(); -} diff --git a/src/com/android/messaging/ui/RemoteInputEntrypointActivity.java b/src/com/android/messaging/ui/RemoteInputEntrypointActivity.java deleted file mode 100644 index c731164..0000000 --- a/src/com/android/messaging/ui/RemoteInputEntrypointActivity.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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; - -import android.content.Intent; -import android.os.Bundle; -import android.telephony.TelephonyManager; - -import com.android.messaging.datamodel.NoConfirmationSmsSendService; -import com.android.messaging.util.LogUtil; - -public class RemoteInputEntrypointActivity extends BaseBugleActivity { - private static final String TAG = LogUtil.BUGLE_TAG; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - if (intent == null) { - LogUtil.w(TAG, "No intent attached"); - setResult(RESULT_CANCELED); - finish(); - return; - } - - // Perform some action depending on the intent - String action = intent.getAction(); - if (Intent.ACTION_SENDTO.equals(action)) { - // Build and send the intent - final Intent sendIntent = new Intent(this, NoConfirmationSmsSendService.class); - sendIntent.setAction(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE); - sendIntent.putExtras(intent); - // Wear apparently passes all of its extras via the clip data. Must pass it along. - sendIntent.setClipData(intent.getClipData()); - startService(sendIntent); - setResult(RESULT_OK); - } else { - LogUtil.w(TAG, "Unrecognized intent action: " + action); - setResult(RESULT_CANCELED); - } - // This activity should never stick around after processing the intent - finish(); - } -} diff --git a/src/com/android/messaging/ui/SmsStorageLowWarningActivity.java b/src/com/android/messaging/ui/SmsStorageLowWarningActivity.java deleted file mode 100644 index 6b3e84b..0000000 --- a/src/com/android/messaging/ui/SmsStorageLowWarningActivity.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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; - -import android.app.FragmentTransaction; -import android.os.Bundle; - -/** - * Activity to contain the dialog of warning sms storage low. - */ -public class SmsStorageLowWarningActivity extends BaseBugleFragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - FragmentTransaction ft = getFragmentManager().beginTransaction(); - SmsStorageLowWarningFragment fragment = - SmsStorageLowWarningFragment.newInstance(); - ft.add(fragment, null/*tag*/); - ft.commit(); - } -} diff --git a/src/com/android/messaging/ui/SmsStorageLowWarningFragment.java b/src/com/android/messaging/ui/SmsStorageLowWarningFragment.java deleted file mode 100644 index 3ebfdcf..0000000 --- a/src/com/android/messaging/ui/SmsStorageLowWarningFragment.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; -import android.app.FragmentTransaction; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.action.HandleLowStorageAction; -import com.android.messaging.sms.SmsReleaseStorage; -import com.android.messaging.sms.SmsReleaseStorage.Duration; -import com.android.messaging.sms.SmsStorageStatusManager; -import com.android.messaging.util.Assert; -import com.google.common.collect.Lists; - -import java.util.List; - -/** - * Dialog to show the sms storage low warning - */ -public class SmsStorageLowWarningFragment extends Fragment { - private SmsStorageLowWarningFragment() { - } - - public static SmsStorageLowWarningFragment newInstance() { - return new SmsStorageLowWarningFragment(); - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final FragmentTransaction ft = getFragmentManager().beginTransaction(); - final ChooseActionDialogFragment dialog = ChooseActionDialogFragment.newInstance(); - dialog.setTargetFragment(this, 0/*requestCode*/); - dialog.show(ft, null/*tag*/); - } - - /** - * Perform confirm action for a specific action - * - * @param actionIndex - */ - private void confirm(final int actionIndex) { - final FragmentTransaction ft = getFragmentManager().beginTransaction(); - final ConfirmationDialog dialog = ConfirmationDialog.newInstance(actionIndex); - dialog.setTargetFragment(this, 0/*requestCode*/); - dialog.show(ft, null/*tag*/); - } - - /** - * The dialog is cancelled at any step - */ - private void cancel() { - getActivity().finish(); - } - - /** - * The dialog to show for user to choose what delete actions to take when storage is low - */ - private static class ChooseActionDialogFragment extends DialogFragment { - public static ChooseActionDialogFragment newInstance() { - return new ChooseActionDialogFragment(); - } - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - - final LayoutInflater inflater = getActivity().getLayoutInflater(); - final View dialogLayout = inflater.inflate( - R.layout.sms_storage_low_warning_dialog, null); - final ListView actionListView = (ListView) dialogLayout.findViewById( - R.id.free_storage_action_list); - final List<String> actions = loadFreeStorageActions(getActivity().getResources()); - final ActionListAdapter listAdapter = new ActionListAdapter(getActivity(), actions); - actionListView.setAdapter(listAdapter); - - builder.setTitle(R.string.sms_storage_low_title) - .setView(dialogLayout) - .setNegativeButton(R.string.ignore, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - - final Dialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - return dialog; - } - - @Override - public void onCancel(final DialogInterface dialog) { - ((SmsStorageLowWarningFragment) getTargetFragment()).cancel(); - } - - private class ActionListAdapter extends ArrayAdapter<String> { - public ActionListAdapter(final Context context, final List<String> actions) { - super(context, R.layout.sms_free_storage_action_item_view, actions); - } - - @Override - public View getView(final int position, final View view, final ViewGroup parent) { - TextView actionItemView; - if (view == null || !(view instanceof TextView)) { - final LayoutInflater inflater = LayoutInflater.from(getContext()); - actionItemView = (TextView) inflater.inflate( - R.layout.sms_free_storage_action_item_view, parent, false); - } else { - actionItemView = (TextView) view; - } - - final String action = getItem(position); - actionItemView.setText(action); - actionItemView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - dismiss(); - ((SmsStorageLowWarningFragment) getTargetFragment()).confirm(position); - } - }); - return actionItemView; - } - } - } - - private static final String KEY_ACTION_INDEX = "action_index"; - - /** - * The dialog to confirm user's delete action - */ - private static class ConfirmationDialog extends DialogFragment { - private Duration mDuration; - private String mDurationString; - - public static ConfirmationDialog newInstance(final int actionIndex) { - final ConfirmationDialog dialog = new ConfirmationDialog(); - final Bundle args = new Bundle(); - args.putInt(KEY_ACTION_INDEX, actionIndex); - dialog.setArguments(args); - return dialog; - } - - @Override - public void onCancel(final DialogInterface dialog) { - ((SmsStorageLowWarningFragment) getTargetFragment()).cancel(); - } - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - mDuration = SmsReleaseStorage.parseMessageRetainingDuration(); - mDurationString = SmsReleaseStorage.getMessageRetainingDurationString(mDuration); - - final int actionIndex = getArguments().getInt(KEY_ACTION_INDEX); - if (actionIndex < 0 || actionIndex > 1) { - return null; - } - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.sms_storage_low_title) - .setMessage(getConfirmDialogMessage(actionIndex)) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int button) { - dismiss(); - ((SmsStorageLowWarningFragment) getTargetFragment()).cancel(); - } - }) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int button) { - dismiss(); - handleAction(actionIndex); - getActivity().finish(); - SmsStorageStatusManager.cancelStorageLowNotification(); - } - }); - return builder.create(); - } - - private void handleAction(final int actionIndex) { - final long durationInMillis = - SmsReleaseStorage.durationToTimeInMillis(mDuration); - switch (actionIndex) { - case 0: - HandleLowStorageAction.handleDeleteMediaMessages(durationInMillis); - break; - - case 1: - HandleLowStorageAction.handleDeleteOldMessages(durationInMillis); - break; - - default: - Assert.fail("Unsupported action"); - break; - } - } - - /** - * Get the confirm dialog text for a specific delete action - * @param index The action index - * @return - */ - private String getConfirmDialogMessage(final int index) { - switch (index) { - case 0: - return getString(R.string.delete_all_media_confirmation, mDurationString); - case 1: - return getString(R.string.delete_oldest_messages_confirmation, mDurationString); - case 2: - return getString(R.string.auto_delete_oldest_messages_confirmation, - mDurationString); - } - throw new IllegalArgumentException( - "SmsStorageLowWarningFragment: invalid action index " + index); - } - } - - /** - * Load the text of delete message actions - * - * @param resources - * @return - */ - private static List<String> loadFreeStorageActions(final Resources resources) { - final Duration duration = SmsReleaseStorage.parseMessageRetainingDuration(); - final String durationString = SmsReleaseStorage.getMessageRetainingDurationString(duration); - final List<String> actions = Lists.newArrayList(); - actions.add(resources.getString(R.string.delete_all_media)); - actions.add(resources.getString(R.string.delete_oldest_messages, durationString)); - - // TODO: Auto-purging is disabled for Bugle V1. - // actions.add(resources.getString(R.string.auto_delete_oldest_messages, durationString)); - return actions; - } -} diff --git a/src/com/android/messaging/ui/SnackBar.java b/src/com/android/messaging/ui/SnackBar.java deleted file mode 100644 index a278040..0000000 --- a/src/com/android/messaging/ui/SnackBar.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup.MarginLayoutParams; -import android.widget.FrameLayout; -import android.widget.TextView; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.util.Assert; - -import java.util.ArrayList; -import java.util.List; - -public class SnackBar { - public static final int LONG_DURATION_IN_MS = 5000; - public static final int SHORT_DURATION_IN_MS = 1000; - public static final int MAX_DURATION_IN_MS = 10000; - - public interface SnackBarListener { - void onActionClick(); - } - - /** - * Defines an action to be performed when the user clicks on the action button on the snack bar - */ - public static class Action { - private final Runnable mActionRunnable; - private final String mActionLabel; - - public final static int SNACK_BAR_UNDO = 0; - public final static int SNACK_BAR_RETRY = 1; - - private Action(@Nullable Runnable actionRunnable, @Nullable String actionLabel) { - mActionRunnable = actionRunnable; - mActionLabel = actionLabel; - } - - Runnable getActionRunnable() { - return mActionRunnable; - } - - String getActionLabel() { - return mActionLabel; - } - - public static Action createUndoAction(final Runnable undoRunnable) { - return createCustomAction(undoRunnable, Factory.get().getApplicationContext() - .getString(R.string.snack_bar_undo)); - } - - public static Action createRetryAction(final Runnable retryRunnable) { - return createCustomAction(retryRunnable, Factory.get().getApplicationContext() - .getString(R.string.snack_bar_retry)); - } - - - public static Action createCustomAction(final Runnable runnable, final String actionLabel) { - return new Action(runnable, actionLabel); - } - } - - /** - * Defines the placement of the snack bar (e.g. anchored view, anchor gravity). - */ - public static class Placement { - private final View mAnchorView; - private final boolean mAnchorAbove; - - private Placement(@NonNull final View anchorView, final boolean anchorAbove) { - Assert.notNull(anchorView); - mAnchorView = anchorView; - mAnchorAbove = anchorAbove; - } - - public View getAnchorView() { - return mAnchorView; - } - - public boolean getAnchorAbove() { - return mAnchorAbove; - } - - /** - * Anchor the snack bar above the given {@code anchorView}. - */ - public static Placement above(final View anchorView) { - return new Placement(anchorView, true); - } - - /** - * Anchor the snack bar below the given {@code anchorView}. - */ - public static Placement below(final View anchorView) { - return new Placement(anchorView, false); - } - } - - public static class Builder { - private static final List<SnackBarInteraction> NO_INTERACTIONS = - new ArrayList<SnackBarInteraction>(); - - private final Context mContext; - private final SnackBarManager mSnackBarManager; - - private String mSnackBarMessage; - private int mDuration = LONG_DURATION_IN_MS; - private List<SnackBarInteraction> mInteractions = NO_INTERACTIONS; - private Action mAction; - private Placement mPlacement; - // The parent view is only used to get a window token and doesn't affect the layout - private View mParentView; - - public Builder(final SnackBarManager snackBarManager, final View parentView) { - Assert.notNull(snackBarManager); - Assert.notNull(parentView); - mSnackBarManager = snackBarManager; - mContext = parentView.getContext(); - mParentView = parentView; - } - - public Builder setText(final String snackBarMessage) { - Assert.isTrue(!TextUtils.isEmpty(snackBarMessage)); - mSnackBarMessage = snackBarMessage; - return this; - } - - public Builder setAction(final Action action) { - mAction = action; - return this; - } - - /** - * Sets the duration to show this toast for in milliseconds. - */ - public Builder setDuration(final int duration) { - Assert.isTrue(0 < duration && duration < MAX_DURATION_IN_MS); - mDuration = duration; - return this; - } - - /** - * Sets the components that this toast's animation will interact with. These components may - * be animated to make room for the toast. - */ - public Builder withInteractions(final List<SnackBarInteraction> interactions) { - mInteractions = interactions; - return this; - } - - /** - * Place the snack bar with the given placement requirement. - */ - public Builder withPlacement(final Placement placement) { - Assert.isNull(mPlacement); - mPlacement = placement; - return this; - } - - public SnackBar build() { - return new SnackBar(this); - } - - public void show() { - mSnackBarManager.show(build()); - } - } - - private final View mRootView; - private final Context mContext; - private final View mSnackBarView; - private final String mText; - private final int mDuration; - private final List<SnackBarInteraction> mInteractions; - private final Action mAction; - private final Placement mPlacement; - private final TextView mActionTextView; - private final TextView mMessageView; - private final FrameLayout mMessageWrapper; - private final View mParentView; - - private SnackBarListener mListener; - - private SnackBar(final Builder builder) { - mContext = builder.mContext; - mRootView = LayoutInflater.from(mContext).inflate(R.layout.snack_bar, - null /* WindowManager will show this in show() below */); - mSnackBarView = mRootView.findViewById(R.id.snack_bar); - mText = builder.mSnackBarMessage; - mDuration = builder.mDuration; - mAction = builder.mAction; - mPlacement = builder.mPlacement; - mParentView = builder.mParentView; - if (builder.mInteractions == null) { - mInteractions = new ArrayList<SnackBarInteraction>(); - } else { - mInteractions = builder.mInteractions; - } - - mActionTextView = (TextView) mRootView.findViewById(R.id.snack_bar_action); - mMessageView = (TextView) mRootView.findViewById(R.id.snack_bar_message); - mMessageWrapper = (FrameLayout) mRootView.findViewById(R.id.snack_bar_message_wrapper); - - setUpButton(); - setUpTextLines(); - } - - public Context getContext() { - return mContext; - } - - public View getRootView() { - return mRootView; - } - - public View getParentView() { - return mParentView; - } - - public View getSnackBarView() { - return mSnackBarView; - } - - public String getMessageText() { - return mText; - } - - public String getActionLabel() { - if (mAction == null) { - return null; - } - return mAction.getActionLabel(); - } - - public int getDuration() { - return mDuration; - } - - public Placement getPlacement() { - return mPlacement; - } - - public List<SnackBarInteraction> getInteractions() { - return mInteractions; - } - - public void setEnabled(final boolean enabled) { - mActionTextView.setClickable(enabled); - } - - public void setListener(final SnackBarListener snackBarListener) { - mListener = snackBarListener; - } - - private void setUpButton() { - if (mAction == null || mAction.getActionRunnable() == null) { - mActionTextView.setVisibility(View.GONE); - // In the XML layout we add left/right padding to the button to add space between - // the message text and the button and on the right side we add padding to put space - // between the button and the edge of the snack bar. This is so the button can use the - // padding area as part of it's click target. Since we have no button, we need to put - // some margin on the right side. While the left margin is already set on the wrapper, - // we're setting it again to not have to make a special case for RTL. - final MarginLayoutParams lp = (MarginLayoutParams) mMessageWrapper.getLayoutParams(); - final int leftRightMargin = mContext.getResources().getDimensionPixelSize( - R.dimen.snack_bar_left_right_margin); - lp.leftMargin = leftRightMargin; - lp.rightMargin = leftRightMargin; - mMessageWrapper.setLayoutParams(lp); - } else { - mActionTextView.setVisibility(View.VISIBLE); - mActionTextView.setText(mAction.getActionLabel()); - mActionTextView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - mAction.getActionRunnable().run(); - if (mListener != null) { - mListener.onActionClick(); - } - } - }); - } - } - - private void setUpTextLines() { - if (mText == null) { - mMessageView.setVisibility(View.GONE); - } else { - mMessageView.setVisibility(View.VISIBLE); - mMessageView.setText(mText); - } - } -} diff --git a/src/com/android/messaging/ui/SnackBarInteraction.java b/src/com/android/messaging/ui/SnackBarInteraction.java deleted file mode 100644 index f723caa..0000000 --- a/src/com/android/messaging/ui/SnackBarInteraction.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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; - -import android.view.Gravity; -import android.view.View; -import android.view.ViewPropertyAnimator; - -import com.google.common.base.Preconditions; - -/** - * An interface that defines how a component can be animated with an {@link SnackBar}. - */ -public interface SnackBarInteraction { - /** - * Returns the animator that will be run in reaction to the given SnackBar being shown. - * - * Implementations may return null here if it determines that the given SnackBar does not need - * to animate this component. - */ - ViewPropertyAnimator animateOnSnackBarShow(SnackBar snackBar); - - /** - * Returns the animator that will be run in reaction to the given SnackBar being dismissed. - * - * Implementations may return null here if it determines that the given SnackBar does not need - * to animate this component. - */ - ViewPropertyAnimator animateOnSnackBarDismiss(SnackBar snackBar); - - /** - * A basic implementation of {@link SnackBarInteraction} that assumes that the - * {@link SnackBar} is always shown with {@link Gravity#BOTTOM} and that the provided View will - * always need to be translated up to make room for the SnackBar. - */ - public static class BasicSnackBarInteraction implements SnackBarInteraction { - private final View mView; - - public BasicSnackBarInteraction(final View view) { - mView = Preconditions.checkNotNull(view); - } - - @Override - public ViewPropertyAnimator animateOnSnackBarShow(final SnackBar snackBar) { - final View rootView = snackBar.getRootView(); - return mView.animate().translationY(-rootView.getMeasuredHeight()); - } - - @Override - public ViewPropertyAnimator animateOnSnackBarDismiss(final SnackBar snackBar) { - return mView.animate().translationY(0); - } - } -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/SnackBarManager.java b/src/com/android/messaging/ui/SnackBarManager.java deleted file mode 100644 index e107999..0000000 --- a/src/com/android/messaging/ui/SnackBarManager.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.Handler; -import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.MeasureSpec; -import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ViewPropertyAnimator; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.WindowManager; -import android.widget.PopupWindow; -import android.widget.PopupWindow.OnDismissListener; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.ui.SnackBar.Placement; -import com.android.messaging.ui.SnackBar.SnackBarListener; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.Assert; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.TextUtil; -import com.android.messaging.util.UiUtils; -import com.google.common.base.Joiner; - -import java.util.List; - -public class SnackBarManager { - - private static SnackBarManager sInstance; - - public static SnackBarManager get() { - if (sInstance == null) { - synchronized (SnackBarManager.class) { - if (sInstance == null) { - sInstance = new SnackBarManager(); - } - } - } - return sInstance; - } - - private final Runnable mDismissRunnable = new Runnable() { - @Override - public void run() { - dismiss(); - } - }; - - private final OnTouchListener mDismissOnTouchListener = new OnTouchListener() { - @Override - public boolean onTouch(final View view, final MotionEvent event) { - // Dismiss the {@link SnackBar} but don't consume the event. - dismiss(); - return false; - } - }; - - private final SnackBarListener mDismissOnUserTapListener = new SnackBarListener() { - @Override - public void onActionClick() { - dismiss(); - } - }; - - private final int mTranslationDurationMs; - private final Handler mHideHandler; - - private SnackBar mCurrentSnackBar; - private SnackBar mLatestSnackBar; - private SnackBar mNextSnackBar; - private boolean mIsCurrentlyDismissing; - private PopupWindow mPopupWindow; - - private SnackBarManager() { - mTranslationDurationMs = Factory.get().getApplicationContext().getResources().getInteger( - R.integer.snackbar_translation_duration_ms); - mHideHandler = new Handler(); - } - - public SnackBar getLatestSnackBar() { - return mLatestSnackBar; - } - - public SnackBar.Builder newBuilder(final View parentView) { - return new SnackBar.Builder(this, parentView); - } - - /** - * The given snackBar is not guaranteed to be shown. If the previous snackBar is animating away, - * and another snackBar is requested to show after this one, this snackBar will be skipped. - */ - public void show(final SnackBar snackBar) { - Assert.notNull(snackBar); - - if (mCurrentSnackBar != null) { - LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar, but currentSnackBar was not null."); - - // Dismiss the current snack bar. That will cause the next snack bar to be shown on - // completion. - mNextSnackBar = snackBar; - mLatestSnackBar = snackBar; - dismiss(); - return; - } - - mCurrentSnackBar = snackBar; - mLatestSnackBar = snackBar; - - // We want to know when either button was tapped so we can dismiss. - snackBar.setListener(mDismissOnUserTapListener); - - // Cancel previous dismisses & set dismiss for the delay time. - mHideHandler.removeCallbacks(mDismissRunnable); - mHideHandler.postDelayed(mDismissRunnable, snackBar.getDuration()); - - snackBar.setEnabled(false); - - // For some reason, the addView function does not respect layoutParams. - // We need to explicitly set it first here. - final View rootView = snackBar.getRootView(); - - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { - LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar: " + snackBar); - } - // Measure the snack bar root view so we know how much to translate by. - measureSnackBar(snackBar); - mPopupWindow = new PopupWindow(snackBar.getContext()); - mPopupWindow.setWidth(LayoutParams.MATCH_PARENT); - mPopupWindow.setHeight(LayoutParams.WRAP_CONTENT); - mPopupWindow.setBackgroundDrawable(null); - mPopupWindow.setContentView(rootView); - final Placement placement = snackBar.getPlacement(); - if (placement == null) { - mPopupWindow.showAtLocation( - snackBar.getParentView(), Gravity.BOTTOM | Gravity.START, - 0, getScreenBottomOffset(snackBar)); - } else { - final View anchorView = placement.getAnchorView(); - - // You'd expect PopupWindow.showAsDropDown to ensure the popup moves with the anchor - // view, which it does for scrolling, but not layout changes, so we have to manually - // update while the snackbar is showing - final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - mPopupWindow.update(anchorView, 0, getRelativeOffset(snackBar), - anchorView.getWidth(), LayoutParams.WRAP_CONTENT); - } - }; - anchorView.getViewTreeObserver().addOnGlobalLayoutListener(listener); - mPopupWindow.setOnDismissListener(new OnDismissListener() { - @Override - public void onDismiss() { - anchorView.getViewTreeObserver().removeOnGlobalLayoutListener(listener); - } - }); - mPopupWindow.showAsDropDown(anchorView, 0, getRelativeOffset(snackBar)); - } - - - // Animate the toast bar into view. - placeSnackBarOffScreen(snackBar); - animateSnackBarOnScreen(snackBar).withEndAction(new Runnable() { - @Override - public void run() { - mCurrentSnackBar.setEnabled(true); - makeCurrentSnackBarDismissibleOnTouch(); - // Fire an accessibility event as needed - String snackBarText = snackBar.getMessageText(); - if (!TextUtils.isEmpty(snackBarText) && - TextUtils.getTrimmedLength(snackBarText) > 0) { - snackBarText = snackBarText.trim(); - final String snackBarActionText = snackBar.getActionLabel(); - if (!TextUtil.isAllWhitespace(snackBarActionText)) { - snackBarText = Joiner.on(", ").join(snackBarText, snackBarActionText); - } - AccessibilityUtil.announceForAccessibilityCompat(snackBar.getSnackBarView(), - null /*accessibilityManager*/, snackBarText); - } - } - }); - - // Animate any interaction views out of the way. - animateInteractionsOnShow(snackBar); - } - - /** - * Dismisses the current toast that is showing. If there is a toast waiting to be shown, that - * toast will be shown when the current one has been dismissed. - */ - public void dismiss() { - mHideHandler.removeCallbacks(mDismissRunnable); - - if (mCurrentSnackBar == null || mIsCurrentlyDismissing) { - return; - } - - final SnackBar snackBar = mCurrentSnackBar; - - LogUtil.d(LogUtil.BUGLE_TAG, "Dismissing snack bar."); - mIsCurrentlyDismissing = true; - - snackBar.setEnabled(false); - - // Animate the toast bar down. - final View rootView = snackBar.getRootView(); - animateSnackBarOffScreen(snackBar).withEndAction(new Runnable() { - @Override - public void run() { - rootView.setVisibility(View.GONE); - try { - mPopupWindow.dismiss(); - } catch (IllegalArgumentException e) { - // PopupWindow.dismiss() will fire an IllegalArgumentException if the activity - // has already ended while we were animating - } - - mCurrentSnackBar = null; - mIsCurrentlyDismissing = false; - - // Show the next toast if one is waiting. - if (mNextSnackBar != null) { - final SnackBar localNextSnackBar = mNextSnackBar; - mNextSnackBar = null; - show(localNextSnackBar); - } - } - }); - - // Animate any interaction views back. - animateInteractionsOnDismiss(snackBar); - } - - private void makeCurrentSnackBarDismissibleOnTouch() { - // Set touching on the entire view, the {@link SnackBar} itself, as - // well as the button's dismiss the toast. - mCurrentSnackBar.getRootView().setOnTouchListener(mDismissOnTouchListener); - mCurrentSnackBar.getSnackBarView().setOnTouchListener(mDismissOnTouchListener); - } - - private void measureSnackBar(final SnackBar snackBar) { - final View rootView = snackBar.getRootView(); - final Point displaySize = new Point(); - getWindowManager(snackBar.getContext()).getDefaultDisplay().getSize(displaySize); - final int widthSpec = ViewGroup.getChildMeasureSpec( - MeasureSpec.makeMeasureSpec(displaySize.x, MeasureSpec.EXACTLY), - 0, LayoutParams.MATCH_PARENT); - final int heightSpec = ViewGroup.getChildMeasureSpec( - MeasureSpec.makeMeasureSpec(displaySize.y, MeasureSpec.EXACTLY), - 0, LayoutParams.WRAP_CONTENT); - rootView.measure(widthSpec, heightSpec); - } - - private void placeSnackBarOffScreen(final SnackBar snackBar) { - final View rootView = snackBar.getRootView(); - final View snackBarView = snackBar.getSnackBarView(); - snackBarView.setTranslationY(rootView.getMeasuredHeight()); - } - - private ViewPropertyAnimator animateSnackBarOnScreen(final SnackBar snackBar) { - final View snackBarView = snackBar.getSnackBarView(); - return normalizeAnimator(snackBarView.animate()).translationX(0).translationY(0); - } - - private ViewPropertyAnimator animateSnackBarOffScreen(final SnackBar snackBar) { - final View rootView = snackBar.getRootView(); - final View snackBarView = snackBar.getSnackBarView(); - return normalizeAnimator(snackBarView.animate()).translationY(rootView.getHeight()); - } - - private void animateInteractionsOnShow(final SnackBar snackBar) { - final List<SnackBarInteraction> interactions = snackBar.getInteractions(); - for (final SnackBarInteraction interaction : interactions) { - if (interaction != null) { - final ViewPropertyAnimator animator = interaction.animateOnSnackBarShow(snackBar); - if (animator != null) { - normalizeAnimator(animator); - } - } - } - } - - private void animateInteractionsOnDismiss(final SnackBar snackBar) { - final List<SnackBarInteraction> interactions = snackBar.getInteractions(); - for (final SnackBarInteraction interaction : interactions) { - if (interaction != null) { - final ViewPropertyAnimator animator = - interaction.animateOnSnackBarDismiss(snackBar); - if (animator != null) { - normalizeAnimator(animator); - } - } - } - } - - private ViewPropertyAnimator normalizeAnimator(final ViewPropertyAnimator animator) { - return animator - .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR) - .setDuration(mTranslationDurationMs); - } - - private WindowManager getWindowManager(final Context context) { - return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - } - - /** - * Get the offset from the bottom of the screen where the snack bar should be placed. - */ - private int getScreenBottomOffset(final SnackBar snackBar) { - final WindowManager windowManager = getWindowManager(snackBar.getContext()); - final DisplayMetrics displayMetrics = new DisplayMetrics(); - if (OsUtil.isAtLeastL()) { - windowManager.getDefaultDisplay().getRealMetrics(displayMetrics); - } else { - windowManager.getDefaultDisplay().getMetrics(displayMetrics); - } - final int screenHeight = displayMetrics.heightPixels; - - if (OsUtil.isAtLeastL()) { - // In L, the navigation bar is included in the space for the popup window, so we have to - // offset by the size of the navigation bar - final Rect displayRect = new Rect(); - snackBar.getParentView().getRootView().getWindowVisibleDisplayFrame(displayRect); - return screenHeight - displayRect.bottom; - } - - return 0; - } - - private int getRelativeOffset(final SnackBar snackBar) { - final Placement placement = snackBar.getPlacement(); - Assert.notNull(placement); - final View anchorView = placement.getAnchorView(); - if (placement.getAnchorAbove()) { - return -snackBar.getRootView().getMeasuredHeight() - anchorView.getHeight(); - } else { - // Use the default dropdown positioning - return 0; - } - } -} diff --git a/src/com/android/messaging/ui/TestActivity.java b/src/com/android/messaging/ui/TestActivity.java deleted file mode 100644 index 1693660..0000000 --- a/src/com/android/messaging/ui/TestActivity.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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; - -import android.app.Fragment; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.view.View; -import android.view.WindowManager; -import android.widget.FrameLayout; - -import com.android.messaging.R; -import com.android.messaging.util.LogUtil; -import com.google.common.annotations.VisibleForTesting; - -/** - * An empty activity that can be used to host a fragment or view for unit testing purposes. Lives in - * app code vs test code due to requirement of ActivityInstrumentationTestCase2. - */ -public class TestActivity extends FragmentActivity { - private FragmentEventListener mFragmentEventListener; - - public interface FragmentEventListener { - public void onAttachFragment(Fragment fragment); - } - - @Override - protected void onCreate(final Bundle bundle) { - super.onCreate(bundle); - - if (bundle != null) { - // The test case may have configured the fragment, and recreating the activity will - // lose that configuration. The real activity is the only activity that would know - // how to reapply that configuration. - throw new IllegalStateException("TestActivity cannot get recreated"); - } - - // There is a race condition, but this often makes it possible for tests to run with the - // key guard up - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | - WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - - setContentView(R.layout.test_activity); - } - - @VisibleForTesting - public void setFragment(final Fragment fragment) { - LogUtil.i(LogUtil.BUGLE_TAG, "TestActivity.setFragment"); - getFragmentManager() - .beginTransaction() - .replace(R.id.test_content, fragment) - .commit(); - getFragmentManager().executePendingTransactions(); - } - - @VisibleForTesting - public void setView(final View view) { - LogUtil.i(LogUtil.BUGLE_TAG, "TestActivity.setView"); - ((FrameLayout) findViewById(R.id.test_content)).addView(view); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - if (mFragmentEventListener != null) { - mFragmentEventListener.onAttachFragment(fragment); - } - } - - public void setFragmentEventListener(final FragmentEventListener fragmentEventListener) { - mFragmentEventListener = fragmentEventListener; - } -} diff --git a/src/com/android/messaging/ui/UIIntents.java b/src/com/android/messaging/ui/UIIntents.java deleted file mode 100644 index e5f8a52..0000000 --- a/src/com/android/messaging/ui/UIIntents.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * 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; - -import android.app.Activity; -import android.app.Fragment; -import android.app.PendingIntent; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.graphics.Point; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.util.ConversationIdSet; - -/** - * A central repository of Intents used to start activities. - */ -public abstract class UIIntents { - public static UIIntents get() { - return Factory.get().getUIIntents(); - } - - // Intent extras - public static final String UI_INTENT_EXTRA_CONVERSATION_ID = "conversation_id"; - - // Sending draft data (from share intent / message forwarding) to the ConversationActivity. - public static final String UI_INTENT_EXTRA_DRAFT_DATA = "draft_data"; - - // The request code for picking image from the Document picker. - public static final int REQUEST_PICK_IMAGE_FROM_DOCUMENT_PICKER = 1400; - - // Indicates what type of notification this applies to (See BugleNotifications: - // UPDATE_NONE, UPDATE_MESSAGES, UPDATE_ERRORS, UPDATE_ALL) - public static final String UI_INTENT_EXTRA_NOTIFICATIONS_UPDATE = "notifications_update"; - - // Pass a set of conversation id's. - public static final String UI_INTENT_EXTRA_CONVERSATION_ID_SET = "conversation_id_set"; - - // Sending class zero message to its activity - public static final String UI_INTENT_EXTRA_MESSAGE_VALUES = "message_values"; - - // For the widget to go to the ConversationList from the Conversation. - public static final String UI_INTENT_EXTRA_GOTO_CONVERSATION_LIST = "goto_conv_list"; - - // Indicates whether a conversation is launched with custom transition. - public static final String UI_INTENT_EXTRA_WITH_CUSTOM_TRANSITION = "with_custom_transition"; - - public static final String ACTION_RESET_NOTIFICATIONS = - "com.android.messaging.reset_notifications"; - - // Sending VCard uri to VCard detail activity - public static final String UI_INTENT_EXTRA_VCARD_URI = "vcard_uri"; - - public static final String CMAS_COMPONENT = "com.android.cellbroadcastreceiver"; - - // Intent action for local broadcast receiver for conversation self id change. - public static final String CONVERSATION_SELF_ID_CHANGE_BROADCAST_ACTION = - "conversation_self_id_change"; - - // Conversation self id - public static final String UI_INTENT_EXTRA_CONVERSATION_SELF_ID = "conversation_self_id"; - - // For opening an APN editor on a particular row in the apn database. - public static final String UI_INTENT_EXTRA_APN_ROW_ID = "apn_row_id"; - - // Subscription id - public static final String UI_INTENT_EXTRA_SUB_ID = "sub_id"; - - // Per-Subscription setting activity title - public static final String UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE = - "per_sub_setting_title"; - - // Is application settings launched as the top level settings activity? - public static final String UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS = "top_level_settings"; - - // Sending attachment uri from widget - public static final String UI_INTENT_EXTRA_ATTACHMENT_URI = "attachment_uri"; - - // Sending attachment content type from widget - public static final String UI_INTENT_EXTRA_ATTACHMENT_TYPE = "attachment_type"; - - public static final String ACTION_WIDGET_CONVERSATION = - "com.android.messaging.widget_conversation:"; - - public static final String UI_INTENT_EXTRA_REQUIRES_MMS = "requires_mms"; - - public static final String UI_INTENT_EXTRA_SELF_ID = "self_id"; - - // Message position to scroll to. - public static final String UI_INTENT_EXTRA_MESSAGE_POSITION = "message_position"; - - /** - * Launch the permission check activity - */ - public abstract void launchPermissionCheckActivity(final Context context); - - public abstract void launchConversationListActivity(final Context context); - - /** - * Launch an activity to show a conversation. This method by default provides no additional - * activity options. - */ - public void launchConversationActivity(final Context context, - final String conversationId, final MessageData draft) { - launchConversationActivity(context, conversationId, draft, null, - false /* withCustomTransition */); - } - - /** - * Launch an activity to show a conversation. - */ - public abstract void launchConversationActivity(final Context context, - final String conversationId, final MessageData draft, final Bundle activityOptions, - final boolean withCustomTransition); - - - /** - * Launch an activity to show conversation with conversation list in back stack. - */ - public abstract void launchConversationActivityWithParentStack(Context context, - String conversationId, String smsBody); - - /** - * Launch an activity to show a conversation as a new task. - */ - public abstract void launchConversationActivityNewTask(final Context context, - final String conversationId); - - /** - * Launch an activity to start a new conversation - */ - public abstract void launchCreateNewConversationActivity(final Context context, - final MessageData draft); - - /** - * Launch debug activity to set MMS config options. - */ - public abstract void launchDebugMmsConfigActivity(final Context context); - - /** - * Launch an activity to change settings. - */ - public abstract void launchSettingsActivity(final Context context); - - /** - * Launch an activity to add a contact with a given destination. - */ - public abstract void launchAddContactActivity(final Context context, final String destination); - - /** - * Launch an activity to show the document picker to pick an image. - * @param fragment the requesting fragment - */ - public abstract void launchDocumentImagePicker(final Fragment fragment); - - /** - * Launch an activity to show people & options for a given conversation. - */ - public abstract void launchPeopleAndOptionsActivity(final Activity context, - final String conversationId); - - /** - * Launch an external activity to handle a phone call - * @param phoneNumber the phone number to call - * @param clickPosition is the location tapped to start this launch for transition use - */ - public abstract void launchPhoneCallActivity(final Context context, final String phoneNumber, - final Point clickPosition); - - /** - * Launch an activity to show archived conversations. - */ - public abstract void launchArchivedConversationsActivity(final Context context); - - /** - * Launch an activity to show blocked participants. - */ - public abstract void launchBlockedParticipantsActivity(final Context context); - - /** - * Launch an activity to show a class zero message - */ - public abstract void launchClassZeroActivity(Context context, ContentValues messageValues); - - /** - * Launch an activity to let the user forward a message - */ - public abstract void launchForwardMessageActivity(Context context, MessageData message); - - /** - * Launch an activity to show details for a VCard - */ - public abstract void launchVCardDetailActivity(Context context, Uri vcardUri); - - /** - * Launch an external activity that handles the intent to add VCard to contacts - */ - public abstract void launchSaveVCardToContactsActivity(Context context, Uri vcardUri); - - /** - * Launch an activity to let the user select & unselect the list of attachments to send. - */ - public abstract void launchAttachmentChooserActivity(final Activity activity, - final String conversationId, final int requestCode); - - /** - * Launch full screen video viewer. - */ - public abstract void launchFullScreenVideoViewer(Context context, Uri videoUri); - - /** - * Launch full screen photo viewer. - */ - public abstract void launchFullScreenPhotoViewer(Activity activity, Uri initialPhoto, - Rect initialPhotoBounds, Uri photosUri); - - /** - * Launch an activity to show general app settings - * @param topLevel indicates whether the app settings is launched as the top-level settings - * activity (instead of SettingsActivity which shows a collapsed view of the app - * settings + one settings item per subscription). This is true when there's only one - * active SIM in the system so we can show this activity directly. - */ - public abstract void launchApplicationSettingsActivity(Context context, boolean topLevel); - - /** - * Launch an activity to show per-subscription settings - */ - public abstract void launchPerSubscriptionSettingsActivity(Context context, int subId, - String settingTitle); - - /** - * Get a ACTION_VIEW intent - * @param url display the data in the url to users - */ - public abstract Intent getViewUrlIntent(final String url); - - /** - * Get an intent to launch the ringtone picker - * @param title the title to show in the ringtone picker - * @param existingUri the currently set uri - * @param defaultUri the default uri if none is currently set - * @param toneType type of ringtone to pick, maybe any of RingtoneManager.TYPE_* - */ - public abstract Intent getRingtonePickerIntent(final String title, final Uri existingUri, - final Uri defaultUri, final int toneType); - - /** - * Get an intent to launch the wireless alert viewer. - */ - public abstract Intent getWirelessAlertsIntent(); - - /** - * Get an intent to launch the dialog for changing the default SMS App. - */ - public abstract Intent getChangeDefaultSmsAppIntent(final Activity activity); - - /** - * Broadcast conversation self id change so it may be reflected in the message compose UI. - */ - public abstract void broadcastConversationSelfIdChange(final Context context, - final String conversationId, final String conversationSelfId); - - /** - * Get a PendingIntent for starting conversation list from notifications. - */ - public abstract PendingIntent getPendingIntentForConversationListActivity( - final Context context); - - /** - * Get a PendingIntent for starting conversation list from widget. - */ - public abstract PendingIntent getWidgetPendingIntentForConversationListActivity( - final Context context); - - /** - * Get a PendingIntent for showing a conversation from notifications. - */ - public abstract PendingIntent getPendingIntentForConversationActivity(final Context context, - final String conversationId, final MessageData draft); - - /** - * Get an Intent for showing a conversation from the widget. - */ - public abstract Intent getIntentForConversationActivity(final Context context, - final String conversationId, final MessageData draft); - - /** - * Get a PendingIntent for sending a message to a conversation, without opening the Bugle UI. - * - * <p>This is intended to be used by the Android Wear companion app when sending transcribed - * voice replies. - */ - public abstract PendingIntent getPendingIntentForSendingMessageToConversation( - final Context context, final String conversationId, final String selfId, - final boolean requiresMms, final int requestCode); - - /** - * Get a PendingIntent for clearing notifications. - * - * <p>This is intended to be used by notifications. - */ - public abstract PendingIntent getPendingIntentForClearingNotifications(final Context context, - final int updateTargets, final ConversationIdSet conversationIdSet, - final int requestCode); - - /** - * Get a PendingIntent for showing low storage notifications. - */ - public abstract PendingIntent getPendingIntentForLowStorageNotifications(final Context context); - - /** - * Get a PendingIntent for showing a new message to a secondary user. - */ - public abstract PendingIntent getPendingIntentForSecondaryUserNewMessageNotification( - final Context context); - - /** - * Get an intent for showing the APN editor. - */ - public abstract Intent getApnEditorIntent(final Context context, final String rowId, int subId); - - /** - * Get an intent for showing the APN settings. - */ - public abstract Intent getApnSettingsIntent(final Context context, final int subId); - - /** - * Get an intent for showing advanced settings. - */ - public abstract Intent getAdvancedSettingsIntent(final Context context); - - /** - * Get an intent for the LaunchConversationActivity. - */ - public abstract Intent getLaunchConversationActivityIntent(final Context context); - - /** - * Tell MediaScanner to re-scan the specified volume. - */ - public abstract void kickMediaScanner(final Context context, final String volume); - - /** - * Launch to browser for a url. - */ - public abstract void launchBrowserForUrl(final Context context, final String url); - - /** - * Get a PendingIntent for the widget conversation template. - */ - public abstract PendingIntent getWidgetPendingIntentForConversationActivity( - final Context context, final String conversationId, final int requestCode); - - /** - * Get a PendingIntent for the conversation widget configuration activity template. - */ - public abstract PendingIntent getWidgetPendingIntentForConfigurationActivity( - final Context context, final int appWidgetId); - -} diff --git a/src/com/android/messaging/ui/UIIntentsImpl.java b/src/com/android/messaging/ui/UIIntentsImpl.java deleted file mode 100644 index b7db719..0000000 --- a/src/com/android/messaging/ui/UIIntentsImpl.java +++ /dev/null @@ -1,577 +0,0 @@ -/* - * 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; - -import android.app.Activity; -import android.app.Fragment; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ComponentName; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.graphics.Point; -import android.graphics.Rect; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Intents; -import android.provider.MediaStore; -import android.provider.Telephony; -import android.support.annotation.Nullable; -import android.support.v4.app.TaskStackBuilder; -import android.support.v4.content.LocalBroadcastManager; -import android.text.TextUtils; - -import com.android.ex.photo.Intents.PhotoViewIntentBuilder; -import com.android.messaging.R; -import com.android.messaging.datamodel.ConversationImagePartsView; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.receiver.NotificationReceiver; -import com.android.messaging.sms.MmsSmsUtils; -import com.android.messaging.ui.appsettings.ApnEditorActivity; -import com.android.messaging.ui.appsettings.ApnSettingsActivity; -import com.android.messaging.ui.appsettings.ApplicationSettingsActivity; -import com.android.messaging.ui.appsettings.PerSubscriptionSettingsActivity; -import com.android.messaging.ui.appsettings.SettingsActivity; -import com.android.messaging.ui.attachmentchooser.AttachmentChooserActivity; -import com.android.messaging.ui.conversation.ConversationActivity; -import com.android.messaging.ui.conversation.LaunchConversationActivity; -import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity; -import com.android.messaging.ui.conversationlist.ConversationListActivity; -import com.android.messaging.ui.conversationlist.ForwardMessageActivity; -import com.android.messaging.ui.conversationsettings.PeopleAndOptionsActivity; -import com.android.messaging.ui.debug.DebugMmsConfigActivity; -import com.android.messaging.ui.photoviewer.BuglePhotoViewActivity; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.ConversationIdSet; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.UiUtils; -import com.android.messaging.util.UriUtil; - -/** - * A central repository of Intents used to start activities. - */ -public class UIIntentsImpl extends UIIntents { - private static final String CELL_BROADCAST_LIST_ACTIVITY = - "com.android.cellbroadcastreceiver.CellBroadcastListActivity"; - private static final String CALL_TARGET_CLICK_KEY = "touchPoint"; - private static final String CALL_TARGET_CLICK_EXTRA_KEY = - "android.telecom.extra.OUTGOING_CALL_EXTRAS"; - private static final String MEDIA_SCANNER_CLASS = - "com.android.providers.media.MediaScannerService"; - private static final String MEDIA_SCANNER_PACKAGE = "com.android.providers.media"; - private static final String MEDIA_SCANNER_SCAN_ACTION = "android.media.IMediaScannerService"; - - /** - * Get an intent which takes you to a conversation - */ - private Intent getConversationActivityIntent(final Context context, - final String conversationId, final MessageData draft, - final boolean withCustomTransition) { - final Intent intent = new Intent(context, ConversationActivity.class); - - // Always try to reuse the same ConversationActivity in the current task so that we don't - // have two conversation activities in the back stack. - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - - // Otherwise we're starting a new conversation - if (conversationId != null) { - intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); - } - if (draft != null) { - intent.putExtra(UI_INTENT_EXTRA_DRAFT_DATA, draft); - - // If draft attachments came from an external content provider via a share intent, we - // need to propagate the URI permissions through to ConversationActivity. This requires - // putting the URIs into the ClipData (setData also works, but accepts only one URI). - ClipData clipData = null; - for (final MessagePartData partData : draft.getParts()) { - if (partData.isAttachment()) { - final Uri uri = partData.getContentUri(); - if (clipData == null) { - clipData = ClipData.newRawUri("Attachments", uri); - } else { - clipData.addItem(new ClipData.Item(uri)); - } - } - } - if (clipData != null) { - intent.setClipData(clipData); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - if (withCustomTransition) { - intent.putExtra(UI_INTENT_EXTRA_WITH_CUSTOM_TRANSITION, true); - } - - if (!(context instanceof Activity)) { - // If the caller supplies an application context, and not an activity context, we must - // include this flag - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - return intent; - } - - @Override - public void launchPermissionCheckActivity(final Context context) { - final Intent intent = new Intent(context, PermissionCheckActivity.class); - context.startActivity(intent); - } - - /** - * Get an intent which takes you to the conversation list - */ - private Intent getConversationListActivityIntent(final Context context) { - return new Intent(context, ConversationListActivity.class); - } - - @Override - public void launchConversationListActivity(final Context context) { - final Intent intent = getConversationListActivityIntent(context); - context.startActivity(intent); - } - - /** - * Get an intent which shows the low storage warning activity. - */ - private Intent getSmsStorageLowWarningActivityIntent(final Context context) { - return new Intent(context, SmsStorageLowWarningActivity.class); - } - - @Override - public void launchConversationActivity(final Context context, - final String conversationId, final MessageData draft, final Bundle activityOptions, - final boolean withCustomTransition) { - Assert.isTrue(!withCustomTransition || activityOptions != null); - final Intent intent = getConversationActivityIntent(context, conversationId, draft, - withCustomTransition); - context.startActivity(intent, activityOptions); - } - - @Override - public void launchConversationActivityNewTask( - final Context context, final String conversationId) { - final Intent intent = getConversationActivityIntent(context, conversationId, null, - false /* withCustomTransition */); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - - @Override - public void launchConversationActivityWithParentStack(final Context context, - final String conversationId, final String smsBody) { - final MessageData messageData = TextUtils.isEmpty(smsBody) - ? null - : MessageData.createDraftSmsMessage(conversationId, null, smsBody); - TaskStackBuilder.create(context) - .addNextIntentWithParentStack( - getConversationActivityIntent(context, conversationId, messageData, - false /* withCustomTransition */)) - .startActivities(); - } - - @Override - public void launchCreateNewConversationActivity(final Context context, - final MessageData draft) { - final Intent intent = getConversationActivityIntent(context, null, draft, - false /* withCustomTransition */); - context.startActivity(intent); - } - - @Override - public void launchDebugMmsConfigActivity(final Context context) { - context.startActivity(new Intent(context, DebugMmsConfigActivity.class)); - } - - @Override - public void launchAddContactActivity(final Context context, final String destination) { - final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - final String destinationType = MmsSmsUtils.isEmailAddress(destination) ? - Intents.Insert.EMAIL : Intents.Insert.PHONE; - intent.setType(Contacts.CONTENT_ITEM_TYPE); - intent.putExtra(destinationType, destination); - startExternalActivity(context, intent); - } - - @Override - public void launchSettingsActivity(final Context context) { - final Intent intent = new Intent(context, SettingsActivity.class); - context.startActivity(intent); - } - - @Override - public void launchArchivedConversationsActivity(final Context context) { - final Intent intent = new Intent(context, ArchivedConversationListActivity.class); - context.startActivity(intent); - } - - @Override - public void launchBlockedParticipantsActivity(final Context context) { - final Intent intent = new Intent(context, BlockedParticipantsActivity.class); - context.startActivity(intent); - } - - @Override - public void launchDocumentImagePicker(final Fragment fragment) { - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.putExtra(Intent.EXTRA_MIME_TYPES, MessagePartData.ACCEPTABLE_IMAGE_TYPES); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(ContentType.IMAGE_UNSPECIFIED); - - fragment.startActivityForResult(intent, REQUEST_PICK_IMAGE_FROM_DOCUMENT_PICKER); - } - - @Override - public void launchPeopleAndOptionsActivity(final Activity activity, - final String conversationId) { - final Intent intent = new Intent(activity, PeopleAndOptionsActivity.class); - intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); - activity.startActivityForResult(intent, 0); - } - - @Override - public void launchPhoneCallActivity(final Context context, final String phoneNumber, - final Point clickPosition) { - final Intent intent = new Intent(Intent.ACTION_CALL, - Uri.parse(UriUtil.SCHEME_TEL + phoneNumber)); - final Bundle extras = new Bundle(); - extras.putParcelable(CALL_TARGET_CLICK_KEY, clickPosition); - intent.putExtra(CALL_TARGET_CLICK_EXTRA_KEY, extras); - startExternalActivity(context, intent); - } - - @Override - public void launchClassZeroActivity(final Context context, final ContentValues messageValues) { - final Intent classZeroIntent = new Intent(context, ClassZeroActivity.class) - .putExtra(UI_INTENT_EXTRA_MESSAGE_VALUES, messageValues) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - context.startActivity(classZeroIntent); - } - - @Override - public void launchForwardMessageActivity(final Context context, final MessageData message) { - final Intent forwardMessageIntent = new Intent(context, ForwardMessageActivity.class) - .putExtra(UI_INTENT_EXTRA_DRAFT_DATA, message); - context.startActivity(forwardMessageIntent); - } - - @Override - public void launchVCardDetailActivity(final Context context, final Uri vcardUri) { - final Intent vcardDetailIntent = new Intent(context, VCardDetailActivity.class) - .putExtra(UI_INTENT_EXTRA_VCARD_URI, vcardUri); - context.startActivity(vcardDetailIntent); - } - - @Override - public void launchSaveVCardToContactsActivity(final Context context, final Uri vcardUri) { - Assert.isTrue(MediaScratchFileProvider.isMediaScratchSpaceUri(vcardUri)); - final Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(vcardUri, ContentType.TEXT_VCARD.toLowerCase()); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startExternalActivity(context, intent); - } - - @Override - public void launchAttachmentChooserActivity(final Activity activity, - final String conversationId, final int requestCode) { - final Intent intent = new Intent(activity, AttachmentChooserActivity.class); - intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); - activity.startActivityForResult(intent, requestCode); - } - - @Override - public void launchFullScreenVideoViewer(final Context context, final Uri videoUri) { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - - // So we don't see "surrounding" images in Gallery - intent.putExtra("SingleItemOnly", true); - intent.setDataAndType(videoUri, ContentType.VIDEO_UNSPECIFIED); - startExternalActivity(context, intent); - } - - @Override - public void launchFullScreenPhotoViewer(final Activity activity, final Uri initialPhoto, - final Rect initialPhotoBounds, final Uri photosUri) { - final PhotoViewIntentBuilder builder = - com.android.ex.photo.Intents.newPhotoViewIntentBuilder( - activity, BuglePhotoViewActivity.class); - builder.setPhotosUri(photosUri.toString()); - builder.setInitialPhotoUri(initialPhoto.toString()); - builder.setProjection(ConversationImagePartsView.PhotoViewQuery.PROJECTION); - - // Set the location of the imageView so that the photoviewer can animate from that location - // to full screen. - builder.setScaleAnimation(initialPhotoBounds.left, initialPhotoBounds.top, - initialPhotoBounds.width(), initialPhotoBounds.height()); - - builder.setDisplayThumbsFullScreen(false); - builder.setMaxInitialScale(8); - activity.startActivity(builder.build()); - activity.overridePendingTransition(0, 0); - } - - @Override - public void launchApplicationSettingsActivity(final Context context, final boolean topLevel) { - final Intent intent = new Intent(context, ApplicationSettingsActivity.class); - intent.putExtra(UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, topLevel); - context.startActivity(intent); - } - - @Override - public void launchPerSubscriptionSettingsActivity(final Context context, final int subId, - final String settingTitle) { - final Intent intent = getPerSubscriptionSettingsIntent(context, subId, settingTitle); - context.startActivity(intent); - } - - @Override - public Intent getViewUrlIntent(final String url) { - final Uri uri = Uri.parse(url); - return new Intent(Intent.ACTION_VIEW, uri); - } - - @Override - public void broadcastConversationSelfIdChange(final Context context, - final String conversationId, final String conversationSelfId) { - final Intent intent = new Intent(CONVERSATION_SELF_ID_CHANGE_BROADCAST_ACTION); - intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); - intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_SELF_ID, conversationSelfId); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - - @Override - public PendingIntent getPendingIntentForConversationListActivity(final Context context) { - final Intent intent = getConversationListActivityIntent(context); - return getPendingIntentWithParentStack(context, intent, 0); - } - - @Override - public PendingIntent getPendingIntentForConversationActivity(final Context context, - final String conversationId, final MessageData draft) { - final Intent intent = getConversationActivityIntent(context, conversationId, draft, - false /* withCustomTransition */); - // Ensure that the platform doesn't reuse PendingIntents across conversations - intent.setData(MessagingContentProvider.buildConversationMetadataUri(conversationId)); - return getPendingIntentWithParentStack(context, intent, 0); - } - - @Override - public Intent getIntentForConversationActivity(final Context context, - final String conversationId, final MessageData draft) { - final Intent intent = getConversationActivityIntent(context, conversationId, draft, - false /* withCustomTransition */); - return intent; - } - - @Override - public PendingIntent getPendingIntentForSendingMessageToConversation(final Context context, - final String conversationId, final String selfId, final boolean requiresMms, - final int requestCode) { - final Intent intent = new Intent(context, RemoteInputEntrypointActivity.class); - intent.setAction(Intent.ACTION_SENDTO); - // Ensure that the platform doesn't reuse PendingIntents across conversations - intent.setData(MessagingContentProvider.buildConversationMetadataUri(conversationId)); - intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); - intent.putExtra(UIIntents.UI_INTENT_EXTRA_SELF_ID, selfId); - intent.putExtra(UIIntents.UI_INTENT_EXTRA_REQUIRES_MMS, requiresMms); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - return getPendingIntentWithParentStack(context, intent, requestCode); - } - - @Override - public PendingIntent getPendingIntentForClearingNotifications(final Context context, - final int updateTargets, final ConversationIdSet conversationIdSet, - final int requestCode) { - final Intent intent = new Intent(context, NotificationReceiver.class); - intent.setAction(ACTION_RESET_NOTIFICATIONS); - intent.putExtra(UI_INTENT_EXTRA_NOTIFICATIONS_UPDATE, updateTargets); - if (conversationIdSet != null) { - intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID_SET, - conversationIdSet.getDelimitedString()); - } - return PendingIntent.getBroadcast(context, - requestCode, intent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - /** - * Gets a PendingIntent associated with an Intent to start an Activity. All notifications - * that starts an Activity must use this method to get a PendingIntent, which achieves two - * goals: - * 1. The target activities will be created, with any existing ones destroyed. This ensures - * we don't end up with multiple instances of ConversationListActivity, for example. - * 2. The target activity, when launched, will have its backstack correctly constructed so - * back navigation will work correctly. - */ - private static PendingIntent getPendingIntentWithParentStack(final Context context, - final Intent intent, final int requestCode) { - final TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); - // Adds the back stack for the Intent (plus the Intent itself) - stackBuilder.addNextIntentWithParentStack(intent); - final PendingIntent resultPendingIntent = - stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT); - return resultPendingIntent; - } - - @Override - public Intent getRingtonePickerIntent(final String title, final Uri existingUri, - final Uri defaultUri, final int toneType) { - return new Intent(RingtoneManager.ACTION_RINGTONE_PICKER) - .putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, toneType) - .putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, title) - .putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existingUri) - .putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri); - } - - @Override - public PendingIntent getPendingIntentForLowStorageNotifications(final Context context) { - final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); - final Intent conversationListIntent = getConversationListActivityIntent(context); - taskStackBuilder.addNextIntent(conversationListIntent); - taskStackBuilder.addNextIntentWithParentStack( - getSmsStorageLowWarningActivityIntent(context)); - - return taskStackBuilder.getPendingIntent( - 0, PendingIntent.FLAG_UPDATE_CURRENT); - } - - @Override - public PendingIntent getPendingIntentForSecondaryUserNewMessageNotification( - final Context context) { - return getPendingIntentForConversationListActivity(context); - } - - @Override - public Intent getWirelessAlertsIntent() { - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(new ComponentName(CMAS_COMPONENT, CELL_BROADCAST_LIST_ACTIVITY)); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } - - @Override - public Intent getApnEditorIntent(final Context context, final String rowId, final int subId) { - final Intent intent = new Intent(context, ApnEditorActivity.class); - intent.putExtra(UI_INTENT_EXTRA_APN_ROW_ID, rowId); - intent.putExtra(UI_INTENT_EXTRA_SUB_ID, subId); - return intent; - } - - @Override - public Intent getApnSettingsIntent(final Context context, final int subId) { - final Intent intent = new Intent(context, ApnSettingsActivity.class) - .putExtra(UI_INTENT_EXTRA_SUB_ID, subId); - return intent; - } - - @Override - public Intent getAdvancedSettingsIntent(final Context context) { - return getPerSubscriptionSettingsIntent(context, ParticipantData.DEFAULT_SELF_SUB_ID, null); - } - - @Override - public Intent getChangeDefaultSmsAppIntent(final Activity activity) { - final Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); - intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, activity.getPackageName()); - return intent; - } - - @Override - public void launchBrowserForUrl(final Context context, final String url) { - final Intent intent = getViewUrlIntent(url); - startExternalActivity(context, intent); - } - - /** - * Provides a safe way to handle external activities which may not exist. - */ - private void startExternalActivity(final Context context, final Intent intent) { - try { - context.startActivity(intent); - } catch (final ActivityNotFoundException ex) { - LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't find activity:", ex); - UiUtils.showToastAtBottom(R.string.activity_not_found_message); - } - } - - private Intent getPerSubscriptionSettingsIntent(final Context context, final int subId, - @Nullable final String settingTitle) { - return new Intent(context, PerSubscriptionSettingsActivity.class) - .putExtra(UI_INTENT_EXTRA_SUB_ID, subId) - .putExtra(UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE, settingTitle); - } - - @Override - public Intent getLaunchConversationActivityIntent(final Context context) { - final Intent intent = new Intent(context, LaunchConversationActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY); - return intent; - } - - @Override - public void kickMediaScanner(final Context context, final String volume) { - final Intent intent = new Intent(MEDIA_SCANNER_SCAN_ACTION) - .putExtra(MediaStore.MEDIA_SCANNER_VOLUME, volume) - .setClassName(MEDIA_SCANNER_PACKAGE, MEDIA_SCANNER_CLASS); - context.startService(intent); - } - - @Override - public PendingIntent getWidgetPendingIntentForConversationActivity(final Context context, - final String conversationId, final int requestCode) { - final Intent intent = getConversationActivityIntent(context, null, null, - false /* withCustomTransition */); - if (conversationId != null) { - intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); - - // Set the action to something unique to this conversation so if someone calls this - // function again on a different conversation, they'll get a new PendingIntent instead - // of the old one. - intent.setAction(ACTION_WIDGET_CONVERSATION + conversationId); - } - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return getPendingIntentWithParentStack(context, intent, requestCode); - } - - @Override - public PendingIntent getWidgetPendingIntentForConversationListActivity( - final Context context) { - final Intent intent = getConversationListActivityIntent(context); - return getPendingIntentWithParentStack(context, intent, 0); - } - - @Override - public PendingIntent getWidgetPendingIntentForConfigurationActivity(final Context context, - final int appWidgetId) { - final Intent configureIntent = new Intent(context, WidgetPickConversationActivity.class); - configureIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - configureIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); - configureIntent.setData(Uri.parse(configureIntent.toUri(Intent.URI_INTENT_SCHEME))); - configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_HISTORY); - return getPendingIntentWithParentStack(context, configureIntent, 0); - } -} diff --git a/src/com/android/messaging/ui/VCardDetailActivity.java b/src/com/android/messaging/ui/VCardDetailActivity.java deleted file mode 100644 index fecdc34..0000000 --- a/src/com/android/messaging/ui/VCardDetailActivity.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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; - -import android.app.Fragment; -import android.net.Uri; -import android.os.Bundle; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.util.Assert; - -/** - * An activity that hosts VCardDetailFragment that shows the content of a VCard that contains one - * or more contacts. - */ -public class VCardDetailActivity extends BugleActionBarActivity { - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.vcard_detail_activity); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - Assert.isTrue(fragment instanceof VCardDetailFragment); - final Uri vCardUri = getIntent().getParcelableExtra(UIIntents.UI_INTENT_EXTRA_VCARD_URI); - Assert.notNull(vCardUri); - final VCardDetailFragment vCardDetailFragment = (VCardDetailFragment) fragment; - vCardDetailFragment.setVCardUri(vCardUri); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - // Treat the home press as back press so that when we go back to - // ConversationActivity, it doesn't lose its original intent (conversation id etc.) - onBackPressed(); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/com/android/messaging/ui/VCardDetailAdapter.java b/src/com/android/messaging/ui/VCardDetailAdapter.java deleted file mode 100644 index cfdd836..0000000 --- a/src/com/android/messaging/ui/VCardDetailAdapter.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; - -import com.android.messaging.R; -import com.android.messaging.datamodel.media.VCardResourceEntry; -import com.android.messaging.datamodel.media.VCardResourceEntry.VCardResourceEntryDestinationItem; - -import java.util.List; - -/** - * Displays a list of expandable contact cards shown in the VCardDetailActivity. - */ -public class VCardDetailAdapter extends BaseExpandableListAdapter { - private final List<VCardResourceEntry> mVCards; - private final LayoutInflater mInflater; - - public VCardDetailAdapter(final Context context, final List<VCardResourceEntry> vCards) { - mVCards = vCards; - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - @Override - public Object getChild(final int groupPosition, final int childPosition) { - return mVCards.get(groupPosition).getContactInfo().get(childPosition); - } - - @Override - public long getChildId(final int groupPosition, final int childPosition) { - return childPosition; - } - - @Override - public View getChildView(final int groupPosition, final int childPosition, - final boolean isLastChild, final View convertView, final ViewGroup parent) { - PersonItemView v; - if (convertView == null) { - v = instantiateView(parent); - } else { - v = (PersonItemView) convertView; - } - - final VCardResourceEntryDestinationItem item = (VCardResourceEntryDestinationItem) - getChild(groupPosition, childPosition); - - v.bind(item.getDisplayItem()); - return v; - } - - @Override - public int getChildrenCount(final int groupPosition) { - return mVCards.get(groupPosition).getContactInfo().size(); - } - - @Override - public Object getGroup(final int groupPosition) { - return mVCards.get(groupPosition); - } - - @Override - public int getGroupCount() { - return mVCards.size(); - } - - @Override - public long getGroupId(final int groupPosition) { - return groupPosition; - } - - @Override - public View getGroupView(final int groupPosition, final boolean isExpanded, - final View convertView, final ViewGroup parent) { - PersonItemView v; - if (convertView == null) { - v = instantiateView(parent); - } else { - v = (PersonItemView) convertView; - } - - final VCardResourceEntry item = (VCardResourceEntry) getGroup(groupPosition); - v.bind(item.getDisplayItem()); - return v; - } - - @Override - public boolean isChildSelectable(final int groupPosition, final int childPosition) { - return true; - } - - @Override - public boolean hasStableIds() { - return true; - } - - private PersonItemView instantiateView(final ViewGroup parent) { - final PersonItemView v = (PersonItemView) mInflater.inflate(R.layout.people_list_item_view, - parent, false); - v.setClickable(false); - return v; - } -} diff --git a/src/com/android/messaging/ui/VCardDetailFragment.java b/src/com/android/messaging/ui/VCardDetailFragment.java deleted file mode 100644 index 1b2b88d..0000000 --- a/src/com/android/messaging/ui/VCardDetailFragment.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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; - -import android.app.Fragment; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.view.ViewGroup; -import android.widget.ExpandableListAdapter; -import android.widget.ExpandableListView; -import android.widget.ExpandableListView.OnChildClickListener; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.datamodel.binding.Binding; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.data.PersonItemData; -import com.android.messaging.datamodel.data.VCardContactItemData; -import com.android.messaging.datamodel.data.PersonItemData.PersonItemDataListener; -import com.android.messaging.util.Assert; -import com.android.messaging.util.SafeAsyncTask; -import com.android.messaging.util.UiUtils; -import com.android.messaging.util.UriUtil; - -/** - * A fragment that shows the content of a VCard that contains one or more contacts. - */ -public class VCardDetailFragment extends Fragment implements PersonItemDataListener { - private final Binding<VCardContactItemData> mBinding = - BindingBase.createBinding(this); - private ExpandableListView mListView; - private VCardDetailAdapter mAdapter; - private Uri mVCardUri; - - /** - * We need to persist the VCard in the scratch directory before letting the user view it. - * We save this Uri locally, so that if the user cancels the action and re-perform the add - * to contacts action we don't have to persist it again. - */ - private Uri mScratchSpaceUri; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - Assert.notNull(mVCardUri); - final View view = inflater.inflate(R.layout.vcard_detail_fragment, container, false); - mListView = (ExpandableListView) view.findViewById(R.id.list); - mListView.addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(final View v, final int left, final int top, final int right, - final int bottom, final int oldLeft, final int oldTop, final int oldRight, - final int oldBottom) { - mListView.setIndicatorBounds(mListView.getWidth() - getResources() - .getDimensionPixelSize(R.dimen.vcard_detail_group_indicator_width), - mListView.getWidth()); - } - }); - mListView.setOnChildClickListener(new OnChildClickListener() { - @Override - public boolean onChildClick(ExpandableListView expandableListView, View clickedView, - int groupPosition, int childPosition, long childId) { - if (!(clickedView instanceof PersonItemView)) { - return false; - } - final Intent intent = ((PersonItemView) clickedView).getClickIntent(); - if (intent != null) { - try { - startActivity(intent); - } catch (ActivityNotFoundException e) { - return false; - } - return true; - } - return false; - } - }); - mBinding.bind(DataModel.get().createVCardContactItemData(getActivity(), mVCardUri)); - mBinding.getData().setListener(this); - return view; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mBinding.isBound()) { - mBinding.unbind(); - } - mListView.setAdapter((ExpandableListAdapter) null); - } - - private boolean shouldShowAddToContactsItem() { - return mBinding.isBound() && mBinding.getData().hasValidVCard(); - } - - @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.vcard_detail_fragment_menu, menu); - final MenuItem addToContactsItem = menu.findItem(R.id.action_add_contact); - addToContactsItem.setVisible(shouldShowAddToContactsItem()); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_add_contact: - mBinding.ensureBound(); - final Uri vCardUri = mBinding.getData().getVCardUri(); - - // We have to do things in the background in case we need to copy the vcard data. - new SafeAsyncTask<Void, Void, Uri>() { - @Override - protected Uri doInBackgroundTimed(final Void... params) { - // We can't delete the persisted vCard file because we don't know when to - // delete it, since the app that uses it (contacts, dialer) may start or - // shut down at any point. Therefore, we rely on the system to clean up - // the cache directory for us. - return mScratchSpaceUri != null ? mScratchSpaceUri : - UriUtil.persistContentToScratchSpace(vCardUri); - } - - @Override - protected void onPostExecute(final Uri result) { - if (result != null) { - mScratchSpaceUri = result; - if (getActivity() != null) { - MediaScratchFileProvider.addUriToDisplayNameEntry( - result, mBinding.getData().getDisplayName()); - UIIntents.get().launchSaveVCardToContactsActivity(getActivity(), - result); - } - } - } - }.executeOnThreadPool(); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } - - public void setVCardUri(final Uri vCardUri) { - Assert.isTrue(!mBinding.isBound()); - mVCardUri = vCardUri; - } - - @Override - public void onPersonDataUpdated(final PersonItemData data) { - Assert.isTrue(data instanceof VCardContactItemData); - mBinding.ensureBound(); - final VCardContactItemData vCardData = (VCardContactItemData) data; - Assert.isTrue(vCardData.hasValidVCard()); - mAdapter = new VCardDetailAdapter(getActivity(), vCardData.getVCardResource().getVCards()); - mListView.setAdapter(mAdapter); - - // Expand the contact card if there's only one contact. - if (mAdapter.getGroupCount() == 1) { - mListView.expandGroup(0); - } - getActivity().invalidateOptionsMenu(); - } - - @Override - public void onPersonDataFailed(final PersonItemData data, final Exception exception) { - mBinding.ensureBound(); - UiUtils.showToastAtBottom(R.string.failed_loading_vcard); - getActivity().finish(); - } -} diff --git a/src/com/android/messaging/ui/VideoThumbnailView.java b/src/com/android/messaging/ui/VideoThumbnailView.java deleted file mode 100644 index 966336e..0000000 --- a/src/com/android/messaging/ui/VideoThumbnailView.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.TypedArray; -import android.media.MediaPlayer; -import android.net.Uri; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView.ScaleType; -import android.widget.VideoView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.media.ImageRequest; -import com.android.messaging.datamodel.media.MessagePartVideoThumbnailRequestDescriptor; -import com.android.messaging.datamodel.media.VideoThumbnailRequest; -import com.android.messaging.util.Assert; - -/** - * View that encapsulates a video preview (either as a thumbnail image, or video player), and the - * a play button to overlay it. Ensures that the video preview maintains the aspect ratio of the - * original video while trying to respect minimum width/height and constraining to the available - * bounds - */ -public class VideoThumbnailView extends FrameLayout { - /** - * When in this mode the VideoThumbnailView is a lightweight AsyncImageView with an ImageButton - * to play the video. Clicking play will launch a full screen player - */ - private static final int MODE_IMAGE_THUMBNAIL = 0; - - /** - * When in this mode the VideoThumbnailVideo will include a VideoView, and the play button will - * play the video inline. When in this mode, the loop and playOnLoad attributes can be applied - * to auto-play or loop the video. - */ - private static final int MODE_PLAYABLE_VIDEO = 1; - - private final int mMode; - private final boolean mPlayOnLoad; - private final boolean mAllowCrop; - private final VideoView mVideoView; - private final ImageButton mPlayButton; - private final AsyncImageView mThumbnailImage; - private int mVideoWidth; - private int mVideoHeight; - private Uri mVideoSource; - private boolean mAnimating; - private boolean mVideoLoaded; - - public VideoThumbnailView(final Context context, final AttributeSet attrs) { - super(context, attrs); - final TypedArray typedAttributes = - context.obtainStyledAttributes(attrs, R.styleable.VideoThumbnailView); - - final LayoutInflater inflater = LayoutInflater.from(context); - inflater.inflate(R.layout.video_thumbnail_view, this, true); - - mPlayOnLoad = typedAttributes.getBoolean(R.styleable.VideoThumbnailView_playOnLoad, false); - final boolean loop = - typedAttributes.getBoolean(R.styleable.VideoThumbnailView_loop, false); - mMode = typedAttributes.getInt(R.styleable.VideoThumbnailView_mode, MODE_IMAGE_THUMBNAIL); - mAllowCrop = typedAttributes.getBoolean(R.styleable.VideoThumbnailView_allowCrop, false); - - mVideoWidth = ImageRequest.UNSPECIFIED_SIZE; - mVideoHeight = ImageRequest.UNSPECIFIED_SIZE; - - if (mMode == MODE_PLAYABLE_VIDEO) { - mVideoView = new VideoView(context); - // Video view tries to request focus on start which pulls focus from the user's intended - // focus when we add this control. Remove focusability to prevent this. The play - // button can still be focused - mVideoView.setFocusable(false); - mVideoView.setFocusableInTouchMode(false); - mVideoView.clearFocus(); - addView(mVideoView, 0, new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { - @Override - public void onPrepared(final MediaPlayer mediaPlayer) { - mVideoLoaded = true; - mVideoWidth = mediaPlayer.getVideoWidth(); - mVideoHeight = mediaPlayer.getVideoHeight(); - mediaPlayer.setLooping(loop); - trySwitchToVideo(); - } - }); - mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(final MediaPlayer mediaPlayer) { - mPlayButton.setVisibility(View.VISIBLE); - } - }); - mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { - @Override - public boolean onError(final MediaPlayer mediaPlayer, final int i, final int i2) { - return true; - } - }); - } else { - mVideoView = null; - } - - mPlayButton = (ImageButton) findViewById(R.id.video_thumbnail_play_button); - if (loop) { - mPlayButton.setVisibility(View.GONE); - } else { - mPlayButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - if (mVideoSource == null) { - return; - } - - if (mMode == MODE_PLAYABLE_VIDEO) { - mVideoView.seekTo(0); - start(); - } else { - UIIntents.get().launchFullScreenVideoViewer(getContext(), mVideoSource); - } - } - }); - mPlayButton.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(final View view) { - // Button prevents long click from propagating up, do it manually - VideoThumbnailView.this.performLongClick(); - return true; - } - }); - } - - mThumbnailImage = (AsyncImageView) findViewById(R.id.video_thumbnail_image); - if (mAllowCrop) { - mThumbnailImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; - mThumbnailImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - mThumbnailImage.setScaleType(ScaleType.CENTER_CROP); - } else { - // This is the default setting in the layout, so No-op. - } - final int maxHeight = typedAttributes.getDimensionPixelSize( - R.styleable.VideoThumbnailView_android_maxHeight, ImageRequest.UNSPECIFIED_SIZE); - if (maxHeight != ImageRequest.UNSPECIFIED_SIZE) { - mThumbnailImage.setMaxHeight(maxHeight); - mThumbnailImage.setAdjustViewBounds(true); - } - - typedAttributes.recycle(); - } - - @Override - protected void onAnimationStart() { - super.onAnimationStart(); - mAnimating = true; - } - - @Override - protected void onAnimationEnd() { - super.onAnimationEnd(); - mAnimating = false; - trySwitchToVideo(); - } - - private void trySwitchToVideo() { - if (mAnimating) { - // Don't start video or hide image until after animation completes - return; - } - - if (!mVideoLoaded) { - // Video hasn't loaded, nothing more to do - return; - } - - if (mPlayOnLoad) { - start(); - } else { - mVideoView.seekTo(0); - } - } - - private boolean hasVideoSize() { - return mVideoWidth != ImageRequest.UNSPECIFIED_SIZE && - mVideoHeight != ImageRequest.UNSPECIFIED_SIZE; - } - - public void start() { - Assert.equals(MODE_PLAYABLE_VIDEO, mMode); - mPlayButton.setVisibility(View.GONE); - mThumbnailImage.setVisibility(View.GONE); - mVideoView.start(); - } - - // TODO: The check could be added to MessagePartData itself so that all users of MessagePartData - // get the right behavior, instead of requiring all the users to do similar checks. - private static boolean shouldUseGenericVideoIcon(final boolean incomingMessage) { - return incomingMessage && !VideoThumbnailRequest.shouldShowIncomingVideoThumbnails(); - } - - public void setSource(final MessagePartData part, final boolean incomingMessage) { - if (part == null) { - clearSource(); - } else { - mVideoSource = part.getContentUri(); - if (shouldUseGenericVideoIcon(incomingMessage)) { - mThumbnailImage.setImageResource(R.drawable.generic_video_icon); - mVideoWidth = ImageRequest.UNSPECIFIED_SIZE; - mVideoHeight = ImageRequest.UNSPECIFIED_SIZE; - } else { - mThumbnailImage.setImageResourceId( - new MessagePartVideoThumbnailRequestDescriptor(part)); - if (mVideoView != null) { - mVideoView.setVideoURI(mVideoSource); - } - mVideoWidth = part.getWidth(); - mVideoHeight = part.getHeight(); - } - } - } - - public void setSource(final Uri videoSource, final boolean incomingMessage) { - if (videoSource == null) { - clearSource(); - } else { - mVideoSource = videoSource; - if (shouldUseGenericVideoIcon(incomingMessage)) { - mThumbnailImage.setImageResource(R.drawable.generic_video_icon); - mVideoWidth = ImageRequest.UNSPECIFIED_SIZE; - mVideoHeight = ImageRequest.UNSPECIFIED_SIZE; - } else { - mThumbnailImage.setImageResourceId( - new MessagePartVideoThumbnailRequestDescriptor(videoSource)); - if (mVideoView != null) { - mVideoView.setVideoURI(videoSource); - } - } - } - } - - private void clearSource() { - mVideoSource = null; - mThumbnailImage.setImageResourceId(null); - mVideoWidth = ImageRequest.UNSPECIFIED_SIZE; - mVideoHeight = ImageRequest.UNSPECIFIED_SIZE; - if (mVideoView != null) { - mVideoView.setVideoURI(null); - } - } - - @Override - public void setMinimumWidth(final int minWidth) { - super.setMinimumWidth(minWidth); - if (mVideoView != null) { - mVideoView.setMinimumWidth(minWidth); - } - } - - @Override - public void setMinimumHeight(final int minHeight) { - super.setMinimumHeight(minHeight); - if (mVideoView != null) { - mVideoView.setMinimumHeight(minHeight); - } - } - - public void setColorFilter(int color) { - mThumbnailImage.setColorFilter(color); - mPlayButton.setColorFilter(color); - } - - public void clearColorFilter() { - mThumbnailImage.clearColorFilter(); - mPlayButton.clearColorFilter(); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - if (mAllowCrop) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - int desiredWidth = 1; - int desiredHeight = 1; - if (mVideoView != null) { - mVideoView.measure(widthMeasureSpec, heightMeasureSpec); - } - mThumbnailImage.measure(widthMeasureSpec, heightMeasureSpec); - if (hasVideoSize()) { - desiredWidth = mVideoWidth; - desiredHeight = mVideoHeight; - } else { - desiredWidth = mThumbnailImage.getMeasuredWidth(); - desiredHeight = mThumbnailImage.getMeasuredHeight(); - } - - final int minimumWidth = getMinimumWidth(); - final int minimumHeight = getMinimumHeight(); - - // Constrain the scale to fit within the supplied size - final float maxScale = Math.max( - MeasureSpec.getSize(widthMeasureSpec) / (float) desiredWidth, - MeasureSpec.getSize(heightMeasureSpec) / (float) desiredHeight); - - // Scale up to reach minimum width/height - final float widthScale = Math.max(1, minimumWidth / (float) desiredWidth); - final float heightScale = Math.max(1, minimumHeight / (float) desiredHeight); - final float scale = Math.min(maxScale, Math.max(widthScale, heightScale)); - desiredWidth = (int) (desiredWidth * scale); - desiredHeight = (int) (desiredHeight * scale); - - setMeasuredDimension(desiredWidth, desiredHeight); - } - - @Override - protected void onLayout(final boolean changed, final int left, final int top, final int right, - final int bottom) { - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - child.layout(0, 0, right - left, bottom - top); - } - } -} diff --git a/src/com/android/messaging/ui/ViewPagerTabStrip.java b/src/com/android/messaging/ui/ViewPagerTabStrip.java deleted file mode 100644 index e088296..0000000 --- a/src/com/android/messaging/ui/ViewPagerTabStrip.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; - -import com.android.messaging.R; -import com.android.messaging.util.OsUtil; - -public class ViewPagerTabStrip extends LinearLayout { - private int mSelectedUnderlineThickness; - private final Paint mSelectedUnderlinePaint; - - private int mIndexForSelection; - private float mSelectionOffset; - - public ViewPagerTabStrip(Context context) { - this(context, null); - } - - public ViewPagerTabStrip(Context context, AttributeSet attrs) { - super(context, attrs); - - final Resources res = context.getResources(); - - mSelectedUnderlineThickness = - res.getDimensionPixelSize(R.dimen.pager_tab_underline_selected); - int underlineColor = res.getColor(R.color.contact_picker_tab_underline); - int backgroundColor = res.getColor(R.color.action_bar_background_color); - - mSelectedUnderlinePaint = new Paint(); - mSelectedUnderlinePaint.setColor(underlineColor); - - setBackgroundColor(backgroundColor); - setWillNotDraw(false); - } - - /** - * Notifies this view that view pager has been scrolled. We save the tab index - * and selection offset for interpolating the position and width of selection - * underline. - */ - void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - mIndexForSelection = position; - mSelectionOffset = positionOffset; - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) { - int childCount = getChildCount(); - - // Thick colored underline below the current selection - if (childCount > 0) { - View selectedTitle = getChildAt(mIndexForSelection); - int selectedLeft = selectedTitle.getLeft(); - int selectedRight = selectedTitle.getRight(); - final boolean isRtl = isRtl(); - final boolean hasNextTab = isRtl ? mIndexForSelection > 0 - : (mIndexForSelection < (getChildCount() - 1)); - if ((mSelectionOffset > 0.0f) && hasNextTab) { - // Draw the selection partway between the tabs - View nextTitle = getChildAt(mIndexForSelection + (isRtl ? -1 : 1)); - int nextLeft = nextTitle.getLeft(); - int nextRight = nextTitle.getRight(); - - selectedLeft = (int) (mSelectionOffset * nextLeft + - (1.0f - mSelectionOffset) * selectedLeft); - selectedRight = (int) (mSelectionOffset * nextRight + - (1.0f - mSelectionOffset) * selectedRight); - } - - int height = getHeight(); - canvas.drawRect(selectedLeft, height - mSelectedUnderlineThickness, - selectedRight, height, mSelectedUnderlinePaint); - } - } - - private boolean isRtl() { - return OsUtil.isAtLeastJB_MR2() ? getLayoutDirection() == View.LAYOUT_DIRECTION_RTL : false; - } -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/ViewPagerTabs.java b/src/com/android/messaging/ui/ViewPagerTabs.java deleted file mode 100644 index f31a071..0000000 --- a/src/com/android/messaging/ui/ViewPagerTabs.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.Outline; -import android.graphics.drawable.ColorDrawable; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.View; -import android.view.ViewOutlineProvider; -import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.util.OsUtil; - -/** - * Lightweight implementation of ViewPager tabs. This looks similar to traditional actionBar tabs, - * but allows for the view containing the tabs to be placed anywhere on screen. Text-related - * attributes can also be assigned in XML - these will get propogated to the child TextViews - * automatically. - * - * Note: this file is taken from the AOSP /packages/apps/ContactsCommon/src/com/android/contacts/ - * common/list/ViewPagerTabs.java. Some platform specific API calls (e.g. ViewOutlineProvider which - * assumes L and above) have been modified to support down to Api Level 16. - */ -public class ViewPagerTabs extends HorizontalScrollView implements ViewPager.OnPageChangeListener { - - ViewPager mPager; - private ViewPagerTabStrip mTabStrip; - - /** - * Linearlayout that will contain the TextViews serving as tabs. This is the only child - * of the parent HorizontalScrollView. - */ - final int mTextStyle; - final ColorStateList mTextColor; - final int mTextSize; - final boolean mTextAllCaps; - int mPrevSelected = -1; - int mSidePadding; - - private static final int TAB_SIDE_PADDING_IN_DPS = 10; - - // TODO: This should use <declare-styleable> in the future - private static final int[] ATTRS = new int[] { - android.R.attr.textSize, - android.R.attr.textStyle, - android.R.attr.textColor, - android.R.attr.textAllCaps - }; - - /** - * Shows a toast with the tab description when long-clicked. - */ - private class OnTabLongClickListener implements OnLongClickListener { - final String mTabDescription; - - public OnTabLongClickListener(String tabDescription) { - mTabDescription = tabDescription; - } - - @Override - public boolean onLongClick(View v) { - final int[] screenPos = new int[2]; - getLocationOnScreen(screenPos); - - final Context context = getContext(); - final int width = getWidth(); - final int height = getHeight(); - final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; - - Toast toast = Toast.makeText(context, mTabDescription, Toast.LENGTH_SHORT); - - // Show the toast under the tab - toast.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, - (screenPos[0] + width / 2) - screenWidth / 2, screenPos[1] + height); - - toast.show(); - return true; - } - } - - public ViewPagerTabs(Context context) { - this(context, null); - } - - public ViewPagerTabs(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ViewPagerTabs(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - setFillViewport(true); - - mSidePadding = (int) (getResources().getDisplayMetrics().density * TAB_SIDE_PADDING_IN_DPS); - - final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); - mTextSize = a.getDimensionPixelSize(0, 0); - mTextStyle = a.getInt(1, 0); - mTextColor = a.getColorStateList(2); - mTextAllCaps = a.getBoolean(3, false); - - mTabStrip = new ViewPagerTabStrip(context); - addView(mTabStrip, - new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); - a.recycle(); - - // enable shadow casting from view bounds - if (OsUtil.isAtLeastL()) { - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRect(0, 0, view.getWidth(), view.getHeight()); - } - }); - } - } - - public void setViewPager(ViewPager viewPager) { - mPager = viewPager; - addTabs(mPager.getAdapter()); - } - - private void addTabs(PagerAdapter adapter) { - mTabStrip.removeAllViews(); - - final int count = adapter.getCount(); - for (int i = 0; i < count; i++) { - addTab(adapter.getPageTitle(i), i); - } - } - - private void addTab(CharSequence tabTitle, final int position) { - final TextView textView = new TextView(getContext()); - textView.setText(tabTitle); - textView.setBackgroundResource(R.drawable.contact_picker_tab_background_selector); - textView.setGravity(Gravity.CENTER); - textView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mPager.setCurrentItem(getRtlPosition(position)); - } - }); - - // Assign various text appearance related attributes to child views. - if (mTextStyle > 0) { - textView.setTypeface(textView.getTypeface(), mTextStyle); - } - if (mTextSize > 0) { - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); - } - if (mTextColor != null) { - textView.setTextColor(mTextColor); - } - textView.setAllCaps(mTextAllCaps); - textView.setPadding(mSidePadding, 0, mSidePadding, 0); - mTabStrip.addView(textView, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.MATCH_PARENT, 1)); - // Default to the first child being selected - if (position == 0) { - mPrevSelected = 0; - textView.setSelected(true); - } - } - - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - position = getRtlPosition(position); - int tabStripChildCount = mTabStrip.getChildCount(); - if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { - return; - } - - mTabStrip.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - - @Override - public void onPageSelected(int position) { - position = getRtlPosition(position); - int tabStripChildCount = mTabStrip.getChildCount(); - if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { - return; - } - - if (mPrevSelected >= 0 && mPrevSelected < tabStripChildCount) { - mTabStrip.getChildAt(mPrevSelected).setSelected(false); - } - final View selectedChild = mTabStrip.getChildAt(position); - selectedChild.setSelected(true); - - // Update scroll position - final int scrollPos = selectedChild.getLeft() - (getWidth() - selectedChild.getWidth()) / 2; - smoothScrollTo(scrollPos, 0); - mPrevSelected = position; - } - - @Override - public void onPageScrollStateChanged(int state) { - } - - private int getRtlPosition(int position) { - if (OsUtil.isAtLeastJB_MR2() && Factory.get().getApplicationContext().getResources() - .getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - return mTabStrip.getChildCount() - 1 - position; - } - return position; - } - - public int getSelectedItemPosition() { - return mPrevSelected; - } -} diff --git a/src/com/android/messaging/ui/WidgetPickConversationActivity.java b/src/com/android/messaging/ui/WidgetPickConversationActivity.java deleted file mode 100644 index 60e1318..0000000 --- a/src/com/android/messaging/ui/WidgetPickConversationActivity.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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; - -import android.app.Fragment; -import android.appwidget.AppWidgetManager; -import android.content.Intent; -import android.os.Bundle; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.ui.conversationlist.ShareIntentFragment; -import com.android.messaging.util.Assert; -import com.android.messaging.util.BuglePrefs; -import com.android.messaging.widget.WidgetConversationProvider; - -public class WidgetPickConversationActivity extends BaseBugleActivity implements - ShareIntentFragment.HostInterface { - - private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - - // Set the result to CANCELED. This will cause the widget host to cancel - // out of the widget placement if they press the back button. - setResult(RESULT_CANCELED); - - // Find the widget id from the intent. - final Intent intent = getIntent(); - final Bundle extras = intent.getExtras(); - if (extras != null) { - mAppWidgetId = extras.getInt( - AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - } - - // If they gave us an intent without the widget id, just bail. - if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { - finish(); - } - - final ShareIntentFragment convPicker = new ShareIntentFragment(); - final Bundle bundle = new Bundle(); - bundle.putBoolean(ShareIntentFragment.HIDE_NEW_CONVERSATION_BUTTON_KEY, true); - convPicker.setArguments(bundle); - convPicker.show(getFragmentManager(), "ShareIntentFragment"); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - final Intent intent = getIntent(); - final String action = intent.getAction(); - if (!AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action)) { - // Unsupported action. - Assert.fail("Unsupported action type: " + action); - } - } - - @Override - public void onConversationClick(final ConversationListItemData conversationListItemData) { - saveConversationidPref(mAppWidgetId, conversationListItemData.getConversationId()); - - // Push widget update to surface with newly set prefix - WidgetConversationProvider.rebuildWidget(this, mAppWidgetId); - - // Make sure we pass back the original appWidgetId - Intent resultValue = new Intent(); - resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); - setResult(RESULT_OK, resultValue); - finish(); - } - - @Override - public void onCreateConversationClick() { - // We should never get here because we're hiding the new conversation button in the - // ShareIntentFragment by setting HIDE_NEW_CONVERSATION_BUTTON_KEY in the arguments. - finish(); - } - - // Write the ConversationId to the SharedPreferences object for this widget - static void saveConversationidPref(int appWidgetId, String conversationId) { - final BuglePrefs prefs = Factory.get().getWidgetPrefs(); - prefs.putString(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID + appWidgetId, conversationId); - } - - // Read the ConversationId from the SharedPreferences object for this widget. - public static String getConversationIdPref(int appWidgetId) { - final BuglePrefs prefs = Factory.get().getWidgetPrefs(); - return prefs.getString(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID + appWidgetId, null); - } - - // Delete the ConversationId preference from the SharedPreferences object for this widget. - public static void deleteConversationIdPref(int appWidgetId) { - final BuglePrefs prefs = Factory.get().getWidgetPrefs(); - prefs.remove(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID + appWidgetId); - } - -} diff --git a/src/com/android/messaging/ui/animation/PopupTransitionAnimation.java b/src/com/android/messaging/ui/animation/PopupTransitionAnimation.java deleted file mode 100644 index 21529c6..0000000 --- a/src/com/android/messaging/ui/animation/PopupTransitionAnimation.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * 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.animation; - -import android.animation.TypeEvaluator; -import android.app.Activity; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Rect; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.Transformation; -import android.widget.PopupWindow; - -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.ThreadUtil; -import com.android.messaging.util.UiUtils; - -/** - * Animates viewToAnimate from startRect to the place where it is in the layout, viewToAnimate - * should be in its final destination location before startAfterLayoutComplete is called. - * viewToAnimate will be drawn scaled and offset in a popupWindow. - * This class handles the case where the viewToAnimate moves during the animation - */ -public class PopupTransitionAnimation extends Animation { - /** The view we're animating */ - private final View mViewToAnimate; - - /** The rect to start the slide in animation from */ - private final Rect mStartRect; - - /** The rect of the currently animated view */ - private Rect mCurrentRect; - - /** The rect that we're animating to. This can change during the animation */ - private final Rect mDestRect; - - /** The bounds of the popup in window coordinates. Does not include notification bar */ - private final Rect mPopupRect; - - /** The bounds of the action bar in window coordinates. We clip the popup to below this */ - private final Rect mActionBarRect; - - /** Interpolates between the start and end rect for every animation tick */ - private final TypeEvaluator<Rect> mRectEvaluator; - - /** The popup window that holds contains the animating view */ - private PopupWindow mPopupWindow; - - /** The layout root for the popup which is where the animated view is rendered */ - private View mPopupRoot; - - /** The action bar's view */ - private final View mActionBarView; - - private Runnable mOnStartCallback; - private Runnable mOnStopCallback; - - public PopupTransitionAnimation(final Rect startRect, final View viewToAnimate) { - mViewToAnimate = viewToAnimate; - mStartRect = startRect; - mCurrentRect = new Rect(mStartRect); - mDestRect = new Rect(); - mPopupRect = new Rect(); - mActionBarRect = new Rect(); - final Activity activity = (Activity) viewToAnimate.getRootView().getContext(); - mActionBarView = activity.getWindow().getDecorView().findViewById( - android.support.v7.appcompat.R.id.action_bar); - mRectEvaluator = RectEvaluatorCompat.create(); - setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION); - setInterpolator(UiUtils.DEFAULT_INTERPOLATOR); - setAnimationListener(new AnimationListener() { - @Override - public void onAnimationStart(final Animation animation) { - if (mOnStartCallback != null) { - mOnStartCallback.run(); - } - mEvents.append("oAS,"); - } - - @Override - public void onAnimationEnd(final Animation animation) { - if (mOnStopCallback != null) { - mOnStopCallback.run(); - } - dismiss(); - mEvents.append("oAE,"); - } - - @Override - public void onAnimationRepeat(final Animation animation) { - } - }); - } - - private final StringBuilder mEvents = new StringBuilder(); - private final Runnable mCleanupRunnable = new Runnable() { - @Override - public void run() { - LogUtil.w(LogUtil.BUGLE_TAG, "PopupTransitionAnimation: " + mEvents); - } - }; - - /** - * Ensures the animation is ready before starting the animation. - * viewToAnimate must first be layed out so we know where we will animate to - */ - public void startAfterLayoutComplete() { - // We want layout to occur, and then we immediately animate it in, so hide it initially to - // reduce jank on the first frame - mViewToAnimate.setVisibility(View.INVISIBLE); - mViewToAnimate.setAlpha(0); - - final Runnable startAnimation = new Runnable() { - boolean mRunComplete = false; - boolean mFirstTry = true; - - @Override - public void run() { - if (mRunComplete) { - return; - } - - mViewToAnimate.getGlobalVisibleRect(mDestRect); - // In Android views which are visible but haven't computed their size yet have a - // size of 1x1 because anything with a size of 0x0 is considered hidden. We can't - // start the animation until after the size is greater than 1x1 - if (mDestRect.width() <= 1 || mDestRect.height() <= 1) { - // Layout hasn't occurred yet - if (!mFirstTry) { - // Give up if this is not the first try, since layout change still doesn't - // yield a size for the view. This is likely because the media picker is - // full screen so there's no space left for the animated view. We give up - // on animation, but need to make sure the view that was initially - // hidden is re-shown. - mViewToAnimate.setAlpha(1); - mViewToAnimate.setVisibility(View.VISIBLE); - } else { - mFirstTry = false; - UiUtils.doOnceAfterLayoutChange(mViewToAnimate, this); - } - return; - } - - mRunComplete = true; - mViewToAnimate.startAnimation(PopupTransitionAnimation.this); - mViewToAnimate.invalidate(); - // http://b/20856505: The PopupWindow sometimes does not get dismissed. - ThreadUtil.getMainThreadHandler().postDelayed(mCleanupRunnable, getDuration() * 2); - } - }; - - startAnimation.run(); - } - - public PopupTransitionAnimation setOnStartCallback(final Runnable onStart) { - mOnStartCallback = onStart; - return this; - } - - public PopupTransitionAnimation setOnStopCallback(final Runnable onStop) { - mOnStopCallback = onStop; - return this; - } - - @Override - protected void applyTransformation(final float interpolatedTime, final Transformation t) { - if (mPopupWindow == null) { - initPopupWindow(); - } - // Update mDestRect as it may have moved during the animation - mPopupRect.set(UiUtils.getMeasuredBoundsOnScreen(mPopupRoot)); - mActionBarRect.set(UiUtils.getMeasuredBoundsOnScreen(mActionBarView)); - computeDestRect(); - - // Update currentRect to the new animated coordinates, and request mPopupRoot to redraw - // itself at the new coordinates - mCurrentRect = mRectEvaluator.evaluate(interpolatedTime, mStartRect, mDestRect); - mPopupRoot.invalidate(); - - if (interpolatedTime >= 0.98) { - mEvents.append("aT").append(interpolatedTime).append(','); - } - if (interpolatedTime == 1) { - dismiss(); - } - } - - private void dismiss() { - mEvents.append("d,"); - mViewToAnimate.setAlpha(1); - mViewToAnimate.setVisibility(View.VISIBLE); - // Delay dismissing the popup window to let mViewToAnimate draw under it and reduce the - // flash - ThreadUtil.getMainThreadHandler().post(new Runnable() { - @Override - public void run() { - try { - mPopupWindow.dismiss(); - } catch (IllegalArgumentException e) { - // PopupWindow.dismiss() will fire an IllegalArgumentException if the activity - // has already ended while we were animating - } - ThreadUtil.getMainThreadHandler().removeCallbacks(mCleanupRunnable); - } - }); - } - - @Override - public boolean willChangeBounds() { - return false; - } - - /** - * Computes mDestRect (the position in window space of the placeholder view that we should - * animate to). Some frames during the animation fail to compute getGlobalVisibleRect, so use - * the last known values in that case - */ - private void computeDestRect() { - final int prevTop = mDestRect.top; - final int prevLeft = mDestRect.left; - final int prevRight = mDestRect.right; - final int prevBottom = mDestRect.bottom; - - if (!getViewScreenMeasureRect(mViewToAnimate, mDestRect)) { - mDestRect.top = prevTop; - mDestRect.left = prevLeft; - mDestRect.bottom = prevBottom; - mDestRect.right = prevRight; - } - } - - /** - * Sets up the PopupWindow that the view will animate in. Animating the size and position of a - * popup can be choppy, so instead we make the popup fill the entire space of the screen, and - * animate the position of viewToAnimate within the popup using a Transformation - */ - private void initPopupWindow() { - mPopupRoot = new View(mViewToAnimate.getContext()) { - @Override - protected void onDraw(final Canvas canvas) { - canvas.save(); - canvas.clipRect(getLeft(), mActionBarRect.bottom - mPopupRect.top, getRight(), - getBottom()); - canvas.drawColor(Color.TRANSPARENT); - final float previousAlpha = mViewToAnimate.getAlpha(); - mViewToAnimate.setAlpha(1); - // The view's global position includes the notification bar height, but - // the popup window may or may not cover the notification bar (depending on screen - // rotation, IME status etc.), so we need to compensate for this difference by - // offseting vertically. - canvas.translate(mCurrentRect.left, mCurrentRect.top - mPopupRect.top); - - final float viewWidth = mViewToAnimate.getWidth(); - final float viewHeight = mViewToAnimate.getHeight(); - if (viewWidth > 0 && viewHeight > 0) { - canvas.scale(mCurrentRect.width() / viewWidth, - mCurrentRect.height() / viewHeight); - } - canvas.clipRect(0, 0, mCurrentRect.width(), mCurrentRect.height()); - if (!mPopupRect.isEmpty()) { - // HACK: Layout is unstable until mPopupRect is non-empty. - mViewToAnimate.draw(canvas); - } - mViewToAnimate.setAlpha(previousAlpha); - canvas.restore(); - } - }; - mPopupWindow = new PopupWindow(mViewToAnimate.getContext()); - mPopupWindow.setBackgroundDrawable(null); - mPopupWindow.setContentView(mPopupRoot); - mPopupWindow.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); - mPopupWindow.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); - mPopupWindow.setTouchable(false); - // We must pass a non-zero value for the y offset, or else the system resets the status bar - // color to black (M only) during the animation. The actual position of the window (and - // the animated view inside it) are still correct, regardless of what we pass for the y - // parameter (e.g. 1 and 100 both work). Not entirely sure why this works. - mPopupWindow.showAtLocation(mViewToAnimate, Gravity.TOP, 0, 1); - } - - private static boolean getViewScreenMeasureRect(final View view, final Rect outRect) { - outRect.set(UiUtils.getMeasuredBoundsOnScreen(view)); - return !outRect.isEmpty(); - } -} diff --git a/src/com/android/messaging/ui/animation/RectEvaluatorCompat.java b/src/com/android/messaging/ui/animation/RectEvaluatorCompat.java deleted file mode 100644 index e3c60fc..0000000 --- a/src/com/android/messaging/ui/animation/RectEvaluatorCompat.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.animation; - -import android.animation.RectEvaluator; -import android.animation.TypeEvaluator; -import android.graphics.Rect; - -import com.android.messaging.util.OsUtil; - -/** - * This evaluator can be used to perform type interpolation between <code>Rect</code> values. - * It's backward compatible to Api Level 11. - */ -public class RectEvaluatorCompat implements TypeEvaluator<Rect> { - public static TypeEvaluator<Rect> create() { - if (OsUtil.isAtLeastJB_MR2()) { - return new RectEvaluator(); - } else { - return new RectEvaluatorCompat(); - } - } - - @Override - public Rect evaluate(float fraction, Rect startValue, Rect endValue) { - int left = startValue.left + (int) ((endValue.left - startValue.left) * fraction); - int top = startValue.top + (int) ((endValue.top - startValue.top) * fraction); - int right = startValue.right + (int) ((endValue.right - startValue.right) * fraction); - int bottom = startValue.bottom + (int) ((endValue.bottom - startValue.bottom) * fraction); - return new Rect(left, top, right, bottom); - } -} diff --git a/src/com/android/messaging/ui/animation/ViewGroupItemVerticalExplodeAnimation.java b/src/com/android/messaging/ui/animation/ViewGroupItemVerticalExplodeAnimation.java deleted file mode 100644 index 6abfdf9..0000000 --- a/src/com/android/messaging/ui/animation/ViewGroupItemVerticalExplodeAnimation.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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.animation; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.support.v4.view.ViewCompat; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroupOverlay; -import android.view.ViewOverlay; -import android.widget.FrameLayout; - -import com.android.messaging.R; -import com.android.messaging.util.ImageUtils; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; - -/** - * <p> - * Shows a vertical "explode" animation for any view inside a view group (e.g. views inside a - * ListView). During the animation, a snapshot is taken for the view to the animated and - * presented in a popup window or view overlay on top of the original view group. The background - * of the view (a highlight) vertically expands (explodes) during the animation. - * </p> - * <p> - * The exact implementation of the animation depends on platform API level. For JB_MR2 and later, - * the implementation utilizes ViewOverlay to perform highly performant overlay animations; for - * older API levels, the implementation falls back to using a full screen popup window to stage - * the animation. - * </p> - * <p> - * To start this animation, call {@link #startAnimationForView(ViewGroup, View, View, boolean, int)} - * </p> - */ -public class ViewGroupItemVerticalExplodeAnimation { - /** - * Starts a vertical explode animation for a given view situated in a given container. - * - * @param container the container of the view which determines the explode animation's final - * size - * @param viewToAnimate the view to be animated. The view will be highlighted by the explode - * highlight, which expands from the size of the view to the size of the container. - * @param animationStagingView the view that stages the animation. Since viewToAnimate may be - * removed from the view tree during the animation, we need a view that'll be alive - * for the duration of the animation so that the animation won't get cancelled. - * @param snapshotView whether a snapshot of the view to animate is needed. - */ - public static void startAnimationForView(final ViewGroup container, final View viewToAnimate, - final View animationStagingView, final boolean snapshotView, final int duration) { - if (OsUtil.isAtLeastJB_MR2() && (viewToAnimate.getContext() instanceof Activity)) { - new ViewExplodeAnimationJellyBeanMR2(viewToAnimate, container, snapshotView, duration) - .startAnimation(); - } else { - // Pre JB_MR2, this animation can cause rendering failures which causes the framework - // to fall back to software rendering where camera preview isn't supported (b/18264647) - // just skip the animation to avoid this case. - } - } - - /** - * Implementation class for API level >= 18. - */ - @TargetApi(18) - private static class ViewExplodeAnimationJellyBeanMR2 { - private final View mViewToAnimate; - private final ViewGroup mContainer; - private final View mSnapshot; - private final Bitmap mViewBitmap; - private final int mDuration; - - public ViewExplodeAnimationJellyBeanMR2(final View viewToAnimate, final ViewGroup container, - final boolean snapshotView, final int duration) { - mViewToAnimate = viewToAnimate; - mContainer = container; - mDuration = duration; - if (snapshotView) { - mViewBitmap = snapshotView(viewToAnimate); - mSnapshot = new View(viewToAnimate.getContext()); - } else { - mSnapshot = null; - mViewBitmap = null; - } - } - - public void startAnimation() { - final Context context = mViewToAnimate.getContext(); - final Resources resources = context.getResources(); - final View decorView = ((Activity) context).getWindow().getDecorView(); - final ViewOverlay viewOverlay = decorView.getOverlay(); - if (viewOverlay instanceof ViewGroupOverlay) { - final ViewGroupOverlay overlay = (ViewGroupOverlay) viewOverlay; - - // Add a shadow layer to the overlay. - final FrameLayout shadowContainerLayer = new FrameLayout(context); - final Drawable oldBackground = mViewToAnimate.getBackground(); - final Rect containerRect = UiUtils.getMeasuredBoundsOnScreen(mContainer); - final Rect decorRect = UiUtils.getMeasuredBoundsOnScreen(decorView); - // Position the container rect relative to the decor rect since the decor rect - // defines whether the view overlay will be positioned. - containerRect.offset(-decorRect.left, -decorRect.top); - shadowContainerLayer.setLeft(containerRect.left); - shadowContainerLayer.setTop(containerRect.top); - shadowContainerLayer.setBottom(containerRect.bottom); - shadowContainerLayer.setRight(containerRect.right); - shadowContainerLayer.setBackgroundColor(resources.getColor( - R.color.open_conversation_animation_background_shadow)); - // Per design request, temporarily clear out the background of the item content - // to not show any ripple effects during animation. - if (!(oldBackground instanceof ColorDrawable)) { - mViewToAnimate.setBackground(null); - } - overlay.add(shadowContainerLayer); - - // Add a expand layer and position it with in the shadow background, so it can - // be properly clipped to the container bounds during the animation. - final View expandLayer = new View(context); - final int elevation = resources.getDimensionPixelSize( - R.dimen.explode_animation_highlight_elevation); - final Rect viewRect = UiUtils.getMeasuredBoundsOnScreen(mViewToAnimate); - // Frame viewRect from screen space to containerRect space. - viewRect.offset(-containerRect.left - decorRect.left, - -containerRect.top - decorRect.top); - // Since the expand layer expands at the same rate above and below, we need to - // compute the expand scale using the bigger of the top/bottom distances. - final int expandLayerHalfHeight = viewRect.height() / 2; - final int topDist = viewRect.top; - final int bottomDist = containerRect.height() - viewRect.bottom; - final float scale = expandLayerHalfHeight == 0 ? 1 : - ((float) Math.max(topDist, bottomDist) + expandLayerHalfHeight) / - expandLayerHalfHeight; - // Position the expand layer initially to exactly match the animated item. - shadowContainerLayer.addView(expandLayer); - expandLayer.setLeft(viewRect.left); - expandLayer.setTop(viewRect.top); - expandLayer.setBottom(viewRect.bottom); - expandLayer.setRight(viewRect.right); - expandLayer.setBackgroundColor(resources.getColor( - R.color.conversation_background)); - ViewCompat.setElevation(expandLayer, elevation); - - // Conditionally stage the snapshot in the overlay. - if (mSnapshot != null) { - shadowContainerLayer.addView(mSnapshot); - mSnapshot.setLeft(viewRect.left); - mSnapshot.setTop(viewRect.top); - mSnapshot.setBottom(viewRect.bottom); - mSnapshot.setRight(viewRect.right); - mSnapshot.setBackground(new BitmapDrawable(resources, mViewBitmap)); - ViewCompat.setElevation(mSnapshot, elevation); - } - - // Apply a scale animation to scale to full screen. - expandLayer.animate().scaleY(scale) - .setDuration(mDuration) - .setInterpolator(UiUtils.EASE_IN_INTERPOLATOR) - .withEndAction(new Runnable() { - @Override - public void run() { - // Clean up the views added to overlay on animation finish. - overlay.remove(shadowContainerLayer); - mViewToAnimate.setBackground(oldBackground); - if (mViewBitmap != null) { - mViewBitmap.recycle(); - } - } - }); - } - } - } - - /** - * Take a snapshot of the given review, return a Bitmap object that's owned by the caller. - */ - static Bitmap snapshotView(final View view) { - // Save the content of the view into a bitmap. - final Bitmap viewBitmap = Bitmap.createBitmap(view.getWidth(), - view.getHeight(), Bitmap.Config.ARGB_8888); - // Strip the view of its background when taking a snapshot so that things like touch - // feedback don't get accidentally snapshotted. - final Drawable viewBackground = view.getBackground(); - ImageUtils.setBackgroundDrawableOnView(view, null); - view.draw(new Canvas(viewBitmap)); - ImageUtils.setBackgroundDrawableOnView(view, viewBackground); - return viewBitmap; - } -} diff --git a/src/com/android/messaging/ui/appsettings/ApnEditorActivity.java b/src/com/android/messaging/ui/appsettings/ApnEditorActivity.java deleted file mode 100644 index b7cb7ae..0000000 --- a/src/com/android/messaging/ui/appsettings/ApnEditorActivity.java +++ /dev/null @@ -1,463 +0,0 @@ -/* - * 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.appsettings; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.ContentValues; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; -import android.os.Bundle; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.provider.Telephony; -import android.support.v4.app.NavUtils; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.sms.ApnDatabase; -import com.android.messaging.sms.BugleApnSettingsLoader; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.PhoneUtils; - -public class ApnEditorActivity extends BugleActionBarActivity { - private static final int ERROR_DIALOG_ID = 0; - private static final String ERROR_MESSAGE_KEY = "error_msg"; - private ApnEditorFragment mApnEditorFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Display the fragment as the main content. - mApnEditorFragment = new ApnEditorFragment(); - mApnEditorFragment.setSubId(getIntent().getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID, - ParticipantData.DEFAULT_SELF_SUB_ID)); - getFragmentManager().beginTransaction() - .replace(android.R.id.content, mApnEditorFragment) - .commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected Dialog onCreateDialog(int id, Bundle args) { - - if (id == ERROR_DIALOG_ID) { - String msg = args.getString(ERROR_MESSAGE_KEY); - - return new AlertDialog.Builder(this) - .setPositiveButton(android.R.string.ok, null) - .setMessage(msg) - .create(); - } - - return super.onCreateDialog(id); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: { - if (mApnEditorFragment.validateAndSave(false)) { - finish(); - } - return true; - } - } - return super.onKeyDown(keyCode, event); - } - - @Override - protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { - super.onPrepareDialog(id, dialog); - - if (id == ERROR_DIALOG_ID) { - final String msg = args.getString(ERROR_MESSAGE_KEY); - - if (msg != null) { - ((AlertDialog) dialog).setMessage(msg); - } - } - } - - public static class ApnEditorFragment extends PreferenceFragment implements - SharedPreferences.OnSharedPreferenceChangeListener { - - private static final String SAVED_POS = "pos"; - - private static final int MENU_DELETE = Menu.FIRST; - private static final int MENU_SAVE = Menu.FIRST + 1; - private static final int MENU_CANCEL = Menu.FIRST + 2; - - private EditTextPreference mMmsProxy; - private EditTextPreference mMmsPort; - private EditTextPreference mName; - private EditTextPreference mMmsc; - private EditTextPreference mMcc; - private EditTextPreference mMnc; - private static String sNotSet; - - private String mCurMnc; - private String mCurMcc; - - private Cursor mCursor; - private boolean mNewApn; - private boolean mFirstTime; - private String mCurrentId; - - private int mSubId; - - /** - * Standard projection for the interesting columns of a normal note. - */ - private static final String[] sProjection = new String[] { - Telephony.Carriers._ID, // 0 - Telephony.Carriers.NAME, // 1 - Telephony.Carriers.MMSC, // 2 - Telephony.Carriers.MCC, // 3 - Telephony.Carriers.MNC, // 4 - Telephony.Carriers.NUMERIC, // 5 - Telephony.Carriers.MMSPROXY, // 6 - Telephony.Carriers.MMSPORT, // 7 - Telephony.Carriers.TYPE, // 8 - }; - - private static final int ID_INDEX = 0; - private static final int NAME_INDEX = 1; - private static final int MMSC_INDEX = 2; - private static final int MCC_INDEX = 3; - private static final int MNC_INDEX = 4; - private static final int NUMERIC_INDEX = 5; - private static final int MMSPROXY_INDEX = 6; - private static final int MMSPORT_INDEX = 7; - private static final int TYPE_INDEX = 8; - - private SQLiteDatabase mDatabase; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - addPreferencesFromResource(R.xml.apn_editor); - - setHasOptionsMenu(true); - - sNotSet = getResources().getString(R.string.apn_not_set); - mName = (EditTextPreference) findPreference("apn_name"); - mMmsProxy = (EditTextPreference) findPreference("apn_mms_proxy"); - mMmsPort = (EditTextPreference) findPreference("apn_mms_port"); - mMmsc = (EditTextPreference) findPreference("apn_mmsc"); - mMcc = (EditTextPreference) findPreference("apn_mcc"); - mMnc = (EditTextPreference) findPreference("apn_mnc"); - - final Intent intent = getActivity().getIntent(); - - mFirstTime = savedInstanceState == null; - mCurrentId = intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_APN_ROW_ID); - mNewApn = mCurrentId == null; - - mDatabase = ApnDatabase.getApnDatabase().getWritableDatabase(); - - if (mNewApn) { - fillUi(); - } else { - // Do initial query not on the UI thread - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - if (mCurrentId != null) { - String selection = Telephony.Carriers._ID + " =?"; - String[] selectionArgs = new String[]{ mCurrentId }; - mCursor = mDatabase.query(ApnDatabase.APN_TABLE, sProjection, selection, - selectionArgs, null, null, null, null); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if (mCursor == null) { - getActivity().finish(); - return; - } - mCursor.moveToFirst(); - - fillUi(); - } - }.execute((Void) null); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mCursor != null) { - mCursor.close(); - mCursor = null; - } - } - - @Override - public void onResume() { - super.onResume(); - getPreferenceScreen().getSharedPreferences() - .registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onPause() { - getPreferenceScreen().getSharedPreferences() - .unregisterOnSharedPreferenceChangeListener(this); - super.onPause(); - } - - public void setSubId(final int subId) { - mSubId = subId; - } - - private void fillUi() { - if (mNewApn) { - mMcc.setText(null); - mMnc.setText(null); - String numeric = PhoneUtils.get(mSubId).getSimOperatorNumeric(); - // MCC is first 3 chars and then in 2 - 3 chars of MNC - if (numeric != null && numeric.length() > 4) { - // Country code - String mcc = numeric.substring(0, 3); - // Network code - String mnc = numeric.substring(3); - // Auto populate MNC and MCC for new entries, based on what SIM reports - mMcc.setText(mcc); - mMnc.setText(mnc); - mCurMnc = mnc; - mCurMcc = mcc; - } - mName.setText(null); - mMmsProxy.setText(null); - mMmsPort.setText(null); - mMmsc.setText(null); - } else if (mFirstTime) { - mFirstTime = false; - // Fill in all the values from the db in both text editor and summary - mName.setText(mCursor.getString(NAME_INDEX)); - mMmsProxy.setText(mCursor.getString(MMSPROXY_INDEX)); - mMmsPort.setText(mCursor.getString(MMSPORT_INDEX)); - mMmsc.setText(mCursor.getString(MMSC_INDEX)); - mMcc.setText(mCursor.getString(MCC_INDEX)); - mMnc.setText(mCursor.getString(MNC_INDEX)); - } - - mName.setSummary(checkNull(mName.getText())); - mMmsProxy.setSummary(checkNull(mMmsProxy.getText())); - mMmsPort.setSummary(checkNull(mMmsPort.getText())); - mMmsc.setSummary(checkNull(mMmsc.getText())); - mMcc.setSummary(checkNull(mMcc.getText())); - mMnc.setSummary(checkNull(mMnc.getText())); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - // If it's a new APN, then cancel will delete the new entry in onPause - if (!mNewApn) { - menu.add(0, MENU_DELETE, 0, R.string.menu_delete_apn) - .setIcon(R.drawable.ic_delete_small_dark); - } - menu.add(0, MENU_SAVE, 0, R.string.menu_save_apn) - .setIcon(android.R.drawable.ic_menu_save); - menu.add(0, MENU_CANCEL, 0, R.string.menu_discard_apn_change) - .setIcon(android.R.drawable.ic_menu_close_clear_cancel); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case MENU_DELETE: - deleteApn(); - return true; - - case MENU_SAVE: - if (validateAndSave(false)) { - getActivity().finish(); - } - return true; - - case MENU_CANCEL: - getActivity().finish(); - return true; - - case android.R.id.home: - getActivity().onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onSaveInstanceState(Bundle icicle) { - super.onSaveInstanceState(icicle); - if (validateAndSave(true) && mCursor != null) { - icicle.putInt(SAVED_POS, mCursor.getInt(ID_INDEX)); - } - } - - /** - * Check the key fields' validity and save if valid. - * @param force save even if the fields are not valid, if the app is - * being suspended - * @return true if the data was saved - */ - private boolean validateAndSave(boolean force) { - final String name = checkNotSet(mName.getText()); - final String mcc = checkNotSet(mMcc.getText()); - final String mnc = checkNotSet(mMnc.getText()); - - if (getErrorMsg() != null && !force) { - final Bundle bundle = new Bundle(); - bundle.putString(ERROR_MESSAGE_KEY, getErrorMsg()); - getActivity().showDialog(ERROR_DIALOG_ID, bundle); - return false; - } - - // Make database changes not on the UI thread - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - ContentValues values = new ContentValues(); - - // Add a dummy name "Untitled", if the user exits the screen without adding a - // name but entered other information worth keeping. - values.put(Telephony.Carriers.NAME, name.length() < 1 ? - getResources().getString(R.string.untitled_apn) : name); - values.put(Telephony.Carriers.MMSPROXY, checkNotSet(mMmsProxy.getText())); - values.put(Telephony.Carriers.MMSPORT, checkNotSet(mMmsPort.getText())); - values.put(Telephony.Carriers.MMSC, checkNotSet(mMmsc.getText())); - - values.put(Telephony.Carriers.TYPE, BugleApnSettingsLoader.APN_TYPE_MMS); - - values.put(Telephony.Carriers.MCC, mcc); - values.put(Telephony.Carriers.MNC, mnc); - - values.put(Telephony.Carriers.NUMERIC, mcc + mnc); - - if (mCurMnc != null && mCurMcc != null) { - if (mCurMnc.equals(mnc) && mCurMcc.equals(mcc)) { - values.put(Telephony.Carriers.CURRENT, 1); - } - } - - if (mNewApn) { - mDatabase.insert(ApnDatabase.APN_TABLE, null, values); - } else { - // update the APN - String selection = Telephony.Carriers._ID + " =?"; - String[] selectionArgs = new String[]{ mCurrentId }; - int updated = mDatabase.update(ApnDatabase.APN_TABLE, values, - selection, selectionArgs); - } - return null; - } - }.execute((Void) null); - - return true; - } - - private void deleteApn() { - // Make database changes not on the UI thread - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - // delete the APN - String where = Telephony.Carriers._ID + " =?"; - String[] whereArgs = new String[]{ mCurrentId }; - - mDatabase.delete(ApnDatabase.APN_TABLE, where, whereArgs); - return null; - } - }.execute((Void) null); - - getActivity().finish(); - } - - private String checkNull(String value) { - if (value == null || value.length() == 0) { - return sNotSet; - } else { - return value; - } - } - - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Preference pref = findPreference(key); - if (pref != null) { - pref.setSummary(checkNull(sharedPreferences.getString(key, ""))); - } - } - - private String getErrorMsg() { - String errorMsg = null; - - String name = checkNotSet(mName.getText()); - String mcc = checkNotSet(mMcc.getText()); - String mnc = checkNotSet(mMnc.getText()); - - if (name.length() < 1) { - errorMsg = getString(R.string.error_apn_name_empty); - } else if (mcc.length() != 3) { - errorMsg = getString(R.string.error_mcc_not3); - } else if ((mnc.length() & 0xFFFE) != 2) { - errorMsg = getString(R.string.error_mnc_not23); - } - - return errorMsg; - } - - private String checkNotSet(String value) { - if (value == null || value.equals(sNotSet)) { - return ""; - } else { - return value; - } - } - } -} diff --git a/src/com/android/messaging/ui/appsettings/ApnPreference.java b/src/com/android/messaging/ui/appsettings/ApnPreference.java deleted file mode 100644 index 74c6a08..0000000 --- a/src/com/android/messaging/ui/appsettings/ApnPreference.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.appsettings; - -import android.content.Context; -import android.preference.Preference; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.RadioButton; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.ui.UIIntents; - -/** - * ApnPreference implements a pref, typically used as a list item, that has a title/summary on - * the left and a radio button on the right. - * - */ -public class ApnPreference extends Preference implements - CompoundButton.OnCheckedChangeListener, OnClickListener { - static final String TAG = "ApnPreference"; - - public ApnPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public ApnPreference(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.apnPreferenceStyle); - } - - public ApnPreference(Context context) { - this(context, null); - } - - private static String mSelectedKey = null; - private static CompoundButton mCurrentChecked = null; - private boolean mProtectFromCheckedChange = false; - private boolean mSelectable = true; - private int mSubId = ParticipantData.DEFAULT_SELF_SUB_ID; - - @Override - public View getView(View convertView, ViewGroup parent) { - View view = super.getView(convertView, parent); - - View widget = view.findViewById(R.id.apn_radiobutton); - if ((widget != null) && widget instanceof RadioButton) { - RadioButton rb = (RadioButton) widget; - if (mSelectable) { - rb.setOnCheckedChangeListener(this); - - boolean isChecked = getKey().equals(mSelectedKey); - if (isChecked) { - mCurrentChecked = rb; - mSelectedKey = getKey(); - } - - mProtectFromCheckedChange = true; - rb.setChecked(isChecked); - mProtectFromCheckedChange = false; - } else { - rb.setVisibility(View.GONE); - } - setApnRadioButtonContentDescription(rb); - } - - View textLayout = view.findViewById(R.id.text_layout); - if ((textLayout != null) && textLayout instanceof RelativeLayout) { - textLayout.setOnClickListener(this); - } - - return view; - } - - public void setApnRadioButtonContentDescription(final CompoundButton buttonView) { - final View widget = (View) buttonView.getParent(); - final TextView tv = (TextView) widget.findViewById(android.R.id.title); - final String apnTitle = tv.getText().toString(); - buttonView.setContentDescription(apnTitle); - } - - public boolean isChecked() { - return getKey().equals(mSelectedKey); - } - - public void setChecked() { - mSelectedKey = getKey(); - } - - public void setSubId(final int subId) { - mSubId = subId; - } - - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - Log.i(TAG, "ID: " + getKey() + " :" + isChecked); - if (mProtectFromCheckedChange) { - return; - } - - if (isChecked) { - if (mCurrentChecked != null) { - mCurrentChecked.setChecked(false); - } - mCurrentChecked = buttonView; - mSelectedKey = getKey(); - callChangeListener(mSelectedKey); - } else { - mCurrentChecked = null; - mSelectedKey = null; - } - setApnRadioButtonContentDescription(buttonView); - } - - public void onClick(android.view.View v) { - if ((v != null) && (R.id.text_layout == v.getId())) { - Context context = getContext(); - if (context != null) { - context.startActivity( - UIIntents.get().getApnEditorIntent(context, getKey(), mSubId)); - } - } - } - - public void setSelectable(boolean selectable) { - mSelectable = selectable; - } - - public boolean getSelectable() { - return mSelectable; - } -} diff --git a/src/com/android/messaging/ui/appsettings/ApnSettingsActivity.java b/src/com/android/messaging/ui/appsettings/ApnSettingsActivity.java deleted file mode 100644 index 28dfc2a..0000000 --- a/src/com/android/messaging/ui/appsettings/ApnSettingsActivity.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * 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.appsettings; - -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.os.UserManager; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceGroup; -import android.preference.PreferenceScreen; -import android.provider.Telephony; -import android.support.v4.app.NavUtils; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.sms.ApnDatabase; -import com.android.messaging.sms.BugleApnSettingsLoader; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; - -public class ApnSettingsActivity extends BugleActionBarActivity { - private static final int DIALOG_RESTORE_DEFAULTAPN = 1001; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Display the fragment as the main content. - final ApnSettingsFragment fragment = new ApnSettingsFragment(); - fragment.setSubId(getIntent().getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID, - ParticipantData.DEFAULT_SELF_SUB_ID)); - getFragmentManager().beginTransaction() - .replace(android.R.id.content, fragment) - .commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected Dialog onCreateDialog(int id) { - if (id == DIALOG_RESTORE_DEFAULTAPN) { - ProgressDialog dialog = new ProgressDialog(this); - dialog.setMessage(getResources().getString(R.string.restore_default_apn)); - dialog.setCancelable(false); - return dialog; - } - return null; - } - - public static class ApnSettingsFragment extends PreferenceFragment implements - Preference.OnPreferenceChangeListener { - public static final String EXTRA_POSITION = "position"; - - public static final String APN_ID = "apn_id"; - - private static final String[] APN_PROJECTION = { - Telephony.Carriers._ID, // 0 - Telephony.Carriers.NAME, // 1 - Telephony.Carriers.APN, // 2 - Telephony.Carriers.TYPE // 3 - }; - private static final int ID_INDEX = 0; - private static final int NAME_INDEX = 1; - private static final int APN_INDEX = 2; - private static final int TYPES_INDEX = 3; - - private static final int MENU_NEW = Menu.FIRST; - private static final int MENU_RESTORE = Menu.FIRST + 1; - - private static final int EVENT_RESTORE_DEFAULTAPN_START = 1; - private static final int EVENT_RESTORE_DEFAULTAPN_COMPLETE = 2; - - private static boolean mRestoreDefaultApnMode; - - private RestoreApnUiHandler mRestoreApnUiHandler; - private RestoreApnProcessHandler mRestoreApnProcessHandler; - private HandlerThread mRestoreDefaultApnThread; - - private String mSelectedKey; - - private static final ContentValues sCurrentNullMap; - private static final ContentValues sCurrentSetMap; - - private UserManager mUm; - - private boolean mUnavailable; - private int mSubId; - - static { - sCurrentNullMap = new ContentValues(1); - sCurrentNullMap.putNull(Telephony.Carriers.CURRENT); - - sCurrentSetMap = new ContentValues(1); - sCurrentSetMap.put(Telephony.Carriers.CURRENT, "2"); // 2 for user-selected APN, - // 1 for Bugle-selected APN - } - - private SQLiteDatabase mDatabase; - - public void setSubId(final int subId) { - mSubId = subId; - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - mDatabase = ApnDatabase.getApnDatabase().getWritableDatabase(); - - if (OsUtil.isAtLeastL()) { - mUm = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); - if (!mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { - setHasOptionsMenu(true); - } - } else { - setHasOptionsMenu(true); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final ListView lv = (ListView) getView().findViewById(android.R.id.list); - TextView empty = (TextView) getView().findViewById(android.R.id.empty); - if (empty != null) { - empty.setText(R.string.apn_settings_not_available); - lv.setEmptyView(empty); - } - - if (OsUtil.isAtLeastL() && - mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { - mUnavailable = true; - setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity())); - return; - } - - addPreferencesFromResource(R.xml.apn_settings); - - lv.setItemsCanFocus(true); - } - - @Override - public void onResume() { - super.onResume(); - - if (mUnavailable) { - return; - } - - if (!mRestoreDefaultApnMode) { - fillList(); - } - } - - @Override - public void onPause() { - super.onPause(); - - if (mUnavailable) { - return; - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (mRestoreDefaultApnThread != null) { - mRestoreDefaultApnThread.quit(); - } - } - - private void fillList() { - final String mccMnc = PhoneUtils.getMccMncString(PhoneUtils.get(mSubId).getMccMnc()); - - new AsyncTask<Void, Void, Cursor>() { - @Override - protected Cursor doInBackground(Void... params) { - String selection = Telephony.Carriers.NUMERIC + " =?"; - String[] selectionArgs = new String[]{ mccMnc }; - final Cursor cursor = mDatabase.query(ApnDatabase.APN_TABLE, APN_PROJECTION, - selection, selectionArgs, null, null, null, null); - return cursor; - } - - @Override - protected void onPostExecute(Cursor cursor) { - if (cursor != null) { - try { - PreferenceGroup apnList = (PreferenceGroup) - findPreference(getString(R.string.apn_list_pref_key)); - apnList.removeAll(); - - mSelectedKey = BugleApnSettingsLoader.getFirstTryApn(mDatabase, mccMnc); - while (cursor.moveToNext()) { - String name = cursor.getString(NAME_INDEX); - String apn = cursor.getString(APN_INDEX); - String key = cursor.getString(ID_INDEX); - String type = cursor.getString(TYPES_INDEX); - - if (BugleApnSettingsLoader.isValidApnType(type, - BugleApnSettingsLoader.APN_TYPE_MMS)) { - ApnPreference pref = new ApnPreference(getActivity()); - pref.setKey(key); - pref.setTitle(name); - pref.setSummary(apn); - pref.setPersistent(false); - pref.setOnPreferenceChangeListener(ApnSettingsFragment.this); - pref.setSelectable(true); - - // Turn on the radio button for the currently selected APN. If - // there is no selected APN, don't select an APN. - if ((mSelectedKey != null && mSelectedKey.equals(key))) { - pref.setChecked(); - } - apnList.addPreference(pref); - } - } - } finally { - cursor.close(); - } - } - } - }.execute((Void) null); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (!mUnavailable) { - menu.add(0, MENU_NEW, 0, - getResources().getString(R.string.menu_new_apn)) - .setIcon(R.drawable.ic_add_gray) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.add(0, MENU_RESTORE, 0, - getResources().getString(R.string.menu_restore_default_apn)) - .setIcon(android.R.drawable.ic_menu_upload); - } - - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case MENU_NEW: - addNewApn(); - return true; - - case MENU_RESTORE: - restoreDefaultApn(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void addNewApn() { - startActivity(UIIntents.get().getApnEditorIntent(getActivity(), null, mSubId)); - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, - Preference preference) { - startActivity( - UIIntents.get().getApnEditorIntent(getActivity(), preference.getKey(), mSubId)); - return true; - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (newValue instanceof String) { - setSelectedApnKey((String) newValue); - } - - return true; - } - - // current=2 means user selected APN - private static final String UPDATE_SELECTION = Telephony.Carriers.CURRENT + " =?"; - private static final String[] UPDATE_SELECTION_ARGS = new String[] { "2" }; - private void setSelectedApnKey(final String key) { - mSelectedKey = key; - - // Make database changes not on the UI thread - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - // null out the previous "current=2" APN - mDatabase.update(ApnDatabase.APN_TABLE, sCurrentNullMap, - UPDATE_SELECTION, UPDATE_SELECTION_ARGS); - - // set the new "current" APN (2) - String selection = Telephony.Carriers._ID + " =?"; - String[] selectionArgs = new String[]{ key }; - - mDatabase.update(ApnDatabase.APN_TABLE, sCurrentSetMap, - selection, selectionArgs); - return null; - } - }.execute((Void) null); - } - - private boolean restoreDefaultApn() { - getActivity().showDialog(DIALOG_RESTORE_DEFAULTAPN); - mRestoreDefaultApnMode = true; - - if (mRestoreApnUiHandler == null) { - mRestoreApnUiHandler = new RestoreApnUiHandler(); - } - - if (mRestoreApnProcessHandler == null || - mRestoreDefaultApnThread == null) { - mRestoreDefaultApnThread = new HandlerThread( - "Restore default APN Handler: Process Thread"); - mRestoreDefaultApnThread.start(); - mRestoreApnProcessHandler = new RestoreApnProcessHandler( - mRestoreDefaultApnThread.getLooper(), mRestoreApnUiHandler); - } - - mRestoreApnProcessHandler.sendEmptyMessage(EVENT_RESTORE_DEFAULTAPN_START); - return true; - } - - private class RestoreApnUiHandler extends Handler { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case EVENT_RESTORE_DEFAULTAPN_COMPLETE: - fillList(); - getPreferenceScreen().setEnabled(true); - mRestoreDefaultApnMode = false; - final Activity activity = getActivity(); - activity.dismissDialog(DIALOG_RESTORE_DEFAULTAPN); - Toast.makeText(activity, getResources().getString( - R.string.restore_default_apn_completed), Toast.LENGTH_LONG) - .show(); - break; - } - } - } - - private class RestoreApnProcessHandler extends Handler { - private Handler mCachedRestoreApnUiHandler; - - public RestoreApnProcessHandler(Looper looper, Handler restoreApnUiHandler) { - super(looper); - this.mCachedRestoreApnUiHandler = restoreApnUiHandler; - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case EVENT_RESTORE_DEFAULTAPN_START: - ApnDatabase.forceBuildAndLoadApnTables(); - mCachedRestoreApnUiHandler.sendEmptyMessage( - EVENT_RESTORE_DEFAULTAPN_COMPLETE); - break; - } - } - } - } -} diff --git a/src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java b/src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java deleted file mode 100644 index 906009f..0000000 --- a/src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * 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.appsettings; - -import android.app.FragmentTransaction; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.preference.RingtonePreference; -import android.preference.TwoStatePreference; -import android.provider.Settings; -import android.support.v4.app.NavUtils; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.LicenseActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.BuglePrefs; -import com.android.messaging.util.DebugUtils; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; - -public class ApplicationSettingsActivity extends BugleActionBarActivity { - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - final boolean topLevel = getIntent().getBooleanExtra( - UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, false); - if (topLevel) { - getSupportActionBar().setTitle(getString(R.string.settings_activity_title)); - } - - FragmentTransaction ft = getFragmentManager().beginTransaction(); - ft.replace(android.R.id.content, new ApplicationSettingsFragment()); - ft.commit(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - if (super.onCreateOptionsMenu(menu)) { - return true; - } - getMenuInflater().inflate(R.menu.settings_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - case R.id.action_license: - final Intent intent = new Intent(this, LicenseActivity.class); - startActivity(intent); - return true; - } - return super.onOptionsItemSelected(item); - } - - public static class ApplicationSettingsFragment extends PreferenceFragment implements - OnSharedPreferenceChangeListener { - - private String mNotificationsEnabledPreferenceKey; - private TwoStatePreference mNotificationsEnabledPreference; - private String mRingtonePreferenceKey; - private RingtonePreference mRingtonePreference; - private Preference mVibratePreference; - private String mSmsDisabledPrefKey; - private Preference mSmsDisabledPreference; - private String mSmsEnabledPrefKey; - private Preference mSmsEnabledPreference; - private boolean mIsSmsPreferenceClicked; - - public ApplicationSettingsFragment() { - // Required empty constructor - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getPreferenceManager().setSharedPreferencesName(BuglePrefs.SHARED_PREFERENCES_NAME); - addPreferencesFromResource(R.xml.preferences_application); - - mNotificationsEnabledPreferenceKey = - getString(R.string.notifications_enabled_pref_key); - mNotificationsEnabledPreference = (TwoStatePreference) findPreference( - mNotificationsEnabledPreferenceKey); - mRingtonePreferenceKey = getString(R.string.notification_sound_pref_key); - mRingtonePreference = (RingtonePreference) findPreference(mRingtonePreferenceKey); - mVibratePreference = findPreference( - getString(R.string.notification_vibration_pref_key)); - mSmsDisabledPrefKey = getString(R.string.sms_disabled_pref_key); - mSmsDisabledPreference = findPreference(mSmsDisabledPrefKey); - mSmsEnabledPrefKey = getString(R.string.sms_enabled_pref_key); - mSmsEnabledPreference = findPreference(mSmsEnabledPrefKey); - mIsSmsPreferenceClicked = false; - - final SharedPreferences prefs = getPreferenceScreen().getSharedPreferences(); - updateSoundSummary(prefs); - - if (!DebugUtils.isDebugEnabled()) { - final Preference debugCategory = findPreference(getString( - R.string.debug_pref_key)); - getPreferenceScreen().removePreference(debugCategory); - } - - final PreferenceScreen advancedScreen = (PreferenceScreen) findPreference( - getString(R.string.advanced_pref_key)); - final boolean topLevel = getActivity().getIntent().getBooleanExtra( - UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, false); - if (topLevel) { - advancedScreen.setIntent(UIIntents.get() - .getAdvancedSettingsIntent(getPreferenceScreen().getContext())); - } else { - // Hide the Advanced settings screen if this is not top-level; these are shown at - // the parent SettingsActivity. - getPreferenceScreen().removePreference(advancedScreen); - } - } - - @Override - public boolean onPreferenceTreeClick (PreferenceScreen preferenceScreen, - Preference preference) { - if (preference.getKey() == mSmsDisabledPrefKey || - preference.getKey() == mSmsEnabledPrefKey) { - mIsSmsPreferenceClicked = true; - } - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - - private void updateSoundSummary(final SharedPreferences sharedPreferences) { - // The silent ringtone just returns an empty string - String ringtoneName = mRingtonePreference.getContext().getString( - R.string.silent_ringtone); - - String ringtoneString = sharedPreferences.getString(mRingtonePreferenceKey, null); - - // Bootstrap the default setting in the preferences so that we have a valid selection - // in the dialog the first time that the user opens it. - if (ringtoneString == null) { - ringtoneString = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); - final SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(mRingtonePreferenceKey, ringtoneString); - editor.apply(); - } - - if (!TextUtils.isEmpty(ringtoneString)) { - final Uri ringtoneUri = Uri.parse(ringtoneString); - final Ringtone tone = RingtoneManager.getRingtone(mRingtonePreference.getContext(), - ringtoneUri); - - if (tone != null) { - ringtoneName = tone.getTitle(mRingtonePreference.getContext()); - } - } - - mRingtonePreference.setSummary(ringtoneName); - } - - private void updateSmsEnabledPreferences() { - if (!OsUtil.isAtLeastKLP()) { - getPreferenceScreen().removePreference(mSmsDisabledPreference); - getPreferenceScreen().removePreference(mSmsEnabledPreference); - } else { - final String defaultSmsAppLabel = getString(R.string.default_sms_app, - PhoneUtils.getDefault().getDefaultSmsAppLabel()); - boolean isSmsEnabledBeforeState; - boolean isSmsEnabledCurrentState; - if (PhoneUtils.getDefault().isDefaultSmsApp()) { - if (getPreferenceScreen().findPreference(mSmsEnabledPrefKey) == null) { - getPreferenceScreen().addPreference(mSmsEnabledPreference); - isSmsEnabledBeforeState = false; - } else { - isSmsEnabledBeforeState = true; - } - isSmsEnabledCurrentState = true; - getPreferenceScreen().removePreference(mSmsDisabledPreference); - mSmsEnabledPreference.setSummary(defaultSmsAppLabel); - } else { - if (getPreferenceScreen().findPreference(mSmsDisabledPrefKey) == null) { - getPreferenceScreen().addPreference(mSmsDisabledPreference); - isSmsEnabledBeforeState = true; - } else { - isSmsEnabledBeforeState = false; - } - isSmsEnabledCurrentState = false; - getPreferenceScreen().removePreference(mSmsEnabledPreference); - mSmsDisabledPreference.setSummary(defaultSmsAppLabel); - } - updateNotificationsPreferences(); - } - mIsSmsPreferenceClicked = false; - } - - private void updateNotificationsPreferences() { - final boolean canNotify = !OsUtil.isAtLeastKLP() - || PhoneUtils.getDefault().isDefaultSmsApp(); - mNotificationsEnabledPreference.setEnabled(canNotify); - } - - @Override - public void onStart() { - super.onStart(); - // We do this on start rather than on resume because the sound picker is in a - // separate activity. - getPreferenceScreen().getSharedPreferences() - .registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onResume() { - super.onResume(); - updateSmsEnabledPreferences(); - updateNotificationsPreferences(); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, - final String key) { - if (key.equals(mNotificationsEnabledPreferenceKey)) { - updateNotificationsPreferences(); - } else if (key.equals(mRingtonePreferenceKey)) { - updateSoundSummary(sharedPreferences); - } - } - - @Override - public void onStop() { - super.onStop(); - getPreferenceScreen().getSharedPreferences() - .unregisterOnSharedPreferenceChangeListener(this); - } - } -} diff --git a/src/com/android/messaging/ui/appsettings/GroupMmsSettingDialog.java b/src/com/android/messaging/ui/appsettings/GroupMmsSettingDialog.java deleted file mode 100644 index 739d2dc..0000000 --- a/src/com/android/messaging/ui/appsettings/GroupMmsSettingDialog.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.appsettings; - -import android.app.AlertDialog; -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.RadioButton; - -import com.android.messaging.R; -import com.android.messaging.util.Assert; -import com.android.messaging.util.BuglePrefs; - -/** - * Displays an on/off switch for group MMS setting for a given subscription. - */ -public class GroupMmsSettingDialog { - private final Context mContext; - private final int mSubId; - private AlertDialog mDialog; - - /** - * Shows a new group MMS setting dialog. - */ - public static void showDialog(final Context context, final int subId) { - new GroupMmsSettingDialog(context, subId).show(); - } - - private GroupMmsSettingDialog(final Context context, final int subId) { - mContext = context; - mSubId = subId; - } - - private void show() { - Assert.isNull(mDialog); - mDialog = new AlertDialog.Builder(mContext) - .setView(createView()) - .setTitle(R.string.group_mms_pref_title) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - - private void changeGroupMmsSettings(final boolean enable) { - Assert.notNull(mDialog); - BuglePrefs.getSubscriptionPrefs(mSubId).putBoolean( - mContext.getString(R.string.group_mms_pref_key), enable); - mDialog.dismiss(); - } - - private View createView() { - final LayoutInflater inflater = (LayoutInflater) mContext - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - final View rootView = inflater.inflate(R.layout.group_mms_setting_dialog, null, false); - final RadioButton disableButton = (RadioButton) - rootView.findViewById(R.id.disable_group_mms_button); - final RadioButton enableButton = (RadioButton) - rootView.findViewById(R.id.enable_group_mms_button); - disableButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - changeGroupMmsSettings(false); - } - }); - enableButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - changeGroupMmsSettings(true); - } - }); - final boolean mmsEnabled = BuglePrefs.getSubscriptionPrefs(mSubId).getBoolean( - mContext.getString(R.string.group_mms_pref_key), - mContext.getResources().getBoolean(R.bool.group_mms_pref_default)); - enableButton.setChecked(mmsEnabled); - disableButton.setChecked(!mmsEnabled); - return rootView; - } -} diff --git a/src/com/android/messaging/ui/appsettings/PerSubscriptionSettingsActivity.java b/src/com/android/messaging/ui/appsettings/PerSubscriptionSettingsActivity.java deleted file mode 100644 index e02823f..0000000 --- a/src/com/android/messaging/ui/appsettings/PerSubscriptionSettingsActivity.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * 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.appsettings; - -import android.app.FragmentTransaction; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceCategory; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.support.v4.app.NavUtils; -import android.text.TextUtils; -import android.view.MenuItem; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.ParticipantRefresh; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.sms.ApnDatabase; -import com.android.messaging.sms.MmsConfig; -import com.android.messaging.sms.MmsUtils; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.Assert; -import com.android.messaging.util.BuglePrefs; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.PhoneUtils; - -public class PerSubscriptionSettingsActivity extends BugleActionBarActivity { - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - final String title = getIntent().getStringExtra( - UIIntents.UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE); - if (!TextUtils.isEmpty(title)) { - getSupportActionBar().setTitle(title); - } else { - // This will fall back to the default title, i.e. "Messaging settings," so No-op. - } - - final FragmentTransaction ft = getFragmentManager().beginTransaction(); - final PerSubscriptionSettingsFragment fragment = new PerSubscriptionSettingsFragment(); - ft.replace(android.R.id.content, fragment); - ft.commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - public static class PerSubscriptionSettingsFragment extends PreferenceFragment - implements OnSharedPreferenceChangeListener { - private PhoneNumberPreference mPhoneNumberPreference; - private Preference mGroupMmsPreference; - private String mGroupMmsPrefKey; - private String mPhoneNumberKey; - private int mSubId; - - public PerSubscriptionSettingsFragment() { - // Required empty constructor - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Get sub id from launch intent - final Intent intent = getActivity().getIntent(); - Assert.notNull(intent); - mSubId = (intent != null) ? intent.getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID, - ParticipantData.DEFAULT_SELF_SUB_ID) : ParticipantData.DEFAULT_SELF_SUB_ID; - - final BuglePrefs subPrefs = Factory.get().getSubscriptionPrefs(mSubId); - getPreferenceManager().setSharedPreferencesName(subPrefs.getSharedPreferencesName()); - addPreferencesFromResource(R.xml.preferences_per_subscription); - - mPhoneNumberKey = getString(R.string.mms_phone_number_pref_key); - mPhoneNumberPreference = (PhoneNumberPreference) findPreference(mPhoneNumberKey); - final PreferenceCategory advancedCategory = (PreferenceCategory) - findPreference(getString(R.string.advanced_category_pref_key)); - final PreferenceCategory mmsCategory = (PreferenceCategory) - findPreference(getString(R.string.mms_messaging_category_pref_key)); - - mPhoneNumberPreference.setDefaultPhoneNumber( - PhoneUtils.get(mSubId).getCanonicalForSelf(false/*allowOverride*/), mSubId); - - mGroupMmsPrefKey = getString(R.string.group_mms_pref_key); - mGroupMmsPreference = findPreference(mGroupMmsPrefKey); - if (!MmsConfig.get(mSubId).getGroupMmsEnabled()) { - // Always show group messaging setting even if the SIM has no number - // If broadcast sms is selected, the SIM number is not needed - // If group mms is selected, the phone number dialog will popup when message - // is being sent, making sure we will have a self number for group mms. - mmsCategory.removePreference(mGroupMmsPreference); - } else { - mGroupMmsPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference pref) { - GroupMmsSettingDialog.showDialog(getActivity(), mSubId); - return true; - } - }); - updateGroupMmsPrefSummary(); - } - - if (!MmsConfig.get(mSubId).getSMSDeliveryReportsEnabled()) { - final Preference deliveryReportsPref = findPreference( - getString(R.string.delivery_reports_pref_key)); - mmsCategory.removePreference(deliveryReportsPref); - } - final Preference wirelessAlertPref = findPreference(getString( - R.string.wireless_alerts_key)); - if (!isCellBroadcastAppLinkEnabled()) { - advancedCategory.removePreference(wirelessAlertPref); - } else { - wirelessAlertPref.setOnPreferenceClickListener( - new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(final Preference preference) { - try { - startActivity(UIIntents.get().getWirelessAlertsIntent()); - } catch (final ActivityNotFoundException e) { - // Handle so we shouldn't crash if the wireless alerts - // implementation is broken. - LogUtil.e(LogUtil.BUGLE_TAG, - "Failed to launch wireless alerts activity", e); - } - return true; - } - }); - } - - // Access Point Names (APNs) - final Preference apnsPref = findPreference(getString(R.string.sms_apns_key)); - - if (MmsUtils.useSystemApnTable() && !ApnDatabase.doesDatabaseExist()) { - // Don't remove the ability to edit the local APN prefs if this device lets us - // access the system APN, but we can't find the MCC/MNC in the APN table and we - // created the local APN table in case the MCC/MNC was in there. In other words, - // if the local APN table exists, let the user edit it. - advancedCategory.removePreference(apnsPref); - } else { - final PreferenceScreen apnsScreen = (PreferenceScreen) findPreference( - getString(R.string.sms_apns_key)); - apnsScreen.setIntent(UIIntents.get() - .getApnSettingsIntent(getPreferenceScreen().getContext(), mSubId)); - } - - // We want to disable preferences if we are not the default app, but we do all of the - // above first so that the user sees the correct information on the screen - if (!PhoneUtils.getDefault().isDefaultSmsApp()) { - mGroupMmsPreference.setEnabled(false); - final Preference autoRetrieveMmsPreference = - findPreference(getString(R.string.auto_retrieve_mms_pref_key)); - autoRetrieveMmsPreference.setEnabled(false); - final Preference deliveryReportsPreference = - findPreference(getString(R.string.delivery_reports_pref_key)); - deliveryReportsPreference.setEnabled(false); - } - } - - private boolean isCellBroadcastAppLinkEnabled() { - if (!MmsConfig.get(mSubId).getShowCellBroadcast()) { - return false; - } - try { - final PackageManager pm = getActivity().getPackageManager(); - return pm.getApplicationEnabledSetting(UIIntents.CMAS_COMPONENT) - != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - } catch (final IllegalArgumentException ignored) { - // CMAS app not installed. - } - return false; - } - - private void updateGroupMmsPrefSummary() { - final boolean groupMmsEnabled = getPreferenceScreen().getSharedPreferences().getBoolean( - mGroupMmsPrefKey, getResources().getBoolean(R.bool.group_mms_pref_default)); - mGroupMmsPreference.setSummary(groupMmsEnabled ? - R.string.enable_group_mms : R.string.disable_group_mms); - } - - @Override - public void onResume() { - super.onResume(); - getPreferenceScreen().getSharedPreferences() - .registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, - final String key) { - if (key.equals(mGroupMmsPrefKey)) { - updateGroupMmsPrefSummary(); - } else if (key.equals(mPhoneNumberKey)) { - // Save the changed phone number in preferences specific to the sub id - final String newPhoneNumber = mPhoneNumberPreference.getText(); - final BuglePrefs subPrefs = BuglePrefs.getSubscriptionPrefs(mSubId); - if (TextUtils.isEmpty(newPhoneNumber)) { - subPrefs.remove(mPhoneNumberKey); - } else { - subPrefs.putString(getString(R.string.mms_phone_number_pref_key), - newPhoneNumber); - } - // Update the self participants so the new phone number will be reflected - // everywhere in the UI. - ParticipantRefresh.refreshSelfParticipants(); - } - } - - @Override - public void onPause() { - super.onPause(); - getPreferenceScreen().getSharedPreferences() - .unregisterOnSharedPreferenceChangeListener(this); - } - } -} diff --git a/src/com/android/messaging/ui/appsettings/PhoneNumberPreference.java b/src/com/android/messaging/ui/appsettings/PhoneNumberPreference.java deleted file mode 100644 index 0c9c018..0000000 --- a/src/com/android/messaging/ui/appsettings/PhoneNumberPreference.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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.appsettings; - -import android.content.Context; -import android.preference.EditTextPreference; -import android.support.v4.text.BidiFormatter; -import android.support.v4.text.TextDirectionHeuristicsCompat; -import android.text.InputType; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; - -import com.android.messaging.R; -import com.android.messaging.util.PhoneUtils; - -/** - * Preference that displays a phone number and allows editing via a dialog. - * <p> - * A default number can be assigned, which is shown in the preference view and - * used to populate the dialog editor when the preference value is not set. If - * the user sets the preference to a number equivalent to the default, the - * underlying preference is cleared. - */ -public class PhoneNumberPreference extends EditTextPreference { - - private String mDefaultPhoneNumber; - private int mSubId; - - public PhoneNumberPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - mDefaultPhoneNumber = ""; - } - - public void setDefaultPhoneNumber(final String phoneNumber, final int subscriptionId) { - mDefaultPhoneNumber = phoneNumber; - mSubId = subscriptionId; - } - - @Override - protected void onBindView(final View view) { - // Show the preference value if it's set, or the default number if not. - // If we don't have a default, fall back to a static string (e.g. Unknown). - String value = getText(); - if (TextUtils.isEmpty(value)) { - value = mDefaultPhoneNumber; - } - final String displayValue = (!TextUtils.isEmpty(value)) - ? PhoneUtils.get(mSubId).formatForDisplay(value) - : getContext().getString(R.string.unknown_phone_number_pref_display_value); - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - final String phoneNumber = bidiFormatter.unicodeWrap - (displayValue, TextDirectionHeuristicsCompat.LTR); - // Set the value as the summary and let the superclass populate the views - setSummary(phoneNumber); - super.onBindView(view); - } - - @Override - protected void onBindDialogView(final View view) { - super.onBindDialogView(view); - - final String value = getText(); - - // If the preference is empty, populate the EditText with the default number instead. - if (TextUtils.isEmpty(value) && !TextUtils.isEmpty(mDefaultPhoneNumber)) { - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - final String phoneNumber = bidiFormatter.unicodeWrap - (PhoneUtils.get(mSubId).getCanonicalBySystemLocale(mDefaultPhoneNumber), - TextDirectionHeuristicsCompat.LTR); - getEditText().setText(phoneNumber); - } - getEditText().setInputType(InputType.TYPE_CLASS_PHONE); - } - - @Override - protected void onDialogClosed(final boolean positiveResult) { - if (positiveResult && mDefaultPhoneNumber != null) { - final String value = getEditText().getText().toString(); - final PhoneUtils phoneUtils = PhoneUtils.get(mSubId); - final String phoneNumber = phoneUtils.getCanonicalBySystemLocale(value); - final String defaultPhoneNumber = phoneUtils.getCanonicalBySystemLocale( - mDefaultPhoneNumber); - - // If the new value is the default, clear the preference. - if (phoneNumber.equals(defaultPhoneNumber)) { - setText(""); - return; - } - } - super.onDialogClosed(positiveResult); - } - - @Override - public void setText(final String text) { - super.setText(text); - - // EditTextPreference doesn't show the value on the preference view, but we do. - // We thus need to force a rebind of the view when a new value is set. - notifyChanged(); - } -} diff --git a/src/com/android/messaging/ui/appsettings/SettingsActivity.java b/src/com/android/messaging/ui/appsettings/SettingsActivity.java deleted file mode 100644 index 75266d8..0000000 --- a/src/com/android/messaging/ui/appsettings/SettingsActivity.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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.appsettings; - -import android.app.Fragment; -import android.content.Context; -import android.os.Bundle; -import android.support.v4.app.NavUtils; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -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.data.SettingsData; -import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener; -import com.android.messaging.datamodel.data.SettingsData.SettingsItem; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.Assert; -import com.android.messaging.util.PhoneUtils; - -import java.util.ArrayList; -import java.util.List; - -/** - * Shows the "master" settings activity that contains two parts, one for application-wide settings - * (dubbed "General settings"), and one or more for per-subscription settings (dubbed "Messaging - * settings" for single-SIM, and the actual SIM name for multi-SIM). Clicking on either item - * (e.g. "General settings") will open the detail settings activity (ApplicationSettingsActivity - * in this case). - */ -public class SettingsActivity extends BugleActionBarActivity { - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Directly open the detailed settings page as the top-level settings activity if this is - // not a multi-SIM device. - if (PhoneUtils.getDefault().getActiveSubscriptionCount() <= 1) { - UIIntents.get().launchApplicationSettingsActivity(this, true /* topLevel */); - finish(); - } else { - getFragmentManager().beginTransaction() - .replace(android.R.id.content, new SettingsFragment()) - .commit(); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - public static class SettingsFragment extends Fragment implements SettingsDataListener { - private ListView mListView; - private SettingsListAdapter mAdapter; - private final Binding<SettingsData> mBinding = BindingBase.createBinding(this); - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mBinding.bind(DataModel.get().createSettingsData(getActivity(), this)); - mBinding.getData().init(getLoaderManager(), mBinding); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.settings_fragment, container, false); - mListView = (ListView) view.findViewById(android.R.id.list); - mAdapter = new SettingsListAdapter(getActivity()); - mListView.setAdapter(mAdapter); - return view; - } - - @Override - public void onDestroy() { - super.onDestroy(); - mBinding.unbind(); - } - - @Override - public void onSelfParticipantDataLoaded(SettingsData data) { - mBinding.ensureBound(data); - mAdapter.setSettingsItems(data.getSettingsItems()); - } - - /** - * An adapter that displays a list of SettingsItem. - */ - private class SettingsListAdapter extends ArrayAdapter<SettingsItem> { - public SettingsListAdapter(final Context context) { - super(context, R.layout.settings_item_view, new ArrayList<SettingsItem>()); - } - - public void setSettingsItems(final List<SettingsItem> newList) { - clear(); - addAll(newList); - notifyDataSetChanged(); - } - - @Override - public View getView(final int position, final View convertView, - final ViewGroup parent) { - View itemView; - if (convertView != null) { - itemView = convertView; - } else { - final LayoutInflater inflater = (LayoutInflater) getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - itemView = inflater.inflate( - R.layout.settings_item_view, parent, false); - } - final SettingsItem item = getItem(position); - final TextView titleTextView = (TextView) itemView.findViewById(R.id.title); - final TextView subtitleTextView = (TextView) itemView.findViewById(R.id.subtitle); - final String summaryText = item.getDisplayDetail(); - titleTextView.setText(item.getDisplayName()); - if (!TextUtils.isEmpty(summaryText)) { - subtitleTextView.setText(summaryText); - subtitleTextView.setVisibility(View.VISIBLE); - } else { - subtitleTextView.setVisibility(View.GONE); - } - itemView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - switch (item.getType()) { - case SettingsItem.TYPE_GENERAL_SETTINGS: - UIIntents.get().launchApplicationSettingsActivity(getActivity(), - false /* topLevel */); - break; - - case SettingsItem.TYPE_PER_SUBSCRIPTION_SETTINGS: - UIIntents.get().launchPerSubscriptionSettingsActivity(getActivity(), - item.getSubId(), item.getActivityTitle()); - break; - - default: - Assert.fail("unrecognized setting type!"); - break; - } - } - }); - return itemView; - } - } - } -} diff --git a/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserActivity.java b/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserActivity.java deleted file mode 100644 index a540597..0000000 --- a/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserActivity.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.attachmentchooser; - -import android.app.Fragment; -import android.os.Bundle; - -import com.android.messaging.R; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.attachmentchooser.AttachmentChooserFragment.AttachmentChooserFragmentHost; -import com.android.messaging.util.Assert; - -public class AttachmentChooserActivity extends BugleActionBarActivity implements - AttachmentChooserFragmentHost { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.attachment_chooser_activity); - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - if (fragment instanceof AttachmentChooserFragment) { - final String conversationId = - getIntent().getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID); - Assert.notNull(conversationId); - final AttachmentChooserFragment chooserFragment = - (AttachmentChooserFragment) fragment; - chooserFragment.setConversationId(conversationId); - chooserFragment.setHost(this); - } - } - - @Override - public void onConfirmSelection() { - setResult(RESULT_OK); - finish(); - } -} diff --git a/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserFragment.java b/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserFragment.java deleted file mode 100644 index b39dc3e..0000000 --- a/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserFragment.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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.attachmentchooser; - -import android.app.Fragment; -import android.content.Context; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.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.ArrayAdapter; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.binding.Binding; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.data.DraftMessageData; -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.attachmentchooser.AttachmentGridView.AttachmentGridHost; -import com.google.common.annotations.VisibleForTesting; - -import java.util.ArrayList; -import java.util.List; - -public class AttachmentChooserFragment extends Fragment implements DraftMessageDataListener, - AttachmentGridHost { - public interface AttachmentChooserFragmentHost { - void onConfirmSelection(); - } - - private AttachmentGridView mAttachmentGridView; - private AttachmentGridAdapter mAdapter; - private AttachmentChooserFragmentHost mHost; - - @VisibleForTesting - Binding<DraftMessageData> mBinding = BindingBase.createBinding(this); - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.attachment_chooser_fragment, container, false); - mAttachmentGridView = (AttachmentGridView) view.findViewById(R.id.grid); - mAdapter = new AttachmentGridAdapter(getActivity()); - mAttachmentGridView.setAdapter(mAdapter); - mAttachmentGridView.setHost(this); - setHasOptionsMenu(true); - return view; - } - - @Override - public void onDestroy() { - super.onDestroy(); - mBinding.unbind(); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.attachment_chooser_menu, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_confirm_selection: - confirmSelection(); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } - - @VisibleForTesting - void confirmSelection() { - if (mBinding.isBound()) { - mBinding.getData().removeExistingAttachments( - mAttachmentGridView.getUnselectedAttachments()); - mBinding.getData().saveToStorage(mBinding); - mHost.onConfirmSelection(); - } - } - - public void setConversationId(final String conversationId) { - mBinding.bind(DataModel.get().createDraftMessageData(conversationId)); - mBinding.getData().addListener(this); - mBinding.getData().loadFromStorage(mBinding, null, false); - } - - public void setHost(final AttachmentChooserFragmentHost host) { - mHost = host; - } - - @Override - public void onDraftChanged(final DraftMessageData data, final int changeFlags) { - mBinding.ensureBound(data); - if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) == - DraftMessageData.ATTACHMENTS_CHANGED) { - mAdapter.onAttachmentsLoaded(data.getReadOnlyAttachments()); - } - } - - @Override - public void onDraftAttachmentLimitReached(final DraftMessageData data) { - // Do nothing since the user is in the process of unselecting attachments. - } - - @Override - public void onDraftAttachmentLoadFailed() { - // Do nothing since the user is in the process of unselecting attachments. - } - - @Override - public void displayPhoto(final Rect viewRect, final Uri photoUri) { - final Uri imagesUri = MessagingContentProvider.buildDraftImagesUri( - mBinding.getData().getConversationId()); - UIIntents.get().launchFullScreenPhotoViewer( - getActivity(), photoUri, viewRect, imagesUri); - } - - @Override - public void updateSelectionCount(int count) { - if (getActivity() instanceof BugleActionBarActivity) { - final ActionBar actionBar = ((BugleActionBarActivity) getActivity()) - .getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getResources().getString( - R.string.attachment_chooser_selection, count)); - } - } - } - - class AttachmentGridAdapter extends ArrayAdapter<MessagePartData> { - public AttachmentGridAdapter(final Context context) { - super(context, R.layout.attachment_grid_item_view, new ArrayList<MessagePartData>()); - } - - public void onAttachmentsLoaded(final List<MessagePartData> attachments) { - clear(); - addAll(attachments); - notifyDataSetChanged(); - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) { - AttachmentGridItemView itemView; - final MessagePartData item = getItem(position); - if (convertView != null && convertView instanceof AttachmentGridItemView) { - itemView = (AttachmentGridItemView) convertView; - } else { - final LayoutInflater inflater = (LayoutInflater) getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - itemView = (AttachmentGridItemView) inflater.inflate( - R.layout.attachment_grid_item_view, parent, false); - } - itemView.bind(item, mAttachmentGridView); - return itemView; - } - } -} diff --git a/src/com/android/messaging/ui/attachmentchooser/AttachmentGridItemView.java b/src/com/android/messaging/ui/attachmentchooser/AttachmentGridItemView.java deleted file mode 100644 index 8bb7356..0000000 --- a/src/com/android/messaging/ui/attachmentchooser/AttachmentGridItemView.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.attachmentchooser; - -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.TouchDelegate; -import android.view.View; -import android.widget.CheckBox; -import android.widget.FrameLayout; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.ui.AttachmentPreviewFactory; -import com.android.messaging.util.Assert; -import com.google.common.annotations.VisibleForTesting; - -/** - * Shows an item in the attachment picker grid. - */ -public class AttachmentGridItemView extends FrameLayout { - public interface HostInterface { - boolean isItemSelected(MessagePartData attachment); - void onItemCheckedChanged(AttachmentGridItemView view, MessagePartData attachment); - void onItemClicked(AttachmentGridItemView view, MessagePartData attachment); - } - - @VisibleForTesting - MessagePartData mAttachmentData; - private FrameLayout mAttachmentViewContainer; - private CheckBox mCheckBox; - private HostInterface mHostInterface; - - public AttachmentGridItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mAttachmentViewContainer = (FrameLayout) findViewById(R.id.attachment_container); - mCheckBox = (CheckBox) findViewById(R.id.checkbox); - mCheckBox.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - mHostInterface.onItemCheckedChanged(AttachmentGridItemView.this, mAttachmentData); - } - }); - setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - mHostInterface.onItemClicked(AttachmentGridItemView.this, mAttachmentData); - } - }); - addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - // Enlarge the clickable region for the checkbox. - final int touchAreaIncrease = getResources().getDimensionPixelOffset( - R.dimen.attachment_grid_checkbox_area_increase); - final Rect region = new Rect(); - mCheckBox.getHitRect(region); - region.inset(-touchAreaIncrease, -touchAreaIncrease); - setTouchDelegate(new TouchDelegate(region, mCheckBox)); - } - }); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - // The grid view auto-fits the columns, so we want to let the height match the width - // to make the attachment preview square. - super.onMeasure(widthMeasureSpec, widthMeasureSpec); - } - - public void bind(final MessagePartData attachment, final HostInterface hostInterface) { - Assert.isTrue(attachment.isAttachment()); - mHostInterface = hostInterface; - updateSelectedState(); - if (mAttachmentData == null || !mAttachmentData.equals(attachment)) { - mAttachmentData = attachment; - updateAttachmentView(); - } - } - - @VisibleForTesting - HostInterface testGetHostInterface() { - return mHostInterface; - } - - public void updateSelectedState() { - mCheckBox.setChecked(mHostInterface.isItemSelected(mAttachmentData)); - } - - private void updateAttachmentView() { - mAttachmentViewContainer.removeAllViews(); - final LayoutInflater inflater = LayoutInflater.from(getContext()); - final View attachmentView = AttachmentPreviewFactory.createAttachmentPreview(inflater, - mAttachmentData, mAttachmentViewContainer, - AttachmentPreviewFactory.TYPE_CHOOSER_GRID, true /* startImageRequest */, null); - mAttachmentViewContainer.addView(attachmentView); - } -} diff --git a/src/com/android/messaging/ui/attachmentchooser/AttachmentGridView.java b/src/com/android/messaging/ui/attachmentchooser/AttachmentGridView.java deleted file mode 100644 index abf61dc..0000000 --- a/src/com/android/messaging/ui/attachmentchooser/AttachmentGridView.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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.attachmentchooser; - -import android.content.Context; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.widget.GridView; - -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.ui.attachmentchooser.AttachmentChooserFragment.AttachmentGridAdapter; -import com.android.messaging.util.Assert; -import com.android.messaging.util.UiUtils; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Displays a grid of attachment previews for the user to choose which to select/unselect - */ -public class AttachmentGridView extends GridView implements - AttachmentGridItemView.HostInterface { - public interface AttachmentGridHost { - void displayPhoto(final Rect viewRect, final Uri photoUri); - void updateSelectionCount(final int count); - } - - // By default everything is selected so only need to keep track of the unselected set. - private final Set<MessagePartData> mUnselectedSet; - private AttachmentGridHost mHost; - - public AttachmentGridView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mUnselectedSet = new HashSet<>(); - } - - public void setHost(final AttachmentGridHost host) { - mHost = host; - } - - @Override - public boolean isItemSelected(final MessagePartData attachment) { - return !mUnselectedSet.contains(attachment); - } - - @Override - public void onItemClicked(final AttachmentGridItemView view, final MessagePartData attachment) { - // If the item is an image, show the photo viewer. All the other types (video, audio, - // vcard) have internal click handling for showing previews so we don't need to handle them - if (attachment.isImage()) { - mHost.displayPhoto(UiUtils.getMeasuredBoundsOnScreen(view), attachment.getContentUri()); - } - } - - @Override - public void onItemCheckedChanged(AttachmentGridItemView view, MessagePartData attachment) { - // Toggle selection. - if (isItemSelected(attachment)) { - mUnselectedSet.add(attachment); - } else { - mUnselectedSet.remove(attachment); - } - view.updateSelectedState(); - updateSelectionCount(); - } - - public Set<MessagePartData> getUnselectedAttachments() { - return Collections.unmodifiableSet(mUnselectedSet); - } - - private void updateSelectionCount() { - final int count = getAdapter().getCount() - mUnselectedSet.size(); - Assert.isTrue(count >= 0); - mHost.updateSelectionCount(count); - } - - private void refreshViews() { - if (getAdapter() instanceof AttachmentGridAdapter) { - ((AttachmentGridAdapter) getAdapter()).notifyDataSetChanged(); - } - } - - @Override - public Parcelable onSaveInstanceState() { - final Parcelable superState = super.onSaveInstanceState(); - final SavedState savedState = new SavedState(superState); - savedState.unselectedParts = mUnselectedSet - .toArray(new MessagePartData[mUnselectedSet.size()]); - return savedState; - } - - @Override - public void onRestoreInstanceState(final Parcelable state) { - if (!(state instanceof SavedState)) { - super.onRestoreInstanceState(state); - return; - } - - final SavedState savedState = (SavedState) state; - super.onRestoreInstanceState(savedState.getSuperState()); - mUnselectedSet.clear(); - for (int i = 0; i < savedState.unselectedParts.length; i++) { - final MessagePartData unselectedPart = savedState.unselectedParts[i]; - mUnselectedSet.add(unselectedPart); - } - refreshViews(); - } - - /** - * Persists the item selection state to saved instance state so we can restore on activity - * recreation - */ - public static class SavedState extends BaseSavedState { - MessagePartData[] unselectedParts; - - SavedState(final Parcelable superState) { - super(superState); - } - - private SavedState(final Parcel in) { - super(in); - - // Read parts - final int partCount = in.readInt(); - unselectedParts = new MessagePartData[partCount]; - for (int i = 0; i < partCount; i++) { - unselectedParts[i] = ((MessagePartData) in.readParcelable( - MessagePartData.class.getClassLoader())); - } - } - - @Override - public void writeToParcel(final Parcel out, final int flags) { - super.writeToParcel(out, flags); - - // Write parts - out.writeInt(unselectedParts.length); - for (final MessagePartData image : unselectedParts) { - out.writeParcelable(image, flags); - } - } - - public static final Parcelable.Creator<SavedState> CREATOR = - new Parcelable.Creator<SavedState>() { - @Override - public SavedState createFromParcel(final Parcel in) { - return new SavedState(in); - } - @Override - public SavedState[] newArray(final int size) { - return new SavedState[size]; - } - }; - } -} diff --git a/src/com/android/messaging/ui/contact/AddContactsConfirmationDialog.java b/src/com/android/messaging/ui/contact/AddContactsConfirmationDialog.java deleted file mode 100644 index 9c1393d..0000000 --- a/src/com/android/messaging/ui/contact/AddContactsConfirmationDialog.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.contact; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.ui.ContactIconView; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.AccessibilityUtil; - -public class AddContactsConfirmationDialog implements DialogInterface.OnClickListener { - private final Context mContext; - private final Uri mAvatarUri; - private final String mNormalizedDestination; - - public AddContactsConfirmationDialog(final Context context, final Uri avatarUri, - final String normalizedDestination) { - mContext = context; - mAvatarUri = avatarUri; - mNormalizedDestination = normalizedDestination; - } - - public void show() { - final int confirmAddContactStringId = R.string.add_contact_confirmation; - final int cancelStringId = android.R.string.cancel; - final AlertDialog alertDialog = new AlertDialog.Builder(mContext) - .setTitle(R.string.add_contact_confirmation_dialog_title) - .setView(createBodyView()) - .setPositiveButton(confirmAddContactStringId, this) - .setNegativeButton(cancelStringId, null) - .create(); - alertDialog.show(); - final Resources resources = mContext.getResources(); - final Button cancelButton = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE); - if (cancelButton != null) { - cancelButton.setTextColor(resources.getColor(R.color.contact_picker_button_text_color)); - } - final Button addButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (addButton != null) { - addButton.setTextColor(resources.getColor(R.color.contact_picker_button_text_color)); - } - } - - @Override - public void onClick(final DialogInterface dialog, final int which) { - UIIntents.get().launchAddContactActivity(mContext, mNormalizedDestination); - } - - private View createBodyView() { - final View view = LayoutInflater.from(mContext).inflate( - R.layout.add_contacts_confirmation_dialog_body, null); - final ContactIconView iconView = (ContactIconView) view.findViewById(R.id.contact_icon); - iconView.setImageResourceUri(mAvatarUri); - final TextView textView = (TextView) view.findViewById(R.id.participant_name); - textView.setText(mNormalizedDestination); - // Accessibility reason : in case phone numbers are mixed in the display name, - // we need to vocalize it for talkback. - final String vocalizedDisplayName = AccessibilityUtil.getVocalizedPhoneNumber( - mContext.getResources(), mNormalizedDestination); - textView.setContentDescription(vocalizedDisplayName); - return view; - } -} diff --git a/src/com/android/messaging/ui/contact/AllContactsListViewHolder.java b/src/com/android/messaging/ui/contact/AllContactsListViewHolder.java deleted file mode 100644 index 7263c54..0000000 --- a/src/com/android/messaging/ui/contact/AllContactsListViewHolder.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; - -import com.android.messaging.R; -import com.android.messaging.ui.CustomHeaderPagerListViewHolder; -import com.android.messaging.ui.contact.ContactListItemView.HostInterface; - -/** - * Holds the all contacts view for the contact picker's view pager. - */ -public class AllContactsListViewHolder extends CustomHeaderPagerListViewHolder { - public AllContactsListViewHolder(final Context context, final HostInterface clivHostInterface) { - super(context, new ContactListAdapter(context, null, clivHostInterface, - true /* needAlphabetHeader */)); - } - - @Override - protected int getLayoutResId() { - return R.layout.all_contacts_list_view; - } - - @Override - protected int getPageTitleResId() { - return R.string.contact_picker_all_contacts_tab_title; - } - - @Override - protected int getEmptyViewResId() { - return R.id.empty_view; - } - - @Override - protected int getListViewResId() { - return R.id.all_contacts_list; - } - - @Override - protected int getEmptyViewTitleResId() { - return R.string.contact_list_empty_text; - } - - @Override - protected int getEmptyViewImageResId() { - return R.drawable.ic_oobe_freq_list; - } -} diff --git a/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java b/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java deleted file mode 100644 index 7df62de..0000000 --- a/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; -import android.graphics.drawable.StateListDrawable; -import android.net.Uri; -import android.support.v4.text.BidiFormatter; -import android.support.v4.text.TextDirectionHeuristicsCompat; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import com.android.ex.chips.DropdownChipLayouter; -import com.android.ex.chips.RecipientEntry; -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ContactListItemData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.ui.ContactIconView; -import com.android.messaging.util.Assert; -import com.android.messaging.util.AvatarUriUtil; -import com.android.messaging.util.ContactRecipientEntryUtils; - -/** - * An implementation for {@link DropdownChipLayouter}. Layouts the dropdown - * list in the ContactRecipientAutoCompleteView in Material style. - */ -public class ContactDropdownLayouter extends DropdownChipLayouter { - private final ContactListItemView.HostInterface mClivHostInterface; - - public ContactDropdownLayouter(final LayoutInflater inflater, final Context context, - final ContactListItemView.HostInterface clivHostInterface) { - super(inflater, context); - mClivHostInterface = new ContactListItemView.HostInterface() { - - @Override - public void onContactListItemClicked(final ContactListItemData item, - final ContactListItemView view) { - // The chips UI will handle auto-complete item click events, so No-op here. - } - - @Override - public boolean isContactSelected(final ContactListItemData item) { - // In chips drop down we don't show any selected checkmark per design. - return false; - } - }; - } - - /** - * Bind a drop down view to a RecipientEntry. We'd like regular dropdown items (BASE_RECIPIENT) - * to behave the same as regular ContactListItemViews, while using the chips library's - * item styling for alternates dropdown items (happens when you click on a chip). - */ - @Override - public View bindView(final View convertView, final ViewGroup parent, final RecipientEntry entry, - final int position, AdapterType type, final String substring, - final StateListDrawable deleteDrawable) { - if (type != AdapterType.BASE_RECIPIENT) { - if (type == AdapterType.SINGLE_RECIPIENT) { - // Treat single recipients the same way as alternates. The base implementation of - // single recipients would try to simplify the destination by tokenizing. We'd - // like to always show the full destination address per design request. - type = AdapterType.RECIPIENT_ALTERNATES; - } - return super.bindView(convertView, parent, entry, position, type, substring, - deleteDrawable); - } - - // Default to show all the information - // RTL : To format contact name and detail if they happen to be phone numbers. - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - final String displayName = bidiFormatter.unicodeWrap( - ContactRecipientEntryUtils.getDisplayNameForContactList(entry), - TextDirectionHeuristicsCompat.LTR); - final String destination = bidiFormatter.unicodeWrap( - ContactRecipientEntryUtils.formatDestination(entry), - TextDirectionHeuristicsCompat.LTR); - final View itemView = reuseOrInflateView(convertView, parent, type); - - // Bold the string that is matched. - final CharSequence[] styledResults = - getStyledResults(substring, displayName, destination); - - Assert.isTrue(itemView instanceof ContactListItemView); - final ContactListItemView contactListItemView = (ContactListItemView) itemView; - contactListItemView.setImageClickHandlerDisabled(true); - contactListItemView.bind(entry, styledResults[0], styledResults[1], - mClivHostInterface, (type == AdapterType.SINGLE_RECIPIENT)); - return itemView; - } - - @Override - protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view, - AdapterType type) { - if (showImage && view instanceof ContactIconView) { - final ContactIconView contactView = (ContactIconView) view; - // These show contact cards by default, but that isn't what we want here - contactView.setImageClickHandlerDisabled(true); - final Uri avatarUri = AvatarUriUtil.createAvatarUri( - ParticipantData.getFromRecipientEntry(entry)); - contactView.setImageResourceUri(avatarUri); - } else { - super.bindIconToView(showImage, entry, view, type); - } - } - - @Override - protected int getItemLayoutResId(AdapterType type) { - switch (type) { - case BASE_RECIPIENT: - return R.layout.contact_list_item_view; - case RECIPIENT_ALTERNATES: - return R.layout.chips_alternates_dropdown_item; - default: - return R.layout.chips_alternates_dropdown_item; - } - } - - @Override - protected int getAlternateItemLayoutResId(AdapterType type) { - return getItemLayoutResId(type); - } -} diff --git a/src/com/android/messaging/ui/contact/ContactListAdapter.java b/src/com/android/messaging/ui/contact/ContactListAdapter.java deleted file mode 100644 index d466b61..0000000 --- a/src/com/android/messaging/ui/contact/ContactListAdapter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; -import android.database.Cursor; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; -import android.widget.SectionIndexer; - -import com.android.messaging.R; -import com.android.messaging.util.Assert; - -public class ContactListAdapter extends CursorAdapter implements SectionIndexer { - private final ContactListItemView.HostInterface mClivHostInterface; - private final boolean mNeedAlphabetHeader; - private ContactSectionIndexer mSectionIndexer; - - public ContactListAdapter(final Context context, final Cursor cursor, - final ContactListItemView.HostInterface clivHostInterface, - final boolean needAlphabetHeader) { - super(context, cursor, 0); - mClivHostInterface = clivHostInterface; - mNeedAlphabetHeader = needAlphabetHeader; - mSectionIndexer = new ContactSectionIndexer(cursor); - } - - @Override - public void bindView(final View view, final Context context, final Cursor cursor) { - Assert.isTrue(view instanceof ContactListItemView); - final ContactListItemView contactListItemView = (ContactListItemView) view; - String alphabetHeader = null; - if (mNeedAlphabetHeader) { - final int position = cursor.getPosition(); - final int section = mSectionIndexer.getSectionForPosition(position); - // Check if the position is the first in the section. - if (mSectionIndexer.getPositionForSection(section) == position) { - alphabetHeader = (String) mSectionIndexer.getSections()[section]; - } - } - contactListItemView.bind(cursor, mClivHostInterface, mNeedAlphabetHeader, alphabetHeader); - } - - @Override - public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { - final LayoutInflater layoutInflater = LayoutInflater.from(context); - return layoutInflater.inflate(R.layout.contact_list_item_view, parent, false); - } - - @Override - public Cursor swapCursor(final Cursor newCursor) { - mSectionIndexer = new ContactSectionIndexer(newCursor); - return super.swapCursor(newCursor); - } - - @Override - public Object[] getSections() { - return mSectionIndexer.getSections(); - } - - @Override - public int getPositionForSection(final int sectionIndex) { - return mSectionIndexer.getPositionForSection(sectionIndex); - } - - @Override - public int getSectionForPosition(final int position) { - return mSectionIndexer.getSectionForPosition(position); - } -} diff --git a/src/com/android/messaging/ui/contact/ContactListItemView.java b/src/com/android/messaging/ui/contact/ContactListItemView.java deleted file mode 100644 index 6904da6..0000000 --- a/src/com/android/messaging/ui/contact/ContactListItemView.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.ex.chips.RecipientEntry; -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.data.ContactListItemData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.ui.ContactIconView; -import com.android.messaging.util.Assert; -import com.android.messaging.util.AvatarUriUtil; -import com.google.common.annotations.VisibleForTesting; - -/** - * The view for a single entry in a contact list. - */ -public class ContactListItemView extends LinearLayout implements OnClickListener { - public interface HostInterface { - void onContactListItemClicked(ContactListItemData item, ContactListItemView view); - boolean isContactSelected(ContactListItemData item); - } - - @VisibleForTesting - final ContactListItemData mData; - private TextView mContactNameTextView; - private TextView mContactDetailsTextView; - private TextView mContactDetailTypeTextView; - private TextView mAlphabetHeaderTextView; - private ContactIconView mContactIconView; - private ImageView mContactCheckmarkView; - private HostInterface mHostInterface; - private boolean mShouldShowAlphabetHeader; - - public ContactListItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mData = DataModel.get().createContactListItemData(); - } - - @Override - protected void onFinishInflate () { - mContactNameTextView = (TextView) findViewById(R.id.contact_name); - mContactDetailsTextView = (TextView) findViewById(R.id.contact_details); - mContactDetailTypeTextView = (TextView) findViewById(R.id.contact_detail_type); - mAlphabetHeaderTextView = (TextView) findViewById(R.id.alphabet_header); - mContactIconView = (ContactIconView) findViewById(R.id.contact_icon); - mContactCheckmarkView = (ImageView) findViewById(R.id.contact_checkmark); - } - - /** - * Fills in the data associated with this view by binding to a contact cursor provided by - * ContactUtil. - * @param cursor the contact cursor. - * @param hostInterface host interface to this view. - * @param shouldShowAlphabetHeader whether an alphabetical header should shown on the side - * of this view. If {@code headerLabel} is empty, we will still leave space for it. - * @param headerLabel the alphabetical header on the side of this view, if it should be shown. - */ - public void bind(final Cursor cursor, final HostInterface hostInterface, - final boolean shouldShowAlphabetHeader, final String headerLabel) { - mData.bind(cursor, headerLabel); - mHostInterface = hostInterface; - mShouldShowAlphabetHeader = shouldShowAlphabetHeader; - setOnClickListener(this); - updateViewAppearance(); - } - - /** - * Binds a RecipientEntry. This is used by the chips text view's dropdown layout. - * @param recipientEntry the source RecipientEntry provided by ContactDropdownLayouter, which - * was in turn directly from one of the existing chips, or from filtered results - * generated by ContactRecipientAdapter. - * @param styledName display name where the portion that matches the search text is bold. - * @param styledDestination number where the portion that matches the search text is bold. - * @param hostInterface host interface to this view. - * @param isSingleRecipient whether this item is shown as the only line item in the single - * recipient drop down from the chips view. If this is the case, we always show the - * contact avatar even if it's not a first-level entry. - */ - public void bind(final RecipientEntry recipientEntry, final CharSequence styledName, - final CharSequence styledDestination, final HostInterface hostInterface, - final boolean isSingleRecipient) { - mData.bind(recipientEntry, styledName, styledDestination, isSingleRecipient); - mHostInterface = hostInterface; - mShouldShowAlphabetHeader = false; - updateViewAppearance(); - } - - private void updateViewAppearance() { - mContactNameTextView.setText(mData.getDisplayName()); - mContactDetailsTextView.setText(mData.getDestination()); - mContactDetailTypeTextView.setText(Phone.getTypeLabel(getResources(), - mData.getDestinationType(), mData.getDestinationLabel())); - final RecipientEntry recipientEntry = mData.getRecipientEntry(); - final String destinationString = String.valueOf(mData.getDestination()); - if (mData.getIsSimpleContactItem()) { - // This is a special number-with-avatar type of contact (for unknown contact chips - // and for direct "send to destination" item). In this case, make sure we only show - // the display name (phone number) and the avatar and hide everything else. - final Uri avatarUri = AvatarUriUtil.createAvatarUri( - ParticipantData.getFromRecipientEntry(recipientEntry)); - mContactIconView.setImageResourceUri(avatarUri, mData.getContactId(), - mData.getLookupKey(), destinationString); - mContactIconView.setVisibility(VISIBLE); - mContactCheckmarkView.setVisibility(GONE); - mContactDetailTypeTextView.setVisibility(GONE); - mContactDetailsTextView.setVisibility(GONE); - mContactNameTextView.setVisibility(VISIBLE); - } else if (mData.getIsFirstLevel()) { - final Uri avatarUri = AvatarUriUtil.createAvatarUri( - ParticipantData.getFromRecipientEntry(recipientEntry)); - mContactIconView.setImageResourceUri(avatarUri, mData.getContactId(), - mData.getLookupKey(), destinationString); - mContactIconView.setVisibility(VISIBLE); - mContactNameTextView.setVisibility(VISIBLE); - final boolean isSelected = mHostInterface.isContactSelected(mData); - setSelected(isSelected); - mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE); - mContactDetailsTextView.setVisibility(VISIBLE); - mContactDetailTypeTextView.setVisibility(VISIBLE); - } else { - mContactIconView.setImageResourceUri(null); - mContactIconView.setVisibility(INVISIBLE); - mContactNameTextView.setVisibility(GONE); - final boolean isSelected = mHostInterface.isContactSelected(mData); - setSelected(isSelected); - mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE); - mContactDetailsTextView.setVisibility(VISIBLE); - mContactDetailTypeTextView.setVisibility(VISIBLE); - } - - if (mShouldShowAlphabetHeader) { - mAlphabetHeaderTextView.setVisibility(VISIBLE); - mAlphabetHeaderTextView.setText(mData.getAlphabetHeader()); - } else { - mAlphabetHeaderTextView.setVisibility(GONE); - } - } - - /** - * {@inheritDoc} from OnClickListener - */ - @Override - public void onClick(final View v) { - Assert.isTrue(v == this); - Assert.isTrue(mHostInterface != null); - mHostInterface.onContactListItemClicked(mData, this); - } - - public void setImageClickHandlerDisabled(final boolean isHandlerDisabled) { - mContactIconView.setImageClickHandlerDisabled(isHandlerDisabled); - } -} diff --git a/src/com/android/messaging/ui/contact/ContactPickerFragment.java b/src/com/android/messaging/ui/contact/ContactPickerFragment.java deleted file mode 100644 index d803087..0000000 --- a/src/com/android/messaging/ui/contact/ContactPickerFragment.java +++ /dev/null @@ -1,607 +0,0 @@ -/* - * 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.contact; - -import android.app.Activity; -import android.app.Fragment; -import android.database.Cursor; -import android.graphics.Rect; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.Toolbar; -import android.support.v7.widget.Toolbar.OnMenuItemClickListener; -import android.text.Editable; -import android.text.InputType; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.transition.Explode; -import android.transition.Transition; -import android.transition.Transition.EpicenterCallback; -import android.transition.TransitionManager; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.action.ActionMonitor; -import com.android.messaging.datamodel.action.GetOrCreateConversationAction; -import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener; -import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor; -import com.android.messaging.datamodel.binding.Binding; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.data.ContactListItemData; -import com.android.messaging.datamodel.data.ContactPickerData; -import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.ui.CustomHeaderPagerViewHolder; -import com.android.messaging.ui.CustomHeaderViewPager; -import com.android.messaging.ui.animation.ViewGroupItemVerticalExplodeAnimation; -import com.android.messaging.ui.contact.ContactRecipientAutoCompleteView.ContactChipsChangeListener; -import com.android.messaging.util.Assert; -import com.android.messaging.util.Assert.RunsOnMainThread; -import com.android.messaging.util.ContactUtil; -import com.android.messaging.util.ImeUtil; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.UiUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.util.ArrayList; -import java.util.Set; - - -/** - * Shows lists of contacts to start conversations with. - */ -public class ContactPickerFragment extends Fragment implements ContactPickerDataListener, - ContactListItemView.HostInterface, ContactChipsChangeListener, OnMenuItemClickListener, - GetOrCreateConversationActionListener { - public static final String FRAGMENT_TAG = "contactpicker"; - - // Undefined contact picker mode. We should never be in this state after the host activity has - // been created. - public static final int MODE_UNDEFINED = 0; - - // The initial contact picker mode for starting a new conversation with one contact. - public static final int MODE_PICK_INITIAL_CONTACT = 1; - - // The contact picker mode where one initial contact has been picked and we are showing - // only the chips edit box. - public static final int MODE_CHIPS_ONLY = 2; - - // The contact picker mode for picking more contacts after starting the initial 1-1. - public static final int MODE_PICK_MORE_CONTACTS = 3; - - // The contact picker mode when max number of participants is reached. - public static final int MODE_PICK_MAX_PARTICIPANTS = 4; - - public interface ContactPickerFragmentHost { - void onGetOrCreateNewConversation(String conversationId); - void onBackButtonPressed(); - void onInitiateAddMoreParticipants(); - void onParticipantCountChanged(boolean canAddMoreParticipants); - void invalidateActionBar(); - } - - @VisibleForTesting - final Binding<ContactPickerData> mBinding = BindingBase.createBinding(this); - - private ContactPickerFragmentHost mHost; - private ContactRecipientAutoCompleteView mRecipientTextView; - private CustomHeaderViewPager mCustomHeaderViewPager; - private AllContactsListViewHolder mAllContactsListViewHolder; - private FrequentContactsListViewHolder mFrequentContactsListViewHolder; - private View mRootView; - private View mPendingExplodeView; - private View mComposeDivider; - private Toolbar mToolbar; - private int mContactPickingMode = MODE_UNDEFINED; - - // Keeps track of the currently selected phone numbers in the chips view to enable fast lookup. - private Set<String> mSelectedPhoneNumbers = null; - - /** - * {@inheritDoc} from Fragment - */ - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mAllContactsListViewHolder = new AllContactsListViewHolder(getActivity(), this); - mFrequentContactsListViewHolder = new FrequentContactsListViewHolder(getActivity(), this); - - if (ContactUtil.hasReadContactsPermission()) { - mBinding.bind(DataModel.get().createContactPickerData(getActivity(), this)); - mBinding.getData().init(getLoaderManager(), mBinding); - } - } - - /** - * {@inheritDoc} from Fragment - */ - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.contact_picker_fragment, container, false); - mRecipientTextView = (ContactRecipientAutoCompleteView) - view.findViewById(R.id.recipient_text_view); - mRecipientTextView.setThreshold(0); - mRecipientTextView.setDropDownAnchor(R.id.compose_contact_divider); - - mRecipientTextView.setContactChipsListener(this); - mRecipientTextView.setDropdownChipLayouter(new ContactDropdownLayouter(inflater, - getActivity(), this)); - mRecipientTextView.setAdapter(new ContactRecipientAdapter(getActivity(), this)); - mRecipientTextView.addTextChangedListener(new TextWatcher() { - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, - final int count) { - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, - final int after) { - } - - @Override - public void afterTextChanged(final Editable s) { - updateTextInputButtonsVisibility(); - } - }); - - final CustomHeaderPagerViewHolder[] viewHolders = { - mFrequentContactsListViewHolder, - mAllContactsListViewHolder }; - - mCustomHeaderViewPager = (CustomHeaderViewPager) view.findViewById(R.id.contact_pager); - mCustomHeaderViewPager.setViewHolders(viewHolders); - mCustomHeaderViewPager.setViewPagerTabHeight(CustomHeaderViewPager.DEFAULT_TAB_STRIP_SIZE); - mCustomHeaderViewPager.setBackgroundColor(getResources() - .getColor(R.color.contact_picker_background)); - - // The view pager defaults to the frequent contacts page. - mCustomHeaderViewPager.setCurrentItem(0); - - mToolbar = (Toolbar) view.findViewById(R.id.toolbar); - mToolbar.setNavigationIcon(R.drawable.ic_arrow_back_light); - mToolbar.setNavigationContentDescription(R.string.back); - mToolbar.setNavigationOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - mHost.onBackButtonPressed(); - } - }); - - mToolbar.inflateMenu(R.menu.compose_menu); - mToolbar.setOnMenuItemClickListener(this); - - mComposeDivider = view.findViewById(R.id.compose_contact_divider); - mRootView = view; - return view; - } - - /** - * {@inheritDoc} - * - * Called when the host activity has been created. At this point, the host activity should - * have set the contact picking mode for us so that we may update our visuals. - */ - @Override - public void onActivityCreated(final Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Assert.isTrue(mContactPickingMode != MODE_UNDEFINED); - updateVisualsForContactPickingMode(false /* animate */); - mHost.invalidateActionBar(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - // We could not have bound to the data if the permission was denied. - if (mBinding.isBound()) { - mBinding.unbind(); - } - - if (mMonitor != null) { - mMonitor.unregister(); - } - mMonitor = null; - } - - @Override - public boolean onMenuItemClick(final MenuItem menuItem) { - switch (menuItem.getItemId()) { - case R.id.action_ime_dialpad_toggle: - final int baseInputType = InputType.TYPE_TEXT_FLAG_MULTI_LINE; - if ((mRecipientTextView.getInputType() & InputType.TYPE_CLASS_PHONE) != - InputType.TYPE_CLASS_PHONE) { - mRecipientTextView.setInputType(baseInputType | InputType.TYPE_CLASS_PHONE); - menuItem.setIcon(R.drawable.ic_ime_light); - } else { - mRecipientTextView.setInputType(baseInputType | InputType.TYPE_CLASS_TEXT); - menuItem.setIcon(R.drawable.ic_numeric_dialpad); - } - ImeUtil.get().showImeKeyboard(getActivity(), mRecipientTextView); - return true; - - case R.id.action_add_more_participants: - mHost.onInitiateAddMoreParticipants(); - return true; - - case R.id.action_confirm_participants: - maybeGetOrCreateConversation(); - return true; - - case R.id.action_delete_text: - Assert.equals(MODE_PICK_INITIAL_CONTACT, mContactPickingMode); - mRecipientTextView.setText(""); - return true; - } - return false; - } - - @Override // From ContactPickerDataListener - public void onAllContactsCursorUpdated(final Cursor data) { - mBinding.ensureBound(); - mAllContactsListViewHolder.onContactsCursorUpdated(data); - } - - @Override // From ContactPickerDataListener - public void onFrequentContactsCursorUpdated(final Cursor data) { - mBinding.ensureBound(); - mFrequentContactsListViewHolder.onContactsCursorUpdated(data); - if (data != null && data.getCount() == 0) { - // Show the all contacts list when there's no frequents. - mCustomHeaderViewPager.setCurrentItem(1); - } - } - - @Override // From ContactListItemView.HostInterface - public void onContactListItemClicked(final ContactListItemData item, - final ContactListItemView view) { - if (!isContactSelected(item)) { - if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT) { - mPendingExplodeView = view; - } - mRecipientTextView.appendRecipientEntry(item.getRecipientEntry()); - } else if (mContactPickingMode != MODE_PICK_INITIAL_CONTACT) { - mRecipientTextView.removeRecipientEntry(item.getRecipientEntry()); - } - } - - @Override // From ContactListItemView.HostInterface - public boolean isContactSelected(final ContactListItemData item) { - return mSelectedPhoneNumbers != null && - mSelectedPhoneNumbers.contains(PhoneUtils.getDefault().getCanonicalBySystemLocale( - item.getRecipientEntry().getDestination())); - } - - /** - * Call this immediately after attaching the fragment, or when there's a ui state change that - * changes our host (i.e. restore from saved instance state). - */ - public void setHost(final ContactPickerFragmentHost host) { - mHost = host; - } - - public void setContactPickingMode(final int mode, final boolean animate) { - if (mContactPickingMode != mode) { - // Guard against impossible transitions. - Assert.isTrue( - // We may start from undefined mode to any mode when we are restoring state. - (mContactPickingMode == MODE_UNDEFINED) || - (mContactPickingMode == MODE_PICK_INITIAL_CONTACT && mode == MODE_CHIPS_ONLY) || - (mContactPickingMode == MODE_CHIPS_ONLY && mode == MODE_PICK_MORE_CONTACTS) || - (mContactPickingMode == MODE_PICK_MORE_CONTACTS - && mode == MODE_PICK_MAX_PARTICIPANTS) || - (mContactPickingMode == MODE_PICK_MAX_PARTICIPANTS - && mode == MODE_PICK_MORE_CONTACTS)); - - mContactPickingMode = mode; - updateVisualsForContactPickingMode(animate); - } - } - - private void showImeKeyboard() { - Assert.notNull(mRecipientTextView); - mRecipientTextView.requestFocus(); - - // showImeKeyboard() won't work until the layout is ready, so wait until layout is complete - // before showing the soft keyboard. - UiUtils.doOnceAfterLayoutChange(mRootView, new Runnable() { - @Override - public void run() { - final Activity activity = getActivity(); - if (activity != null) { - ImeUtil.get().showImeKeyboard(activity, mRecipientTextView); - } - } - }); - mRecipientTextView.invalidate(); - } - - private void updateVisualsForContactPickingMode(final boolean animate) { - // Don't update visuals if the visuals haven't been inflated yet. - if (mRootView != null) { - final Menu menu = mToolbar.getMenu(); - final MenuItem addMoreParticipantsItem = menu.findItem( - R.id.action_add_more_participants); - final MenuItem confirmParticipantsItem = menu.findItem( - R.id.action_confirm_participants); - switch (mContactPickingMode) { - case MODE_PICK_INITIAL_CONTACT: - addMoreParticipantsItem.setVisible(false); - confirmParticipantsItem.setVisible(false); - mCustomHeaderViewPager.setVisibility(View.VISIBLE); - mComposeDivider.setVisibility(View.INVISIBLE); - mRecipientTextView.setEnabled(true); - showImeKeyboard(); - break; - - case MODE_CHIPS_ONLY: - if (animate) { - if (mPendingExplodeView == null) { - // The user didn't click on any contact item, so use the toolbar as - // the view to "explode." - mPendingExplodeView = mToolbar; - } - startExplodeTransitionForContactLists(false /* show */); - - ViewGroupItemVerticalExplodeAnimation.startAnimationForView( - mCustomHeaderViewPager, mPendingExplodeView, mRootView, - true /* snapshotView */, UiUtils.COMPOSE_TRANSITION_DURATION); - showHideContactPagerWithAnimation(false /* show */); - } else { - mCustomHeaderViewPager.setVisibility(View.GONE); - } - - addMoreParticipantsItem.setVisible(true); - confirmParticipantsItem.setVisible(false); - mComposeDivider.setVisibility(View.VISIBLE); - mRecipientTextView.setEnabled(true); - break; - - case MODE_PICK_MORE_CONTACTS: - if (animate) { - // Correctly set the start visibility state for the view pager and - // individual list items (hidden initially), so that the transition - // manager can properly track the visibility change for the explode. - mCustomHeaderViewPager.setVisibility(View.VISIBLE); - toggleContactListItemsVisibilityForPendingTransition(false /* show */); - startExplodeTransitionForContactLists(true /* show */); - } - addMoreParticipantsItem.setVisible(false); - confirmParticipantsItem.setVisible(true); - mCustomHeaderViewPager.setVisibility(View.VISIBLE); - mComposeDivider.setVisibility(View.INVISIBLE); - mRecipientTextView.setEnabled(true); - showImeKeyboard(); - break; - - case MODE_PICK_MAX_PARTICIPANTS: - addMoreParticipantsItem.setVisible(false); - confirmParticipantsItem.setVisible(true); - mCustomHeaderViewPager.setVisibility(View.VISIBLE); - mComposeDivider.setVisibility(View.INVISIBLE); - // TODO: Verify that this is okay for accessibility - mRecipientTextView.setEnabled(false); - break; - - default: - Assert.fail("Unsupported contact picker mode!"); - break; - } - updateTextInputButtonsVisibility(); - } - } - - private void updateTextInputButtonsVisibility() { - final Menu menu = mToolbar.getMenu(); - final MenuItem keypadToggleItem = menu.findItem(R.id.action_ime_dialpad_toggle); - final MenuItem deleteTextItem = menu.findItem(R.id.action_delete_text); - if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT) { - if (TextUtils.isEmpty(mRecipientTextView.getText())) { - deleteTextItem.setVisible(false); - keypadToggleItem.setVisible(true); - } else { - deleteTextItem.setVisible(true); - keypadToggleItem.setVisible(false); - } - } else { - deleteTextItem.setVisible(false); - keypadToggleItem.setVisible(false); - } - } - - private void maybeGetOrCreateConversation() { - final ArrayList<ParticipantData> participants = - mRecipientTextView.getRecipientParticipantDataForConversationCreation(); - if (ContactPickerData.isTooManyParticipants(participants.size())) { - UiUtils.showToast(R.string.too_many_participants); - } else if (participants.size() > 0 && mMonitor == null) { - mMonitor = GetOrCreateConversationAction.getOrCreateConversation(participants, - null, this); - } - } - - /** - * Watches changes in contact chips to determine possible state transitions (e.g. creating - * the initial conversation, adding more participants or finish the current conversation) - */ - @Override - public void onContactChipsChanged(final int oldCount, final int newCount) { - Assert.isTrue(oldCount != newCount); - if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT) { - // Initial picking mode. Start a conversation once a recipient has been picked. - maybeGetOrCreateConversation(); - } else if (mContactPickingMode == MODE_CHIPS_ONLY) { - // oldCount == 0 means we are restoring from savedInstanceState to add the existing - // chips, don't switch to "add more participants" mode in this case. - if (oldCount > 0 && mRecipientTextView.isFocused()) { - // Chips only mode. The user may have picked an additional contact or deleted the - // only existing contact. Either way, switch to picking more participants mode. - mHost.onInitiateAddMoreParticipants(); - } - } - mHost.onParticipantCountChanged(ContactPickerData.getCanAddMoreParticipants(newCount)); - - // Refresh our local copy of the selected chips set to keep it up-to-date. - mSelectedPhoneNumbers = mRecipientTextView.getSelectedDestinations(); - invalidateContactLists(); - } - - /** - * Listens for notification that invalid contacts have been removed during resolving them. - * These contacts were not local contacts, valid email, or valid phone numbers - */ - @Override - public void onInvalidContactChipsPruned(final int prunedCount) { - Assert.isTrue(prunedCount > 0); - UiUtils.showToast(R.plurals.add_invalid_contact_error, prunedCount); - } - - /** - * Listens for notification that the user has pressed enter/done on the keyboard with all - * contacts in place and we should create or go to the existing conversation now - */ - @Override - public void onEntryComplete() { - if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT || - mContactPickingMode == MODE_PICK_MORE_CONTACTS || - mContactPickingMode == MODE_PICK_MAX_PARTICIPANTS) { - // Avoid multiple calls to create in race cases (hit done right after selecting contact) - maybeGetOrCreateConversation(); - } - } - - private void invalidateContactLists() { - mAllContactsListViewHolder.invalidateList(); - mFrequentContactsListViewHolder.invalidateList(); - } - - /** - * Kicks off a scene transition that animates visibility changes of individual contact list - * items via explode animation. - * @param show whether the contact lists are to be shown or hidden. - */ - private void startExplodeTransitionForContactLists(final boolean show) { - if (!OsUtil.isAtLeastL()) { - // Explode animation is not supported pre-L. - return; - } - final Explode transition = new Explode(); - final Rect epicenter = mPendingExplodeView == null ? null : - UiUtils.getMeasuredBoundsOnScreen(mPendingExplodeView); - transition.setDuration(UiUtils.COMPOSE_TRANSITION_DURATION); - transition.setInterpolator(UiUtils.EASE_IN_INTERPOLATOR); - transition.setEpicenterCallback(new EpicenterCallback() { - @Override - public Rect onGetEpicenter(final Transition transition) { - return epicenter; - } - }); - - // Kick off the delayed scene explode transition. Anything happens after this line in this - // method before the next frame will be tracked by the transition manager for visibility - // changes and animated accordingly. - TransitionManager.beginDelayedTransition(mCustomHeaderViewPager, - transition); - - toggleContactListItemsVisibilityForPendingTransition(show); - } - - /** - * Toggle the visibility of contact list items in the contact lists for them to be tracked by - * the transition manager for pending explode transition. - */ - private void toggleContactListItemsVisibilityForPendingTransition(final boolean show) { - if (!OsUtil.isAtLeastL()) { - // Explode animation is not supported pre-L. - return; - } - mAllContactsListViewHolder.toggleVisibilityForPendingTransition(show, mPendingExplodeView); - mFrequentContactsListViewHolder.toggleVisibilityForPendingTransition(show, - mPendingExplodeView); - } - - private void showHideContactPagerWithAnimation(final boolean show) { - final boolean isPagerVisible = (mCustomHeaderViewPager.getVisibility() == View.VISIBLE); - if (show == isPagerVisible) { - return; - } - - mCustomHeaderViewPager.animate().alpha(show ? 1F : 0F) - .setStartDelay(!show ? UiUtils.COMPOSE_TRANSITION_DURATION : 0) - .withStartAction(new Runnable() { - @Override - public void run() { - mCustomHeaderViewPager.setVisibility(View.VISIBLE); - mCustomHeaderViewPager.setAlpha(show ? 0F : 1F); - } - }) - .withEndAction(new Runnable() { - @Override - public void run() { - mCustomHeaderViewPager.setVisibility(show ? View.VISIBLE : View.GONE); - mCustomHeaderViewPager.setAlpha(1F); - } - }); - } - - @Override - public void onContactCustomColorLoaded(final ContactPickerData data) { - mBinding.ensureBound(data); - invalidateContactLists(); - } - - public void updateActionBar(final ActionBar actionBar) { - // Hide the action bar for contact picker mode. The custom ToolBar containing chips UI - // etc. will take the spot of the action bar. - actionBar.hide(); - UiUtils.setStatusBarColor(getActivity(), - getResources().getColor(R.color.compose_notification_bar_background)); - } - - private GetOrCreateConversationActionMonitor mMonitor; - - @Override - @RunsOnMainThread - public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor, - final Object data, final String conversationId) { - Assert.isTrue(monitor == mMonitor); - Assert.isTrue(conversationId != null); - - mRecipientTextView.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | - InputType.TYPE_CLASS_TEXT); - mHost.onGetOrCreateNewConversation(conversationId); - - mMonitor = null; - } - - @Override - @RunsOnMainThread - public void onGetOrCreateConversationFailed(final ActionMonitor monitor, - final Object data) { - Assert.isTrue(monitor == mMonitor); - LogUtil.e(LogUtil.BUGLE_TAG, "onGetOrCreateConversationFailed"); - mMonitor = null; - } -} diff --git a/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java b/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java deleted file mode 100644 index 25f422e..0000000 --- a/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; -import android.database.Cursor; -import android.database.MergeCursor; -import android.support.v4.util.Pair; -import android.text.TextUtils; -import android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; -import android.widget.Filter; - -import com.android.ex.chips.BaseRecipientAdapter; -import com.android.ex.chips.RecipientAlternatesAdapter; -import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback; -import com.android.ex.chips.RecipientEntry; -import com.android.messaging.util.Assert; -import com.android.messaging.util.Assert.DoesNotRunOnMainThread; -import com.android.messaging.util.BugleGservices; -import com.android.messaging.util.BugleGservicesKeys; -import com.android.messaging.util.ContactRecipientEntryUtils; -import com.android.messaging.util.ContactUtil; -import com.android.messaging.util.PhoneUtils; - -import java.text.Collator; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -/** - * An extension on the base {@link BaseRecipientAdapter} that uses data layer from Bugle, - * such as the ContactRecipientPhotoManager that uses our own MediaResourceManager, and - * contact lookup that relies on ContactUtil. It provides data source and filtering ability - * for {@link ContactRecipientAutoCompleteView} - */ -public final class ContactRecipientAdapter extends BaseRecipientAdapter { - public ContactRecipientAdapter(final Context context, - final ContactListItemView.HostInterface clivHost) { - this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost); - } - - public ContactRecipientAdapter(final Context context, final int preferredMaxResultCount, - final int queryMode, final ContactListItemView.HostInterface clivHost) { - super(context, preferredMaxResultCount, queryMode); - setPhotoManager(new ContactRecipientPhotoManager(context, clivHost)); - } - - @Override - public boolean forceShowAddress() { - // We should always use the SingleRecipientAddressAdapter - // And never use the RecipientAlternatesAdapter - return true; - } - - @Override - public Filter getFilter() { - return new ContactFilter(); - } - - /** - * A Filter for a RecipientEditTextView that queries Bugle's ContactUtil for auto-complete - * results. - */ - public class ContactFilter extends Filter { - // Used to sort filtered contacts when it has combined results from email and phone. - private final RecipientEntryComparator mComparator = new RecipientEntryComparator(); - - /** - * Returns a cursor containing the filtered results in contacts given the search text, - * and a boolean indicating whether the results are sorted. - * - * The queries are synchronously performed since this is not run on the main thread. - * - * Some locales (e.g. JPN) expect email addresses to be auto-completed for MMS. - * If this is the case, perform two queries on phone number followed by email and - * return the merged results. - */ - @DoesNotRunOnMainThread - private Pair<Cursor, Boolean> getFilteredResultsCursor(final Context context, - final String searchText) { - Assert.isNotMainThread(); - if (BugleGservices.get().getBoolean( - BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS, - BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) { - return Pair.create((Cursor) new MergeCursor(new Cursor[] { - ContactUtil.filterPhones(getContext(), searchText) - .performSynchronousQuery(), - ContactUtil.filterEmails(getContext(), searchText) - .performSynchronousQuery() - }), false /* the merged cursor is not sorted */); - } else { - return Pair.create(ContactUtil.filterDestination(getContext(), searchText) - .performSynchronousQuery(), true); - } - } - - @Override - protected FilterResults performFiltering(final CharSequence constraint) { - Assert.isNotMainThread(); - final FilterResults results = new FilterResults(); - - // No query, return empty results. - if (TextUtils.isEmpty(constraint)) { - clearTempEntries(); - return results; - } - - final String searchText = constraint.toString(); - - // Query for auto-complete results, since performFiltering() is not done on the - // main thread, perform the cursor loader queries directly. - final Pair<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(), - searchText); - final Cursor cursor = filteredResults.first; - final boolean sorted = filteredResults.second; - if (cursor != null) { - try { - final List<RecipientEntry> entries = new ArrayList<RecipientEntry>(); - - // First check if the constraint is a valid SMS destination. If so, add the - // destination as a suggestion item to the drop down. - if (PhoneUtils.isValidSmsMmsDestination(searchText)) { - entries.add(ContactRecipientEntryUtils - .constructSendToDestinationEntry(searchText)); - } - - HashSet<Long> existingContactIds = new HashSet<Long>(); - while (cursor.moveToNext()) { - // Make sure there's only one first-level contact (i.e. contact for which - // we show the avatar picture and name) for every contact id. - final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID); - final boolean isFirstLevel = !existingContactIds.contains(contactId); - if (isFirstLevel) { - existingContactIds.add(contactId); - } - entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor, - isFirstLevel)); - } - - if (!sorted) { - Collections.sort(entries, mComparator); - } - results.values = entries; - results.count = 1; - - } finally { - cursor.close(); - } - } - return results; - } - - @Override - protected void publishResults(final CharSequence constraint, final FilterResults results) { - mCurrentConstraint = constraint; - clearTempEntries(); - - if (results.values != null) { - @SuppressWarnings("unchecked") - final List<RecipientEntry> entries = (List<RecipientEntry>) results.values; - updateEntries(entries); - } else { - updateEntries(Collections.<RecipientEntry>emptyList()); - } - } - - private class RecipientEntryComparator implements Comparator<RecipientEntry> { - private final Collator mCollator; - - public RecipientEntryComparator() { - mCollator = Collator.getInstance(Locale.getDefault()); - mCollator.setStrength(Collator.PRIMARY); - } - - /** - * Compare two RecipientEntry's, first by locale-aware display name comparison, then by - * contact id comparison, finally by first-level-ness comparison. - */ - @Override - public int compare(RecipientEntry lhs, RecipientEntry rhs) { - // Send-to-destinations always appear before everything else. - final boolean sendToLhs = ContactRecipientEntryUtils - .isSendToDestinationContact(lhs); - final boolean sendToRhs = ContactRecipientEntryUtils - .isSendToDestinationContact(lhs); - if (sendToLhs != sendToRhs) { - if (sendToLhs) { - return -1; - } else if (sendToRhs) { - return 1; - } - } - - final int displayNameCompare = mCollator.compare(lhs.getDisplayName(), - rhs.getDisplayName()); - if (displayNameCompare != 0) { - return displayNameCompare; - } - - // Long.compare could accomplish the following three lines, but this is only - // available in API 19+ - final long lhsContactId = lhs.getContactId(); - final long rhsContactId = rhs.getContactId(); - final int contactCompare = lhsContactId < rhsContactId ? -1 : - (lhsContactId == rhsContactId ? 0 : 1); - if (contactCompare != 0) { - return contactCompare; - } - - // These are the same contact. Make sure first-level contacts always - // appear at the front. - if (lhs.isFirstLevel()) { - return -1; - } else if (rhs.isFirstLevel()) { - return 1; - } else { - return 0; - } - } - } - } - - /** - * Called when we need to substitute temporary recipient chips with better alternatives. - * For example, if a list of comma-delimited phone numbers are pasted into the edit box, - * we want to be able to look up in the ContactUtil for exact matches and get contact - * details such as name and photo thumbnail for the contact to display a better chip. - */ - @Override - public void getMatchingRecipients(final ArrayList<String> inAddresses, - final RecipientMatchCallback callback) { - final int addressesSize = Math.min( - RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size()); - final HashSet<String> addresses = new HashSet<String>(); - for (int i = 0; i < addressesSize; i++) { - final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase()); - addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i)); - } - - final Map<String, RecipientEntry> recipientEntries = - new HashMap<String, RecipientEntry>(); - // query for each address - for (final String address : addresses) { - final Cursor cursor = ContactUtil.lookupDestination(getContext(), address) - .performSynchronousQuery(); - if (cursor != null) { - try { - if (cursor.moveToNext()) { - // There may be multiple matches to the same number, always take the - // first match. - // TODO: May need to consider if there's an existing conversation - // that matches this particular contact and prioritize that contact. - final RecipientEntry entry = - ContactUtil.createRecipientEntryForPhoneQuery(cursor, true); - recipientEntries.put(address, entry); - } - - } finally { - cursor.close(); - } - } - } - - // report matches - callback.matchesFound(recipientEntries); - } -} diff --git a/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java b/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java deleted file mode 100644 index c7c2731..0000000 --- a/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Rect; -import android.os.AsyncTask; -import android.support.v7.appcompat.R; -import android.text.Editable; -import android.text.TextPaint; -import android.text.TextWatcher; -import android.text.util.Rfc822Tokenizer; -import android.util.AttributeSet; -import android.view.ContextThemeWrapper; -import android.view.KeyEvent; -import android.view.inputmethod.EditorInfo; -import android.widget.TextView; - -import com.android.ex.chips.RecipientEditTextView; -import com.android.ex.chips.RecipientEntry; -import com.android.ex.chips.recipientchip.DrawableRecipientChip; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.util.ContactRecipientEntryUtils; -import com.android.messaging.util.ContactUtil; -import com.android.messaging.util.PhoneUtils; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -/** - * An extension for {@link RecipientEditTextView} which shows a list of Materialized contact chips. - * It uses Bugle's ContactUtil to perform contact lookup, and is able to return the list of - * recipients in the form of a ParticipantData list. - */ -public class ContactRecipientAutoCompleteView extends RecipientEditTextView { - public interface ContactChipsChangeListener { - void onContactChipsChanged(int oldCount, int newCount); - void onInvalidContactChipsPruned(int prunedCount); - void onEntryComplete(); - } - - private final int mTextHeight; - private ContactChipsChangeListener mChipsChangeListener; - - /** - * Watches changes in contact chips to determine possible state transitions. - */ - private class ContactChipsWatcher implements TextWatcher { - /** - * Tracks the old chips count before text changes. Note that we currently don't compare - * the entire chip sets but just the cheaper-to-do before and after counts, because - * the chips view don't allow for replacing chips. - */ - private int mLastChipsCount = 0; - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, - final int count) { - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, - final int after) { - // We don't take mLastChipsCount from here but from the last afterTextChanged() run. - // The reason is because at this point, any chip spans to be removed is already removed - // from s in the chips text view. - } - - @Override - public void afterTextChanged(final Editable s) { - final int currentChipsCount = s.getSpans(0, s.length(), - DrawableRecipientChip.class).length; - if (currentChipsCount != mLastChipsCount) { - // When a sanitizing task is running, we don't want to notify any chips count - // change, but we do want to track the last chip count. - if (mChipsChangeListener != null && mCurrentSanitizeTask == null) { - mChipsChangeListener.onContactChipsChanged(mLastChipsCount, currentChipsCount); - } - mLastChipsCount = currentChipsCount; - } - } - } - - private static final String TEXT_HEIGHT_SAMPLE = "a"; - - public ContactRecipientAutoCompleteView(final Context context, final AttributeSet attrs) { - super(new ContextThemeWrapper(context, R.style.ColorAccentGrayOverrideStyle), attrs); - - // Get the height of the text, given the currently set font face and size. - final Rect textBounds = new Rect(0, 0, 0, 0); - final TextPaint paint = getPaint(); - paint.getTextBounds(TEXT_HEIGHT_SAMPLE, 0, TEXT_HEIGHT_SAMPLE.length(), textBounds); - mTextHeight = textBounds.height(); - - setTokenizer(new Rfc822Tokenizer()); - addTextChangedListener(new ContactChipsWatcher()); - setOnFocusListShrinkRecipients(false); - - setBackground(context.getResources().getDrawable( - R.drawable.abc_textfield_search_default_mtrl_alpha)); - } - - public void setContactChipsListener(final ContactChipsChangeListener listener) { - mChipsChangeListener = listener; - } - - /** - * A tuple of chips which AsyncContactChipSanitizeTask reports as progress to have the - * chip actually replaced/removed on the UI thread. - */ - private class ChipReplacementTuple { - public final DrawableRecipientChip removedChip; - public final RecipientEntry replacedChipEntry; - - public ChipReplacementTuple(final DrawableRecipientChip removedChip, - final RecipientEntry replacedChipEntry) { - this.removedChip = removedChip; - this.replacedChipEntry = replacedChipEntry; - } - } - - /** - * An AsyncTask that cleans up contact chips on every chips commit (i.e. get or create a new - * conversation with the given chips). - */ - private class AsyncContactChipSanitizeTask extends - AsyncTask<Void, ChipReplacementTuple, Integer> { - - @Override - protected Integer doInBackground(final Void... params) { - final DrawableRecipientChip[] recips = getText() - .getSpans(0, getText().length(), DrawableRecipientChip.class); - int invalidChipsRemoved = 0; - for (final DrawableRecipientChip recipient : recips) { - final RecipientEntry entry = recipient.getEntry(); - if (entry != null) { - if (entry.isValid()) { - if (RecipientEntry.isCreatedRecipient(entry.getContactId()) || - ContactRecipientEntryUtils.isSendToDestinationContact(entry)) { - // This is a generated/send-to contact chip, try to look it up and - // display a chip for the corresponding local contact. - final Cursor lookupResult = ContactUtil.lookupDestination(getContext(), - entry.getDestination()).performSynchronousQuery(); - if (lookupResult != null && lookupResult.moveToNext()) { - // Found a match, remove the generated entry and replace with - // a better local entry. - publishProgress(new ChipReplacementTuple(recipient, - ContactUtil.createRecipientEntryForPhoneQuery( - lookupResult, true))); - } else if (PhoneUtils.isValidSmsMmsDestination( - entry.getDestination())){ - // No match was found, but we have a valid destination so let's at - // least create an entry that shows an avatar. - publishProgress(new ChipReplacementTuple(recipient, - ContactRecipientEntryUtils.constructNumberWithAvatarEntry( - entry.getDestination()))); - } else { - // Not a valid contact. Remove and show an error. - publishProgress(new ChipReplacementTuple(recipient, null)); - invalidChipsRemoved++; - } - } - } else { - publishProgress(new ChipReplacementTuple(recipient, null)); - invalidChipsRemoved++; - } - } - } - return invalidChipsRemoved; - } - - @Override - protected void onProgressUpdate(final ChipReplacementTuple... values) { - for (final ChipReplacementTuple tuple : values) { - if (tuple.removedChip != null) { - final Editable text = getText(); - final int chipStart = text.getSpanStart(tuple.removedChip); - final int chipEnd = text.getSpanEnd(tuple.removedChip); - if (chipStart >= 0 && chipEnd >= 0) { - text.delete(chipStart, chipEnd); - } - - if (tuple.replacedChipEntry != null) { - appendRecipientEntry(tuple.replacedChipEntry); - } - } - } - } - - @Override - protected void onPostExecute(final Integer invalidChipsRemoved) { - mCurrentSanitizeTask = null; - if (invalidChipsRemoved > 0) { - mChipsChangeListener.onInvalidContactChipsPruned(invalidChipsRemoved); - } - } - } - - /** - * We don't use SafeAsyncTask but instead use a single threaded executor to ensure that - * all sanitization tasks are serially executed so as not to interfere with each other. - */ - private static final Executor SANITIZE_EXECUTOR = Executors.newSingleThreadExecutor(); - - private AsyncContactChipSanitizeTask mCurrentSanitizeTask; - - /** - * Whenever the caller wants to start a new conversation with the list of chips we have, - * make sure we asynchronously: - * 1. Remove invalid chips. - * 2. Attempt to resolve unknown contacts to known local contacts. - * 3. Convert still unknown chips to chips with generated avatar. - * - * Note that we don't need to perform this synchronously since we can - * resolve any unknown contacts to local contacts when needed. - */ - private void sanitizeContactChips() { - if (mCurrentSanitizeTask != null && !mCurrentSanitizeTask.isCancelled()) { - mCurrentSanitizeTask.cancel(false); - mCurrentSanitizeTask = null; - } - mCurrentSanitizeTask = new AsyncContactChipSanitizeTask(); - mCurrentSanitizeTask.executeOnExecutor(SANITIZE_EXECUTOR); - } - - /** - * Returns a list of ParticipantData from the entered chips in order to create - * new conversation. - */ - public ArrayList<ParticipantData> getRecipientParticipantDataForConversationCreation() { - final DrawableRecipientChip[] recips = getText() - .getSpans(0, getText().length(), DrawableRecipientChip.class); - final ArrayList<ParticipantData> contacts = - new ArrayList<ParticipantData>(recips.length); - for (final DrawableRecipientChip recipient : recips) { - final RecipientEntry entry = recipient.getEntry(); - if (entry != null && entry.isValid() && entry.getDestination() != null && - PhoneUtils.isValidSmsMmsDestination(entry.getDestination())) { - contacts.add(ParticipantData.getFromRecipientEntry(recipient.getEntry())); - } - } - sanitizeContactChips(); - return contacts; - } - - /**c - * Gets a set of currently selected chips' emails/phone numbers. This will facilitate the - * consumer with determining quickly whether a contact is currently selected. - */ - public Set<String> getSelectedDestinations() { - Set<String> set = new HashSet<String>(); - final DrawableRecipientChip[] recips = getText() - .getSpans(0, getText().length(), DrawableRecipientChip.class); - - for (final DrawableRecipientChip recipient : recips) { - final RecipientEntry entry = recipient.getEntry(); - if (entry != null && entry.isValid() && entry.getDestination() != null) { - set.add(PhoneUtils.getDefault().getCanonicalBySystemLocale( - entry.getDestination())); - } - } - return set; - } - - @Override - public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - mChipsChangeListener.onEntryComplete(); - } - return super.onEditorAction(view, actionId, event); - } -} diff --git a/src/com/android/messaging/ui/contact/ContactRecipientPhotoManager.java b/src/com/android/messaging/ui/contact/ContactRecipientPhotoManager.java deleted file mode 100644 index d69ba64..0000000 --- a/src/com/android/messaging/ui/contact/ContactRecipientPhotoManager.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; -import android.net.Uri; - -import com.android.ex.chips.PhotoManager; -import com.android.ex.chips.RecipientEntry; -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.datamodel.media.AvatarRequestDescriptor; -import com.android.messaging.datamodel.media.BindableMediaRequest; -import com.android.messaging.datamodel.media.ImageResource; -import com.android.messaging.datamodel.media.MediaRequest; -import com.android.messaging.datamodel.media.MediaResourceManager; -import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener; -import com.android.messaging.util.AvatarUriUtil; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.ThreadUtil; - -/** - * An implementation of {@link PhotoManager} that hooks up the chips UI's photos with our own - * {@link MediaResourceManager} for retrieving and caching contact avatars. - */ -public class ContactRecipientPhotoManager implements PhotoManager { - private static final String IMAGE_BYTES_REQUEST_STATIC_BINDING_ID = "imagebytes"; - private final Context mContext; - private final int mIconSize; - private final ContactListItemView.HostInterface mClivHostInterface; - - public ContactRecipientPhotoManager(final Context context, - final ContactListItemView.HostInterface clivHostInterface) { - mContext = context; - mIconSize = context.getResources().getDimensionPixelSize( - R.dimen.compose_message_chip_height) - context.getResources().getDimensionPixelSize( - R.dimen.compose_message_chip_padding) * 2; - mClivHostInterface = clivHostInterface; - } - - /** - * {@inheritDoc} - */ - @Override - public void populatePhotoBytesAsync(final RecipientEntry entry, - final PhotoManagerCallback callback) { - // Post all media resource request to the main thread. - ThreadUtil.getMainThreadHandler().post(new Runnable() { - @Override - public void run() { - final Uri avatarUri = AvatarUriUtil.createAvatarUri( - ParticipantData.getFromRecipientEntry(entry)); - final AvatarRequestDescriptor descriptor = - new AvatarRequestDescriptor(avatarUri, mIconSize, mIconSize); - final BindableMediaRequest<ImageResource> req = descriptor.buildAsyncMediaRequest( - mContext, - new MediaResourceLoadListener<ImageResource>() { - @Override - public void onMediaResourceLoaded(final MediaRequest<ImageResource> request, - final ImageResource resource, final boolean isCached) { - entry.setPhotoBytes(resource.getBytes()); - callback.onPhotoBytesAsynchronouslyPopulated(); - } - - @Override - public void onMediaResourceLoadError(final MediaRequest<ImageResource> request, - final Exception exception) { - LogUtil.e(LogUtil.BUGLE_TAG, "Photo bytes loading failed due to " + - exception + " request key=" + request.getKey()); - - // Fall back to the default avatar image. - callback.onPhotoBytesAsyncLoadFailed(); - }}); - - // Statically bind the request since it's not bound to any specific piece of UI. - req.bind(IMAGE_BYTES_REQUEST_STATIC_BINDING_ID); - - Factory.get().getMediaResourceManager().requestMediaResourceAsync(req); - } - }); - } -} diff --git a/src/com/android/messaging/ui/contact/ContactSectionIndexer.java b/src/com/android/messaging/ui/contact/ContactSectionIndexer.java deleted file mode 100644 index 1d5abf3..0000000 --- a/src/com/android/messaging/ui/contact/ContactSectionIndexer.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.contact; - -import android.database.Cursor; -import android.os.Bundle; -import android.provider.ContactsContract.Contacts; -import android.text.TextUtils; -import android.widget.SectionIndexer; - -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContactUtil; -import com.android.messaging.util.LogUtil; - -import java.util.ArrayList; - -/** - * Indexes contact alphabetical sections so we can report to the fast scrolling list view - * where we are in the list when the user scrolls through the contact list, allowing us to show - * alphabetical indicators for the fast scroller as well as list section headers. - */ -public class ContactSectionIndexer implements SectionIndexer { - private String[] mSections; - private ArrayList<Integer> mSectionStartingPositions; - private static final String BLANK_HEADER_STRING = " "; - - public ContactSectionIndexer(final Cursor contactsCursor) { - buildIndexer(contactsCursor); - } - - @Override - public Object[] getSections() { - return mSections; - } - - @Override - public int getPositionForSection(final int sectionIndex) { - if (mSectionStartingPositions.isEmpty()) { - return 0; - } - // Clamp to the bounds of the section position array per Android API doc. - return mSectionStartingPositions.get( - Math.max(Math.min(sectionIndex, mSectionStartingPositions.size() - 1), 0)); - } - - @Override - public int getSectionForPosition(final int position) { - if (mSectionStartingPositions.isEmpty()) { - return 0; - } - - // Perform a binary search on the starting positions of the sections to the find the - // section for the position. - int left = 0; - int right = mSectionStartingPositions.size() - 1; - - // According to getSectionForPosition()'s doc, we should always clamp the value when the - // position is out of bound. - if (position <= mSectionStartingPositions.get(left)) { - return left; - } else if (position >= mSectionStartingPositions.get(right)) { - return right; - } - - while (left <= right) { - final int mid = (left + right) / 2; - final int startingPos = mSectionStartingPositions.get(mid); - final int nextStartingPos = mSectionStartingPositions.get(mid + 1); - if (position >= startingPos && position < nextStartingPos) { - return mid; - } else if (position < startingPos) { - right = mid - 1; - } else if (position >= nextStartingPos) { - left = mid + 1; - } - } - Assert.fail("Invalid section indexer state: couldn't find section for pos " + position); - return -1; - } - - private boolean buildIndexerFromCursorExtras(final Cursor cursor) { - if (cursor == null) { - return false; - } - final Bundle cursorExtras = cursor.getExtras(); - if (cursorExtras == null) { - return false; - } - final String[] sections = cursorExtras.getStringArray( - Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); - final int[] counts = cursorExtras.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); - if (sections == null || counts == null) { - return false; - } - - if (sections.length != counts.length) { - return false; - } - - this.mSections = sections; - mSectionStartingPositions = new ArrayList<Integer>(counts.length); - int position = 0; - for (int i = 0; i < counts.length; i++) { - if (TextUtils.isEmpty(mSections[i])) { - mSections[i] = BLANK_HEADER_STRING; - } else if (!mSections[i].equals(BLANK_HEADER_STRING)) { - mSections[i] = mSections[i].trim(); - } - - mSectionStartingPositions.add(position); - position += counts[i]; - } - return true; - } - - private void buildIndexerFromDisplayNames(final Cursor cursor) { - // Loop through the contact cursor and get the starting position for each first character. - // The result is stored into two arrays, one for the section header (i.e. the first - // character), and one for the starting position, which is guaranteed to be sorted in - // ascending order. - final ArrayList<String> sections = new ArrayList<String>(); - mSectionStartingPositions = new ArrayList<Integer>(); - if (cursor != null) { - cursor.moveToPosition(-1); - int currentPosition = 0; - while (cursor.moveToNext()) { - // The sort key is typically the contact's display name, so for example, a contact - // named "Bob" will go into section "B". The Contacts provider generally uses a - // a slightly more sophisticated heuristic, but as a fallback this is good enough. - final String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY); - final String section = TextUtils.isEmpty(sortKey) ? BLANK_HEADER_STRING : - sortKey.substring(0, 1).toUpperCase(); - - final int lastIndex = sections.size() - 1; - final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null; - if (!TextUtils.equals(currentSection, section)) { - sections.add(section); - mSectionStartingPositions.add(currentPosition); - } - currentPosition++; - } - } - mSections = new String[sections.size()]; - sections.toArray(mSections); - } - - private void buildIndexer(final Cursor cursor) { - // First check if we get indexer label extras from the contact provider; if not, fall back - // to building from display names. - if (!buildIndexerFromCursorExtras(cursor)) { - LogUtil.w(LogUtil.BUGLE_TAG, "contact provider didn't provide contact label " + - "information, fall back to using display name!"); - buildIndexerFromDisplayNames(cursor); - } - } -} diff --git a/src/com/android/messaging/ui/contact/FrequentContactsListViewHolder.java b/src/com/android/messaging/ui/contact/FrequentContactsListViewHolder.java deleted file mode 100644 index 1f3c795..0000000 --- a/src/com/android/messaging/ui/contact/FrequentContactsListViewHolder.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.contact; - -import android.content.Context; - -import com.android.messaging.R; -import com.android.messaging.ui.CustomHeaderPagerListViewHolder; -import com.android.messaging.ui.contact.ContactListItemView.HostInterface; - -/** - * Holds the frequent contacts view for the contact picker's view pager. - */ -public class FrequentContactsListViewHolder extends CustomHeaderPagerListViewHolder { - public FrequentContactsListViewHolder(final Context context, - final HostInterface clivHostInterface) { - super(context, new ContactListAdapter(context, null, clivHostInterface, - false /* needAlphabetHeader */)); - } - - @Override - protected int getLayoutResId() { - return R.layout.frequent_contacts_list_view; - } - - @Override - protected int getPageTitleResId() { - return R.string.contact_picker_frequents_tab_title; - } - - @Override - protected int getEmptyViewResId() { - return R.id.empty_view; - } - - @Override - protected int getListViewResId() { - return R.id.frequent_contacts_list; - } - - @Override - protected int getEmptyViewTitleResId() { - return R.string.contact_list_empty_text; - } - - @Override - protected int getEmptyViewImageResId() { - return R.drawable.ic_oobe_freq_list; - } -} diff --git a/src/com/android/messaging/ui/conversation/ComposeMessageView.java b/src/com/android/messaging/ui/conversation/ComposeMessageView.java deleted file mode 100644 index 17f8f74..0000000 --- a/src/com/android/messaging/ui/conversation/ComposeMessageView.java +++ /dev/null @@ -1,962 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.text.Editable; -import android.text.Html; -import android.text.InputFilter; -import android.text.InputFilter.LengthFilter; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.view.ContextThemeWrapper; -import android.view.KeyEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.inputmethod.EditorInfo; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.messaging.Factory; -import com.android.messaging.R; -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.ConversationData; -import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener; -import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener; -import com.android.messaging.datamodel.data.DraftMessageData; -import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftForSendTask; -import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftTaskCallback; -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.datamodel.data.PendingAttachmentData; -import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; -import com.android.messaging.sms.MmsConfig; -import com.android.messaging.ui.AttachmentPreview; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.PlainTextEditText; -import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.Assert; -import com.android.messaging.util.AvatarUriUtil; -import com.android.messaging.util.BuglePrefs; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.MediaUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; - -import java.util.Collection; -import java.util.List; - -/** - * This view contains the UI required to generate and send messages. - */ -public class ComposeMessageView extends LinearLayout - implements TextView.OnEditorActionListener, DraftMessageDataListener, TextWatcher, - ConversationInputSink { - - public interface IComposeMessageViewHost extends - DraftMessageData.DraftMessageSubscriptionDataProvider { - void sendMessage(MessageData message); - void onComposeEditTextFocused(); - void onAttachmentsCleared(); - void onAttachmentsChanged(final boolean haveAttachments); - void displayPhoto(Uri photoUri, Rect imageBounds, boolean isDraft); - void promptForSelfPhoneNumber(); - boolean isReadyForAction(); - void warnOfMissingActionConditions(final boolean sending, - final Runnable commandToRunAfterActionConditionResolved); - void warnOfExceedingMessageLimit(final boolean showAttachmentChooser, - boolean tooManyVideos); - void notifyOfAttachmentLoadFailed(); - void showAttachmentChooser(); - boolean shouldShowSubjectEditor(); - boolean shouldHideAttachmentsWhenSimSelectorShown(); - Uri getSelfSendButtonIconUri(); - int overrideCounterColor(); - int getAttachmentsClearedFlags(); - } - - public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10; - - // There is no draft and there is no need for the SIM selector - private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1; - // There is no draft but we need to show the SIM selector - private static final int SEND_WIDGET_MODE_SIM_SELECTOR = 2; - // There is a draft - private static final int SEND_WIDGET_MODE_SEND_BUTTON = 3; - - private PlainTextEditText mComposeEditText; - private PlainTextEditText mComposeSubjectText; - private TextView mCharCounter; - private TextView mMmsIndicator; - private SimIconView mSelfSendIcon; - private ImageButton mSendButton; - private View mSubjectView; - private ImageButton mDeleteSubjectButton; - private AttachmentPreview mAttachmentPreview; - private ImageButton mAttachMediaButton; - - private final Binding<DraftMessageData> mBinding; - private IComposeMessageViewHost mHost; - private final Context mOriginalContext; - private int mSendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR; - - // Shared data model object binding from the conversation. - private ImmutableBindingRef<ConversationData> mConversationDataModel; - - // Centrally manages all the mutual exclusive UI components accepting user input, i.e. - // media picker, IME keyboard and SIM selector. - private ConversationInputManager mInputManager; - - private final ConversationDataListener mDataListener = new SimpleConversationDataListener() { - @Override - public void onConversationMetadataUpdated(ConversationData data) { - mConversationDataModel.ensureBound(data); - updateVisualsOnDraftChanged(); - } - - @Override - public void onConversationParticipantDataLoaded(ConversationData data) { - mConversationDataModel.ensureBound(data); - updateVisualsOnDraftChanged(); - } - - @Override - public void onSubscriptionListDataLoaded(ConversationData data) { - mConversationDataModel.ensureBound(data); - updateOnSelfSubscriptionChange(); - updateVisualsOnDraftChanged(); - } - }; - - public ComposeMessageView(final Context context, final AttributeSet attrs) { - super(new ContextThemeWrapper(context, R.style.ColorAccentBlueOverrideStyle), attrs); - mOriginalContext = context; - mBinding = BindingBase.createBinding(this); - } - - /** - * Host calls this to bind view to DraftMessageData object - */ - public void bind(final DraftMessageData data, final IComposeMessageViewHost host) { - mHost = host; - mBinding.bind(data); - data.addListener(this); - data.setSubscriptionDataProvider(host); - - final int counterColor = mHost.overrideCounterColor(); - if (counterColor != -1) { - mCharCounter.setTextColor(counterColor); - } - } - - /** - * Host calls this to unbind view - */ - public void unbind() { - mBinding.unbind(); - mHost = null; - mInputManager.onDetach(); - } - - @Override - protected void onFinishInflate() { - mComposeEditText = (PlainTextEditText) findViewById( - R.id.compose_message_text); - mComposeEditText.setOnEditorActionListener(this); - mComposeEditText.addTextChangedListener(this); - mComposeEditText.setOnFocusChangeListener(new OnFocusChangeListener() { - @Override - public void onFocusChange(final View v, final boolean hasFocus) { - if (v == mComposeEditText && hasFocus) { - mHost.onComposeEditTextFocused(); - } - } - }); - mComposeEditText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View arg0) { - if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) { - hideSimSelector(); - } - } - }); - - // onFinishInflate() is called before self is loaded from db. We set the default text - // limit here, and apply the real limit later in updateOnSelfSubscriptionChange(). - mComposeEditText.setFilters(new InputFilter[] { - new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID) - .getMaxTextLimit()) }); - - mSelfSendIcon = (SimIconView) findViewById(R.id.self_send_icon); - mSelfSendIcon.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - boolean shown = mInputManager.toggleSimSelector(true /* animate */, - getSelfSubscriptionListEntry()); - hideAttachmentsWhenShowingSims(shown); - } - }); - mSelfSendIcon.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(final View v) { - if (mHost.shouldShowSubjectEditor()) { - showSubjectEditor(); - } else { - boolean shown = mInputManager.toggleSimSelector(true /* animate */, - getSelfSubscriptionListEntry()); - hideAttachmentsWhenShowingSims(shown); - } - return true; - } - }); - - mComposeSubjectText = (PlainTextEditText) findViewById( - R.id.compose_subject_text); - // We need the listener to change the avatar to the send button when the user starts - // typing a subject without a message. - mComposeSubjectText.addTextChangedListener(this); - // onFinishInflate() is called before self is loaded from db. We set the default text - // limit here, and apply the real limit later in updateOnSelfSubscriptionChange(). - mComposeSubjectText.setFilters(new InputFilter[] { - new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID) - .getMaxSubjectLength())}); - - mDeleteSubjectButton = (ImageButton) findViewById(R.id.delete_subject_button); - mDeleteSubjectButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View clickView) { - hideSubjectEditor(); - mComposeSubjectText.setText(null); - mBinding.getData().setMessageSubject(null); - } - }); - - mSubjectView = findViewById(R.id.subject_view); - - mSendButton = (ImageButton) findViewById(R.id.send_message_button); - mSendButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View clickView) { - sendMessageInternal(true /* checkMessageSize */); - } - }); - mSendButton.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(final View arg0) { - boolean shown = mInputManager.toggleSimSelector(true /* animate */, - getSelfSubscriptionListEntry()); - hideAttachmentsWhenShowingSims(shown); - if (mHost.shouldShowSubjectEditor()) { - showSubjectEditor(); - } - return true; - } - }); - mSendButton.setAccessibilityDelegate(new AccessibilityDelegate() { - @Override - public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(host, event); - // When the send button is long clicked, we want TalkBack to announce the real - // action (select SIM or edit subject), as opposed to "long press send button." - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) { - event.getText().clear(); - event.getText().add(getResources() - .getText(shouldShowSimSelector(mConversationDataModel.getData()) ? - R.string.send_button_long_click_description_with_sim_selector : - R.string.send_button_long_click_description_no_sim_selector)); - // Make this an announcement so TalkBack will read our custom message. - event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); - } - } - }); - - mAttachMediaButton = - (ImageButton) findViewById(R.id.attach_media_button); - mAttachMediaButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View clickView) { - // Showing the media picker is treated as starting to compose the message. - mInputManager.showHideMediaPicker(true /* show */, true /* animate */); - } - }); - - mAttachmentPreview = (AttachmentPreview) findViewById(R.id.attachment_draft_view); - mAttachmentPreview.setComposeMessageView(this); - - mCharCounter = (TextView) findViewById(R.id.char_counter); - mMmsIndicator = (TextView) findViewById(R.id.mms_indicator); - } - - private void hideAttachmentsWhenShowingSims(final boolean simPickerVisible) { - if (!mHost.shouldHideAttachmentsWhenSimSelectorShown()) { - return; - } - final boolean haveAttachments = mBinding.getData().hasAttachments(); - if (simPickerVisible && haveAttachments) { - mHost.onAttachmentsChanged(false); - mAttachmentPreview.hideAttachmentPreview(); - } else { - mHost.onAttachmentsChanged(haveAttachments); - mAttachmentPreview.onAttachmentsChanged(mBinding.getData()); - } - } - - public void setInputManager(final ConversationInputManager inputManager) { - mInputManager = inputManager; - } - - public void setConversationDataModel(final ImmutableBindingRef<ConversationData> refDataModel) { - mConversationDataModel = refDataModel; - mConversationDataModel.getData().addConversationDataListener(mDataListener); - } - - ImmutableBindingRef<DraftMessageData> getDraftDataModel() { - return BindingBase.createBindingReference(mBinding); - } - - // returns true if it actually shows the subject editor and false if already showing - private boolean showSubjectEditor() { - // show the subject editor - if (mSubjectView.getVisibility() == View.GONE) { - mSubjectView.setVisibility(View.VISIBLE); - mSubjectView.requestFocus(); - return true; - } - return false; - } - - private void hideSubjectEditor() { - mSubjectView.setVisibility(View.GONE); - mComposeEditText.requestFocus(); - } - - /** - * {@inheritDoc} from TextView.OnEditorActionListener - */ - @Override // TextView.OnEditorActionListener.onEditorAction - public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_SEND) { - sendMessageInternal(true /* checkMessageSize */); - return true; - } - return false; - } - - private void sendMessageInternal(final boolean checkMessageSize) { - LogUtil.i(LogUtil.BUGLE_TAG, "UI initiated message sending in conversation " + - mBinding.getData().getConversationId()); - if (mBinding.getData().isCheckingDraft()) { - // Don't send message if we are currently checking draft for sending. - LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: still checking draft"); - return; - } - // Check the host for pre-conditions about any action. - if (mHost.isReadyForAction()) { - mInputManager.showHideSimSelector(false /* show */, true /* animate */); - final String messageToSend = mComposeEditText.getText().toString(); - mBinding.getData().setMessageText(messageToSend); - final String subject = mComposeSubjectText.getText().toString(); - mBinding.getData().setMessageSubject(subject); - // Asynchronously check the draft against various requirements before sending. - mBinding.getData().checkDraftForAction(checkMessageSize, - mHost.getConversationSelfSubId(), new CheckDraftTaskCallback() { - @Override - public void onDraftChecked(DraftMessageData data, int result) { - mBinding.ensureBound(data); - switch (result) { - case CheckDraftForSendTask.RESULT_PASSED: - // Continue sending after check succeeded. - final MessageData message = mBinding.getData() - .prepareMessageForSending(mBinding); - if (message != null && message.hasContent()) { - playSentSound(); - mHost.sendMessage(message); - hideSubjectEditor(); - if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { - AccessibilityUtil.announceForAccessibilityCompat( - ComposeMessageView.this, null, - R.string.sending_message); - } - } - break; - - case CheckDraftForSendTask.RESULT_HAS_PENDING_ATTACHMENTS: - // Cannot send while there's still attachment(s) being loaded. - UiUtils.showToastAtBottom( - R.string.cant_send_message_while_loading_attachments); - break; - - case CheckDraftForSendTask.RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS: - mHost.promptForSelfPhoneNumber(); - break; - - case CheckDraftForSendTask.RESULT_MESSAGE_OVER_LIMIT: - Assert.isTrue(checkMessageSize); - mHost.warnOfExceedingMessageLimit( - true /*sending*/, false /* tooManyVideos */); - break; - - case CheckDraftForSendTask.RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED: - Assert.isTrue(checkMessageSize); - mHost.warnOfExceedingMessageLimit( - true /*sending*/, true /* tooManyVideos */); - break; - - case CheckDraftForSendTask.RESULT_SIM_NOT_READY: - // Cannot send if there is no active subscription - UiUtils.showToastAtBottom( - R.string.cant_send_message_without_active_subscription); - break; - - default: - break; - } - } - }, mBinding); - } else { - mHost.warnOfMissingActionConditions(true /*sending*/, - new Runnable() { - @Override - public void run() { - sendMessageInternal(checkMessageSize); - } - - }); - } - } - - public static void playSentSound() { - // Check if this setting is enabled before playing - final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); - final Context context = Factory.get().getApplicationContext(); - final String prefKey = context.getString(R.string.send_sound_pref_key); - final boolean defaultValue = context.getResources().getBoolean( - R.bool.send_sound_pref_default); - if (!prefs.getBoolean(prefKey, defaultValue)) { - return; - } - MediaUtil.get().playSound(context, R.raw.message_sent, null /* completionListener */); - } - - /** - * {@inheritDoc} from DraftMessageDataListener - */ - @Override // From DraftMessageDataListener - public void onDraftChanged(final DraftMessageData data, final int changeFlags) { - // As this is called asynchronously when message read check bound before updating text - mBinding.ensureBound(data); - - // We have to cache the values of the DraftMessageData because when we set - // mComposeEditText, its onTextChanged calls updateVisualsOnDraftChanged, - // which immediately reloads the text from the subject and message fields and replaces - // what's in the DraftMessageData. - - final String subject = data.getMessageSubject(); - final String message = data.getMessageText(); - - if ((changeFlags & DraftMessageData.MESSAGE_SUBJECT_CHANGED) == - DraftMessageData.MESSAGE_SUBJECT_CHANGED) { - mComposeSubjectText.setText(subject); - - // Set the cursor selection to the end since setText resets it to the start - mComposeSubjectText.setSelection(mComposeSubjectText.getText().length()); - } - - if ((changeFlags & DraftMessageData.MESSAGE_TEXT_CHANGED) == - DraftMessageData.MESSAGE_TEXT_CHANGED) { - mComposeEditText.setText(message); - - // Set the cursor selection to the end since setText resets it to the start - mComposeEditText.setSelection(mComposeEditText.getText().length()); - } - - if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) == - DraftMessageData.ATTACHMENTS_CHANGED) { - final boolean haveAttachments = mAttachmentPreview.onAttachmentsChanged(data); - mHost.onAttachmentsChanged(haveAttachments); - } - - if ((changeFlags & DraftMessageData.SELF_CHANGED) == DraftMessageData.SELF_CHANGED) { - updateOnSelfSubscriptionChange(); - } - updateVisualsOnDraftChanged(); - } - - @Override // From DraftMessageDataListener - public void onDraftAttachmentLimitReached(final DraftMessageData data) { - mBinding.ensureBound(data); - mHost.warnOfExceedingMessageLimit(false /* sending */, false /* tooManyVideos */); - } - - private void updateOnSelfSubscriptionChange() { - // Refresh the length filters according to the selected self's MmsConfig. - mComposeEditText.setFilters(new InputFilter[] { - new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId()) - .getMaxTextLimit()) }); - mComposeSubjectText.setFilters(new InputFilter[] { - new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId()) - .getMaxSubjectLength())}); - } - - @Override - public void onMediaItemsSelected(final Collection<MessagePartData> items) { - mBinding.getData().addAttachments(items); - announceMediaItemState(true /*isSelected*/); - } - - @Override - public void onMediaItemsUnselected(final MessagePartData item) { - mBinding.getData().removeAttachment(item); - announceMediaItemState(false /*isSelected*/); - } - - @Override - public void onPendingAttachmentAdded(final PendingAttachmentData pendingItem) { - mBinding.getData().addPendingAttachment(pendingItem, mBinding); - resumeComposeMessage(); - } - - private void announceMediaItemState(final boolean isSelected) { - final Resources res = getContext().getResources(); - final String announcement = isSelected ? res.getString( - R.string.mediapicker_gallery_item_selected_content_description) : - res.getString(R.string.mediapicker_gallery_item_unselected_content_description); - AccessibilityUtil.announceForAccessibilityCompat( - this, null, announcement); - } - - private void announceAttachmentState() { - if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { - int attachmentCount = mBinding.getData().getReadOnlyAttachments().size() - + mBinding.getData().getReadOnlyPendingAttachments().size(); - final String announcement = getContext().getResources().getQuantityString( - R.plurals.attachment_changed_accessibility_announcement, - attachmentCount, attachmentCount); - AccessibilityUtil.announceForAccessibilityCompat( - this, null, announcement); - } - } - - @Override - public void resumeComposeMessage() { - mComposeEditText.requestFocus(); - mInputManager.showHideImeKeyboard(true, true); - announceAttachmentState(); - } - - public void clearAttachments() { - mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags()); - mHost.onAttachmentsCleared(); - } - - public void requestDraftMessage(boolean clearLocalDraft) { - mBinding.getData().loadFromStorage(mBinding, null, clearLocalDraft); - } - - public void setDraftMessage(final MessageData message) { - mBinding.getData().loadFromStorage(mBinding, message, false); - } - - public void writeDraftMessage() { - final String messageText = mComposeEditText.getText().toString(); - mBinding.getData().setMessageText(messageText); - - final String subject = mComposeSubjectText.getText().toString(); - mBinding.getData().setMessageSubject(subject); - - mBinding.getData().saveToStorage(mBinding); - } - - private void updateConversationSelfId(final String selfId, final boolean notify) { - mBinding.getData().setSelfId(selfId, notify); - } - - private Uri getSelfSendButtonIconUri() { - final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri(); - if (overridenSelfUri != null) { - return overridenSelfUri; - } - final SubscriptionListEntry subscriptionListEntry = getSelfSubscriptionListEntry(); - - if (subscriptionListEntry != null) { - return subscriptionListEntry.selectedIconUri; - } - - // Fall back to default self-avatar in the base case. - final ParticipantData self = mConversationDataModel.getData().getDefaultSelfParticipant(); - return self == null ? null : AvatarUriUtil.createAvatarUri(self); - } - - private SubscriptionListEntry getSelfSubscriptionListEntry() { - return mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant( - mBinding.getData().getSelfId(), false /* excludeDefault */); - } - - private boolean isDataLoadedForMessageSend() { - // Check data loading prerequisites for sending a message. - return mConversationDataModel != null && mConversationDataModel.isBound() && - mConversationDataModel.getData().getParticipantsLoaded(); - } - - private void updateVisualsOnDraftChanged() { - final String messageText = mComposeEditText.getText().toString(); - final DraftMessageData draftMessageData = mBinding.getData(); - draftMessageData.setMessageText(messageText); - - final String subject = mComposeSubjectText.getText().toString(); - draftMessageData.setMessageSubject(subject); - if (!TextUtils.isEmpty(subject)) { - mSubjectView.setVisibility(View.VISIBLE); - } - - final boolean hasMessageText = (TextUtils.getTrimmedLength(messageText) > 0); - final boolean hasSubject = (TextUtils.getTrimmedLength(subject) > 0); - final boolean hasWorkingDraft = hasMessageText || hasSubject || - mBinding.getData().hasAttachments(); - - // Update the SMS text counter. - final int messageCount = draftMessageData.getNumMessagesToBeSent(); - final int codePointsRemaining = draftMessageData.getCodePointsRemainingInCurrentMessage(); - // Show the counter only if: - // - We are not in MMS mode - // - We are going to send more than one message OR we are getting close - boolean showCounter = false; - if (!draftMessageData.getIsMms() && (messageCount > 1 || - codePointsRemaining <= CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN)) { - showCounter = true; - } - - if (showCounter) { - // Update the remaining characters and number of messages required. - final String counterText = messageCount > 1 ? codePointsRemaining + " / " + - messageCount : String.valueOf(codePointsRemaining); - mCharCounter.setText(counterText); - mCharCounter.setVisibility(View.VISIBLE); - } else { - mCharCounter.setVisibility(View.INVISIBLE); - } - - // Update the send message button. Self icon uri might be null if self participant data - // and/or conversation metadata hasn't been loaded by the host. - final Uri selfSendButtonUri = getSelfSendButtonIconUri(); - int sendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR; - if (selfSendButtonUri != null) { - if (hasWorkingDraft && isDataLoadedForMessageSend()) { - UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null); - if (isOverriddenAvatarAGroup()) { - // If the host has overriden the avatar to show a group avatar where the - // send button sits, we have to hide the group avatar because it can be larger - // than the send button and pieces of the avatar will stick out from behind - // the send button. - UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null); - } - mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE); - sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON; - } else { - mSelfSendIcon.setImageResourceUri(selfSendButtonUri); - if (isOverriddenAvatarAGroup()) { - UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, VISIBLE, null); - } - UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null); - mMmsIndicator.setVisibility(INVISIBLE); - if (shouldShowSimSelector(mConversationDataModel.getData())) { - sendWidgetMode = SEND_WIDGET_MODE_SIM_SELECTOR; - } - } - } else { - mSelfSendIcon.setImageResourceUri(null); - } - - if (mSendWidgetMode != sendWidgetMode || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR) { - setSendButtonAccessibility(sendWidgetMode); - mSendWidgetMode = sendWidgetMode; - } - - // Update the text hint on the message box depending on the attachment type. - final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments(); - final int attachmentCount = attachments.size(); - if (attachmentCount == 0) { - final SubscriptionListEntry subscriptionListEntry = - mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant( - mBinding.getData().getSelfId(), false /* excludeDefault */); - if (subscriptionListEntry == null) { - mComposeEditText.setHint(R.string.compose_message_view_hint_text); - } else { - mComposeEditText.setHint(Html.fromHtml(getResources().getString( - R.string.compose_message_view_hint_text_multi_sim, - subscriptionListEntry.displayName))); - } - } else { - int type = -1; - for (final MessagePartData attachment : attachments) { - int newType; - if (attachment.isImage()) { - newType = ContentType.TYPE_IMAGE; - } else if (attachment.isAudio()) { - newType = ContentType.TYPE_AUDIO; - } else if (attachment.isVideo()) { - newType = ContentType.TYPE_VIDEO; - } else if (attachment.isVCard()) { - newType = ContentType.TYPE_VCARD; - } else { - newType = ContentType.TYPE_OTHER; - } - - if (type == -1) { - type = newType; - } else if (type != newType || type == ContentType.TYPE_OTHER) { - type = ContentType.TYPE_OTHER; - break; - } - } - - switch (type) { - case ContentType.TYPE_IMAGE: - mComposeEditText.setHint(getResources().getQuantityString( - R.plurals.compose_message_view_hint_text_photo, attachmentCount)); - break; - - case ContentType.TYPE_AUDIO: - mComposeEditText.setHint(getResources().getQuantityString( - R.plurals.compose_message_view_hint_text_audio, attachmentCount)); - break; - - case ContentType.TYPE_VIDEO: - mComposeEditText.setHint(getResources().getQuantityString( - R.plurals.compose_message_view_hint_text_video, attachmentCount)); - break; - - case ContentType.TYPE_VCARD: - mComposeEditText.setHint(getResources().getQuantityString( - R.plurals.compose_message_view_hint_text_vcard, attachmentCount)); - break; - - case ContentType.TYPE_OTHER: - mComposeEditText.setHint(getResources().getQuantityString( - R.plurals.compose_message_view_hint_text_attachments, attachmentCount)); - break; - - default: - Assert.fail("Unsupported attachment type!"); - break; - } - } - } - - private void setSendButtonAccessibility(final int sendWidgetMode) { - switch (sendWidgetMode) { - case SEND_WIDGET_MODE_SELF_AVATAR: - // No send button and no SIM selector; the self send button is no longer - // important for accessibility. - mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - mSelfSendIcon.setContentDescription(null); - mSendButton.setVisibility(View.GONE); - setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SELF_AVATAR); - break; - - case SEND_WIDGET_MODE_SIM_SELECTOR: - mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - mSelfSendIcon.setContentDescription(getSimContentDescription()); - setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SIM_SELECTOR); - break; - - case SEND_WIDGET_MODE_SEND_BUTTON: - mMmsIndicator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - mMmsIndicator.setContentDescription(null); - setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SEND_BUTTON); - break; - } - } - - private String getSimContentDescription() { - final SubscriptionListEntry sub = getSelfSubscriptionListEntry(); - if (sub != null) { - return getResources().getString( - R.string.sim_selector_button_content_description_with_selection, - sub.displayName); - } else { - return getResources().getString( - R.string.sim_selector_button_content_description); - } - } - - // Set accessibility traversal order of the components in the send widget. - private void setSendWidgetAccessibilityTraversalOrder(final int mode) { - if (OsUtil.isAtLeastL_MR1()) { - mAttachMediaButton.setAccessibilityTraversalBefore(R.id.compose_message_text); - switch (mode) { - case SEND_WIDGET_MODE_SIM_SELECTOR: - mComposeEditText.setAccessibilityTraversalBefore(R.id.self_send_icon); - break; - case SEND_WIDGET_MODE_SEND_BUTTON: - mComposeEditText.setAccessibilityTraversalBefore(R.id.send_message_button); - break; - default: - break; - } - } - } - - @Override - public void afterTextChanged(final Editable editable) { - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, - final int after) { - if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) { - hideSimSelector(); - } - } - - private void hideSimSelector() { - if (mInputManager.showHideSimSelector(false /* show */, true /* animate */)) { - // Now that the sim selector has been hidden, reshow the attachments if they - // have been hidden. - hideAttachmentsWhenShowingSims(false /*simPickerVisible*/); - } - } - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, - final int count) { - final BugleActionBarActivity activity = (mOriginalContext instanceof BugleActionBarActivity) - ? (BugleActionBarActivity) mOriginalContext : null; - if (activity != null && activity.getIsDestroyed()) { - LogUtil.v(LogUtil.BUGLE_TAG, "got onTextChanged after onDestroy"); - - // if we get onTextChanged after the activity is destroyed then, ah, wtf - // b/18176615 - // This appears to have occurred as the result of orientation change. - return; - } - - mBinding.ensureBound(); - updateVisualsOnDraftChanged(); - } - - @Override - public PlainTextEditText getComposeEditText() { - return mComposeEditText; - } - - public void displayPhoto(final Uri photoUri, final Rect imageBounds) { - mHost.displayPhoto(photoUri, imageBounds, true /* isDraft */); - } - - public void updateConversationSelfIdOnExternalChange(final String selfId) { - updateConversationSelfId(selfId, true /* notify */); - } - - /** - * The selfId of the conversation. As soon as the DraftMessageData successfully loads (i.e. - * getSelfId() is non-null), the selfId in DraftMessageData is treated as the sole source - * of truth for conversation self id since it reflects any pending self id change the user - * makes in the UI. - */ - public String getConversationSelfId() { - return mBinding.getData().getSelfId(); - } - - public void selectSim(SubscriptionListEntry subscriptionData) { - final String oldSelfId = getConversationSelfId(); - final String newSelfId = subscriptionData.selfParticipantId; - Assert.notNull(newSelfId); - // Don't attempt to change self if self hasn't been loaded, or if self hasn't changed. - if (oldSelfId == null || TextUtils.equals(oldSelfId, newSelfId)) { - return; - } - updateConversationSelfId(newSelfId, true /* notify */); - } - - public void hideAllComposeInputs(final boolean animate) { - mInputManager.hideAllInputs(animate); - } - - public void saveInputState(final Bundle outState) { - mInputManager.onSaveInputState(outState); - } - - public void resetMediaPickerState() { - mInputManager.resetMediaPickerState(); - } - - public boolean onBackPressed() { - return mInputManager.onBackPressed(); - } - - public boolean onNavigationUpPressed() { - return mInputManager.onNavigationUpPressed(); - } - - public boolean updateActionBar(final ActionBar actionBar) { - return mInputManager != null ? mInputManager.updateActionBar(actionBar) : false; - } - - public static boolean shouldShowSimSelector(final ConversationData convData) { - return OsUtil.isAtLeastL_MR1() && - convData.getSelfParticipantsCountExcludingDefault(true /* activeOnly */) > 1; - } - - public void sendMessageIgnoreMessageSizeLimit() { - sendMessageInternal(false /* checkMessageSize */); - } - - public void onAttachmentPreviewLongClicked() { - mHost.showAttachmentChooser(); - } - - @Override - public void onDraftAttachmentLoadFailed() { - mHost.notifyOfAttachmentLoadFailed(); - } - - private boolean isOverriddenAvatarAGroup() { - final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri(); - if (overridenSelfUri == null) { - return false; - } - return AvatarUriUtil.TYPE_GROUP_URI.equals(AvatarUriUtil.getAvatarType(overridenSelfUri)); - } - - @Override - public void setAccessibility(boolean enabled) { - if (enabled) { - mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - setSendButtonAccessibility(mSendWidgetMode); - } else { - mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - } - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationActivity.java b/src/com/android/messaging/ui/conversation/ConversationActivity.java deleted file mode 100644 index 66310ea..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationActivity.java +++ /dev/null @@ -1,379 +0,0 @@ -/* - * 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.conversation; - -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.Intent; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.text.TextUtils; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.contact.ContactPickerFragment; -import com.android.messaging.ui.contact.ContactPickerFragment.ContactPickerFragmentHost; -import com.android.messaging.ui.conversation.ConversationActivityUiState.ConversationActivityUiStateHost; -import com.android.messaging.ui.conversation.ConversationFragment.ConversationFragmentHost; -import com.android.messaging.ui.conversationlist.ConversationListActivity; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; - -public class ConversationActivity extends BugleActionBarActivity - implements ContactPickerFragmentHost, ConversationFragmentHost, - ConversationActivityUiStateHost { - public static final int FINISH_RESULT_CODE = 1; - private static final String SAVED_INSTANCE_STATE_UI_STATE_KEY = "uistate"; - - private ConversationActivityUiState mUiState; - - // Fragment transactions cannot be performed after onSaveInstanceState() has been called since - // it will cause state loss. We don't want to call commitAllowingStateLoss() since it's - // dangerous. Therefore, we note when instance state is saved and avoid performing UI state - // updates concerning fragments past that point. - private boolean mInstanceStateSaved; - - // Tracks whether onPause is called. - private boolean mIsPaused; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.conversation_activity); - - final Intent intent = getIntent(); - - // Do our best to restore UI state from saved instance state. - if (savedInstanceState != null) { - mUiState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_UI_STATE_KEY); - } else { - if (intent. - getBooleanExtra(UIIntents.UI_INTENT_EXTRA_GOTO_CONVERSATION_LIST, false)) { - // See the comment in BugleWidgetService.getViewMoreConversationsView() why this - // is unfortunately necessary. The Bugle desktop widget can display a list of - // conversations. When there are more conversations that can be displayed in - // the widget, the last item is a "More conversations" item. The way widgets - // are built, the list items can only go to a single fill-in intent which points - // to this ConversationActivity. When the user taps on "More conversations", we - // really want to go to the ConversationList. This code makes that possible. - finish(); - final Intent convListIntent = new Intent(this, ConversationListActivity.class); - convListIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(convListIntent); - return; - } - } - - // If saved instance state doesn't offer a clue, get the info from the intent. - if (mUiState == null) { - final String conversationId = intent.getStringExtra( - UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID); - mUiState = new ConversationActivityUiState(conversationId); - } - mUiState.setHost(this); - mInstanceStateSaved = false; - - // Don't animate UI state change for initial setup. - updateUiState(false /* animate */); - - // See if we're getting called from a widget to directly display an image or video - final String extraToDisplay = - intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_URI); - if (!TextUtils.isEmpty(extraToDisplay)) { - final String contentType = - intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_TYPE); - final Rect bounds = UiUtils.getMeasuredBoundsOnScreen( - findViewById(R.id.conversation_and_compose_container)); - if (ContentType.isImageType(contentType)) { - final Uri imagesUri = MessagingContentProvider.buildConversationImagesUri( - mUiState.getConversationId()); - UIIntents.get().launchFullScreenPhotoViewer( - this, Uri.parse(extraToDisplay), bounds, imagesUri); - } else if (ContentType.isVideoType(contentType)) { - UIIntents.get().launchFullScreenVideoViewer(this, Uri.parse(extraToDisplay)); - } - } - } - - @Override - protected void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - // After onSaveInstanceState() is called, future changes to mUiState won't update the UI - // anymore, because fragment transactions are not allowed past this point. - // For an activity recreation due to orientation change, the saved instance state keeps - // using the in-memory copy of the UI state instead of writing it to parcel as an - // optimization, so the UI state values may still change in response to, for example, - // focus change from the framework, making mUiState and actual UI inconsistent. - // Therefore, save an exact "snapshot" (clone) of the UI state object to make sure the - // restored UI state ALWAYS matches the actual restored UI components. - outState.putParcelable(SAVED_INSTANCE_STATE_UI_STATE_KEY, mUiState.clone()); - mInstanceStateSaved = true; - } - - @Override - protected void onResume() { - super.onResume(); - - // we need to reset the mInstanceStateSaved flag since we may have just been restored from - // a previous onStop() instead of an onDestroy(). - mInstanceStateSaved = false; - mIsPaused = false; - } - - @Override - protected void onPause() { - super.onPause(); - mIsPaused = true; - } - - @Override - public void onWindowFocusChanged(final boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - final ConversationFragment conversationFragment = getConversationFragment(); - // When the screen is turned on, the last used activity gets resumed, but it gets - // window focus only after the lock screen is unlocked. - if (hasFocus && conversationFragment != null) { - conversationFragment.setConversationFocus(); - } - } - - @Override - public void onDisplayHeightChanged(final int heightSpecification) { - super.onDisplayHeightChanged(heightSpecification); - invalidateActionBar(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (mUiState != null) { - mUiState.setHost(null); - } - } - - @Override - public void updateActionBar(final ActionBar actionBar) { - super.updateActionBar(actionBar); - final ConversationFragment conversation = getConversationFragment(); - final ContactPickerFragment contactPicker = getContactPicker(); - if (contactPicker != null && mUiState.shouldShowContactPickerFragment()) { - contactPicker.updateActionBar(actionBar); - } else if (conversation != null && mUiState.shouldShowConversationFragment()) { - conversation.updateActionBar(actionBar); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem menuItem) { - if (super.onOptionsItemSelected(menuItem)) { - return true; - } - if (menuItem.getItemId() == android.R.id.home) { - onNavigationUpPressed(); - return true; - } - return false; - } - - public void onNavigationUpPressed() { - // Let the conversation fragment handle the navigation up press. - final ConversationFragment conversationFragment = getConversationFragment(); - if (conversationFragment != null && conversationFragment.onNavigationUpPressed()) { - return; - } - onFinishCurrentConversation(); - } - - @Override - public void onBackPressed() { - // If action mode is active dismiss it - if (getActionMode() != null) { - dismissActionMode(); - return; - } - - // Let the conversation fragment handle the back press. - final ConversationFragment conversationFragment = getConversationFragment(); - if (conversationFragment != null && conversationFragment.onBackPressed()) { - return; - } - super.onBackPressed(); - } - - private ContactPickerFragment getContactPicker() { - return (ContactPickerFragment) getFragmentManager().findFragmentByTag( - ContactPickerFragment.FRAGMENT_TAG); - } - - private ConversationFragment getConversationFragment() { - return (ConversationFragment) getFragmentManager().findFragmentByTag( - ConversationFragment.FRAGMENT_TAG); - } - - @Override // From ContactPickerFragmentHost - public void onGetOrCreateNewConversation(final String conversationId) { - Assert.isTrue(conversationId != null); - mUiState.onGetOrCreateConversation(conversationId); - } - - @Override // From ContactPickerFragmentHost - public void onBackButtonPressed() { - onBackPressed(); - } - - @Override // From ContactPickerFragmentHost - public void onInitiateAddMoreParticipants() { - mUiState.onAddMoreParticipants(); - } - - - @Override - public void onParticipantCountChanged(final boolean canAddMoreParticipants) { - mUiState.onParticipantCountUpdated(canAddMoreParticipants); - } - - @Override // From ConversationFragmentHost - public void onStartComposeMessage() { - mUiState.onStartMessageCompose(); - } - - @Override // From ConversationFragmentHost - public void onConversationMetadataUpdated() { - invalidateActionBar(); - } - - @Override // From ConversationFragmentHost - public void onConversationMessagesUpdated(final int numberOfMessages) { - } - - @Override // From ConversationFragmentHost - public void onConversationParticipantDataLoaded(final int numberOfParticipants) { - } - - @Override // From ConversationFragmentHost - public boolean isActiveAndFocused() { - return !mIsPaused && hasWindowFocus(); - } - - @Override // From ConversationActivityUiStateListener - public void onConversationContactPickerUiStateChanged(final int oldState, final int newState, - final boolean animate) { - Assert.isTrue(oldState != newState); - updateUiState(animate); - } - - private void updateUiState(final boolean animate) { - if (mInstanceStateSaved || mIsPaused) { - return; - } - Assert.notNull(mUiState); - final Intent intent = getIntent(); - final String conversationId = mUiState.getConversationId(); - - final FragmentManager fragmentManager = getFragmentManager(); - final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); - - final boolean needConversationFragment = mUiState.shouldShowConversationFragment(); - final boolean needContactPickerFragment = mUiState.shouldShowContactPickerFragment(); - ConversationFragment conversationFragment = getConversationFragment(); - - // Set up the conversation fragment. - if (needConversationFragment) { - Assert.notNull(conversationId); - if (conversationFragment == null) { - conversationFragment = new ConversationFragment(); - fragmentTransaction.add(R.id.conversation_fragment_container, - conversationFragment, ConversationFragment.FRAGMENT_TAG); - } - final MessageData draftData = intent.getParcelableExtra( - UIIntents.UI_INTENT_EXTRA_DRAFT_DATA); - if (!needContactPickerFragment) { - // Once the user has committed the audience,remove the draft data from the - // intent to prevent reuse - intent.removeExtra(UIIntents.UI_INTENT_EXTRA_DRAFT_DATA); - } - conversationFragment.setHost(this); - conversationFragment.setConversationInfo(this, conversationId, draftData); - } else if (conversationFragment != null) { - // Don't save draft to DB when removing conversation fragment and switching to - // contact picking mode. The draft is intended for the new group. - conversationFragment.suppressWriteDraft(); - fragmentTransaction.remove(conversationFragment); - } - - // Set up the contact picker fragment. - ContactPickerFragment contactPickerFragment = getContactPicker(); - if (needContactPickerFragment) { - if (contactPickerFragment == null) { - contactPickerFragment = new ContactPickerFragment(); - fragmentTransaction.add(R.id.contact_picker_fragment_container, - contactPickerFragment, ContactPickerFragment.FRAGMENT_TAG); - } - contactPickerFragment.setHost(this); - contactPickerFragment.setContactPickingMode(mUiState.getDesiredContactPickingMode(), - animate); - } else if (contactPickerFragment != null) { - fragmentTransaction.remove(contactPickerFragment); - } - - fragmentTransaction.commit(); - invalidateActionBar(); - } - - @Override - public void onFinishCurrentConversation() { - // Simply finish the current activity. The current design is to leave any empty - // conversations as is. - if (OsUtil.isAtLeastL()) { - finishAfterTransition(); - } else { - finish(); - } - } - - @Override - public boolean shouldResumeComposeMessage() { - return mUiState.shouldResumeComposeMessage(); - } - - @Override - protected void onActivityResult(final int requestCode, final int resultCode, - final Intent data) { - if (requestCode == ConversationFragment.REQUEST_CHOOSE_ATTACHMENTS && - resultCode == RESULT_OK) { - final ConversationFragment conversationFragment = getConversationFragment(); - if (conversationFragment != null) { - conversationFragment.onAttachmentChoosen(); - } else { - LogUtil.e(LogUtil.BUGLE_TAG, "ConversationFragment is missing after launching " + - "AttachmentChooserActivity!"); - } - } else if (resultCode == FINISH_RESULT_CODE) { - finish(); - } - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationActivityUiState.java b/src/com/android/messaging/ui/conversation/ConversationActivityUiState.java deleted file mode 100644 index 1469c93..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationActivityUiState.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * 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.conversation; - -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.messaging.ui.contact.ContactPickerFragment; -import com.android.messaging.util.Assert; -import com.google.common.annotations.VisibleForTesting; - -/** - * Keeps track of the different UI states that the ConversationActivity may be in. This acts as - * a state machine which, based on different actions (e.g. onAddMoreParticipants), notifies the - * ConversationActivity about any state UI change so it can update the visuals. This class - * implements Parcelable and it's persisted across activity tear down and relaunch. - */ -public class ConversationActivityUiState implements Parcelable, Cloneable { - interface ConversationActivityUiStateHost { - void onConversationContactPickerUiStateChanged(int oldState, int newState, boolean animate); - } - - /*------ Overall UI states (conversation & contact picker) ------*/ - - /** Only a full screen conversation is showing. */ - public static final int STATE_CONVERSATION_ONLY = 1; - /** Only a full screen contact picker is showing asking user to pick the initial contact. */ - public static final int STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT = 2; - /** - * Only a full screen contact picker is showing asking user to pick more participants. This - * happens after the user picked the initial contact, and then decide to go back and add more. - */ - public static final int STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS = 3; - /** - * Only a full screen contact picker is showing asking user to pick more participants. However - * user has reached max number of conversation participants and can add no more. - */ - public static final int STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS = 4; - /** - * A hybrid mode where the conversation view + contact chips view are showing. This happens - * right after the user picked the initial contact for which a 1-1 conversation is fetched or - * created. - */ - public static final int STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW = 5; - - // The overall UI state of the ConversationActivity. - private int mConversationContactUiState; - - // The currently displayed conversation (if any). - private String mConversationId; - - // Indicates whether we should put focus in the compose message view when the - // ConversationFragment is attached. This is a transient state that's not persisted as - // part of the parcelable. - private boolean mPendingResumeComposeMessage = false; - - // The owner ConversationActivity. This is not parceled since the instance always change upon - // object reuse. - private ConversationActivityUiStateHost mHost; - - // Indicates the owning ConverastionActivity is in the process of updating its UI presentation - // to be in sync with the UI states. Outside of the UI updates, the UI states here should - // ALWAYS be consistent with the actual states of the activity. - private int mUiUpdateCount; - - /** - * Create a new instance with an initial conversation id. - */ - ConversationActivityUiState(final String conversationId) { - // The conversation activity may be initialized with only one of two states: - // Conversation-only (when there's a conversation id) or picking initial contact - // (when no conversation id is given). - mConversationId = conversationId; - mConversationContactUiState = conversationId == null ? - STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT : STATE_CONVERSATION_ONLY; - } - - public void setHost(final ConversationActivityUiStateHost host) { - mHost = host; - } - - public boolean shouldShowConversationFragment() { - return mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW || - mConversationContactUiState == STATE_CONVERSATION_ONLY; - } - - public boolean shouldShowContactPickerFragment() { - return mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS || - mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS || - mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT || - mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW; - } - - /** - * Returns whether there's a pending request to resume message compose (i.e. set focus to - * the compose message view and show the soft keyboard). If so, this request will be served - * when the conversation fragment get created and resumed. This happens when the user commits - * participant selection for a group conversation and goes back to the conversation fragment. - * Since conversation fragment creation happens asynchronously, we issue and track this - * pending request for it to be eventually fulfilled. - */ - public boolean shouldResumeComposeMessage() { - if (mPendingResumeComposeMessage) { - // This is a one-shot operation that just keeps track of the pending resume compose - // state. This is also a non-critical operation so we don't care about failure case. - mPendingResumeComposeMessage = false; - return true; - } - return false; - } - - public int getDesiredContactPickingMode() { - switch (mConversationContactUiState) { - case STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS: - return ContactPickerFragment.MODE_PICK_MORE_CONTACTS; - case STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS: - return ContactPickerFragment.MODE_PICK_MAX_PARTICIPANTS; - case STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT: - return ContactPickerFragment.MODE_PICK_INITIAL_CONTACT; - case STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW: - return ContactPickerFragment.MODE_CHIPS_ONLY; - default: - Assert.fail("Invalid contact picking mode for ConversationActivity!"); - return ContactPickerFragment.MODE_UNDEFINED; - } - } - - public String getConversationId() { - return mConversationId; - } - - /** - * Called whenever the contact picker fragment successfully fetched or created a conversation. - */ - public void onGetOrCreateConversation(final String conversationId) { - int newState = STATE_CONVERSATION_ONLY; - if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) { - newState = STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW; - } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS || - mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS) { - newState = STATE_CONVERSATION_ONLY; - } else { - // New conversation should only be created when we are in one of the contact picking - // modes. - Assert.fail("Invalid conversation activity state: can't create conversation!"); - } - mConversationId = conversationId; - performUiStateUpdate(newState, true); - } - - /** - * Called when the user started composing message. If we are in the hybrid chips state, we - * should commit to enter the conversation only state. - */ - public void onStartMessageCompose() { - // This cannot happen when we are in one of the full-screen contact picking states. - Assert.isTrue(mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT && - mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS && - mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS); - if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) { - performUiStateUpdate(STATE_CONVERSATION_ONLY, true); - } - } - - /** - * Called when the user initiated an action to add more participants in the hybrid state, - * namely clicking on the "add more participants" button or entered a new contact chip via - * auto-complete. - */ - public void onAddMoreParticipants() { - if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) { - mPendingResumeComposeMessage = true; - performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, true); - } else { - // This is only possible in the hybrid state. - Assert.fail("Invalid conversation activity state: can't add more participants!"); - } - } - - /** - * Called each time the number of participants is updated to check against the limit and - * update the ui state accordingly. - */ - public void onParticipantCountUpdated(final boolean canAddMoreParticipants) { - if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS - && !canAddMoreParticipants) { - performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS, false); - } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS - && canAddMoreParticipants) { - performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, false); - } - } - - private void performUiStateUpdate(final int conversationContactState, final boolean animate) { - // This starts one UI update cycle, during which we allow the conversation activity's - // UI presentation to be temporarily out of sync with the states here. - beginUiUpdate(); - - if (conversationContactState != mConversationContactUiState) { - final int oldState = mConversationContactUiState; - mConversationContactUiState = conversationContactState; - notifyOnOverallUiStateChanged(oldState, mConversationContactUiState, animate); - } - endUiUpdate(); - } - - private void notifyOnOverallUiStateChanged( - final int oldState, final int newState, final boolean animate) { - // Always verify state validity whenever we have a state change. - assertValidState(); - Assert.isTrue(isUiUpdateInProgress()); - - // Only do this if we are still attached to the host. mHost can be null if the host - // activity is already destroyed, but due to timing the contained UI components may still - // receive events such as focus change and trigger a callback to the Ui state. We'd like - // to guard against those cases. - if (mHost != null) { - mHost.onConversationContactPickerUiStateChanged(oldState, newState, animate); - } - } - - private void assertValidState() { - // Conversation id may be null IF AND ONLY IF the user is picking the initial contact to - // start a conversation. - Assert.isTrue((mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) == - (mConversationId == null)); - } - - private void beginUiUpdate() { - mUiUpdateCount++; - } - - private void endUiUpdate() { - if (--mUiUpdateCount < 0) { - Assert.fail("Unbalanced Ui updates!"); - } - } - - private boolean isUiUpdateInProgress() { - return mUiUpdateCount > 0; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - dest.writeInt(mConversationContactUiState); - dest.writeString(mConversationId); - } - - private ConversationActivityUiState(final Parcel in) { - mConversationContactUiState = in.readInt(); - mConversationId = in.readString(); - - // Always verify state validity whenever we initialize states. - assertValidState(); - } - - public static final Parcelable.Creator<ConversationActivityUiState> CREATOR - = new Parcelable.Creator<ConversationActivityUiState>() { - @Override - public ConversationActivityUiState createFromParcel(final Parcel in) { - return new ConversationActivityUiState(in); - } - - @Override - public ConversationActivityUiState[] newArray(final int size) { - return new ConversationActivityUiState[size]; - } - }; - - @Override - protected ConversationActivityUiState clone() { - try { - return (ConversationActivityUiState) super.clone(); - } catch (CloneNotSupportedException e) { - Assert.fail("ConversationActivityUiState: failed to clone(). Is there a mutable " + - "reference?"); - } - return null; - } - - /** - * allows for overridding the internal UI state. Should never be called except by test code. - */ - @VisibleForTesting - void testSetUiState(final int uiState) { - mConversationContactUiState = uiState; - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationFastScroller.java b/src/com/android/messaging/ui/conversation/ConversationFastScroller.java deleted file mode 100644 index b15f05a..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationFastScroller.java +++ /dev/null @@ -1,489 +0,0 @@ -/* - * 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.conversation; - -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Rect; -import android.graphics.drawable.StateListDrawable; -import android.os.Handler; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.AdapterDataObserver; -import android.support.v7.widget.RecyclerView.ViewHolder; -import android.util.StateSet; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.MeasureSpec; -import android.view.View.OnLayoutChangeListener; -import android.view.ViewGroupOverlay; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ConversationMessageData; -import com.android.messaging.ui.ConversationDrawables; -import com.android.messaging.util.Dates; -import com.android.messaging.util.OsUtil; - -/** - * Adds a "fast-scroll" bar to the conversation RecyclerView that shows the current position within - * the conversation and allows quickly moving to another position by dragging the scrollbar thumb - * up or down. As the thumb is dragged, we show a floating bubble alongside it that shows the - * date/time of the first visible message at the current position. - */ -public class ConversationFastScroller extends RecyclerView.OnScrollListener implements - OnLayoutChangeListener, RecyclerView.OnItemTouchListener { - - /** - * Creates a {@link ConversationFastScroller} instance, attached to the provided - * {@link RecyclerView}. - * - * @param rv the conversation RecyclerView - * @param position where the scrollbar should appear (either {@code POSITION_RIGHT_SIDE} or - * {@code POSITION_LEFT_SIDE}) - * @return a new ConversationFastScroller, or {@code null} if fast-scrolling is not supported - * (the feature requires Jellybean MR2 or newer) - */ - public static ConversationFastScroller addTo(RecyclerView rv, int position) { - if (OsUtil.isAtLeastJB_MR2()) { - return new ConversationFastScroller(rv, position); - } - return null; - } - - public static final int POSITION_RIGHT_SIDE = 0; - public static final int POSITION_LEFT_SIDE = 1; - - private static final int MIN_PAGES_TO_ENABLE = 7; - private static final int SHOW_ANIMATION_DURATION_MS = 150; - private static final int HIDE_ANIMATION_DURATION_MS = 300; - private static final int HIDE_DELAY_MS = 1500; - - private final Context mContext; - private final RecyclerView mRv; - private final ViewGroupOverlay mOverlay; - private final ImageView mTrackImageView; - private final ImageView mThumbImageView; - private final TextView mPreviewTextView; - - private final int mTrackWidth; - private final int mThumbHeight; - private final int mPreviewHeight; - private final int mPreviewMinWidth; - private final int mPreviewMarginTop; - private final int mPreviewMarginLeftRight; - private final int mTouchSlop; - - private final Rect mContainer = new Rect(); - private final Handler mHandler = new Handler(); - - // Whether to render the scrollbar on the right side (otherwise it'll be on the left). - private final boolean mPosRight; - - // Whether the scrollbar is currently visible (it may still be animating). - private boolean mVisible = false; - - // Whether we are waiting to hide the scrollbar (i.e. scrolling has stopped). - private boolean mPendingHide = false; - - // Whether the user is currently dragging the thumb up or down. - private boolean mDragging = false; - - // Animations responsible for hiding the scrollbar & preview. May be null. - private AnimatorSet mHideAnimation; - private ObjectAnimator mHidePreviewAnimation; - - private final Runnable mHideTrackRunnable = new Runnable() { - @Override - public void run() { - hide(true /* animate */); - mPendingHide = false; - } - }; - - private ConversationFastScroller(RecyclerView rv, int position) { - mContext = rv.getContext(); - mRv = rv; - mRv.addOnLayoutChangeListener(this); - mRv.addOnScrollListener(this); - mRv.addOnItemTouchListener(this); - mRv.getAdapter().registerAdapterDataObserver(new AdapterDataObserver() { - @Override - public void onChanged() { - updateScrollPos(); - } - }); - mPosRight = (position == POSITION_RIGHT_SIDE); - - // Cache the dimensions we'll need during layout - final Resources res = mContext.getResources(); - mTrackWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_width); - mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height); - mPreviewHeight = res.getDimensionPixelSize(R.dimen.fastscroll_preview_height); - mPreviewMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_preview_min_width); - mPreviewMarginTop = res.getDimensionPixelOffset(R.dimen.fastscroll_preview_margin_top); - mPreviewMarginLeftRight = res.getDimensionPixelOffset( - R.dimen.fastscroll_preview_margin_left_right); - mTouchSlop = res.getDimensionPixelOffset(R.dimen.fastscroll_touch_slop); - - final LayoutInflater inflator = LayoutInflater.from(mContext); - mTrackImageView = (ImageView) inflator.inflate(R.layout.fastscroll_track, null); - mThumbImageView = (ImageView) inflator.inflate(R.layout.fastscroll_thumb, null); - mPreviewTextView = (TextView) inflator.inflate(R.layout.fastscroll_preview, null); - - refreshConversationThemeColor(); - - // Add the fast scroll views to the overlay, so they are rendered above the list - mOverlay = rv.getOverlay(); - mOverlay.add(mTrackImageView); - mOverlay.add(mThumbImageView); - mOverlay.add(mPreviewTextView); - - hide(false /* animate */); - mPreviewTextView.setAlpha(0f); - } - - public void refreshConversationThemeColor() { - mPreviewTextView.setBackground( - ConversationDrawables.get().getFastScrollPreviewDrawable(mPosRight)); - if (OsUtil.isAtLeastL()) { - final StateListDrawable drawable = new StateListDrawable(); - drawable.addState(new int[]{ android.R.attr.state_pressed }, - ConversationDrawables.get().getFastScrollThumbDrawable(true /* pressed */)); - drawable.addState(StateSet.WILD_CARD, - ConversationDrawables.get().getFastScrollThumbDrawable(false /* pressed */)); - mThumbImageView.setImageDrawable(drawable); - } else { - // Android pre-L doesn't seem to handle a StateListDrawable containing a tinted - // drawable (it's rendered in the filter base color, which is red), so fall back to - // just the regular (non-pressed) drawable. - mThumbImageView.setImageDrawable( - ConversationDrawables.get().getFastScrollThumbDrawable(false /* pressed */)); - } - } - - @Override - public void onScrollStateChanged(final RecyclerView view, final int newState) { - if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { - // Only show the scrollbar once the user starts scrolling - if (!mVisible && isEnabled()) { - show(); - } - cancelAnyPendingHide(); - } else if (newState == RecyclerView.SCROLL_STATE_IDLE && !mDragging) { - // Hide the scrollbar again after scrolling stops - hideAfterDelay(); - } - } - - private boolean isEnabled() { - final int range = mRv.computeVerticalScrollRange(); - final int extent = mRv.computeVerticalScrollExtent(); - - if (range == 0 || extent == 0) { - return false; // Conversation isn't long enough to scroll - } - // Only enable scrollbars for conversations long enough that they would require several - // flings to scroll through. - final float pages = (float) range / extent; - return (pages > MIN_PAGES_TO_ENABLE); - } - - private void show() { - if (mHideAnimation != null && mHideAnimation.isRunning()) { - mHideAnimation.cancel(); - } - // Slide the scrollbar in from the side - ObjectAnimator trackSlide = ObjectAnimator.ofFloat(mTrackImageView, View.TRANSLATION_X, 0); - ObjectAnimator thumbSlide = ObjectAnimator.ofFloat(mThumbImageView, View.TRANSLATION_X, 0); - AnimatorSet animation = new AnimatorSet(); - animation.playTogether(trackSlide, thumbSlide); - animation.setDuration(SHOW_ANIMATION_DURATION_MS); - animation.start(); - - mVisible = true; - updateScrollPos(); - } - - private void hideAfterDelay() { - cancelAnyPendingHide(); - mHandler.postDelayed(mHideTrackRunnable, HIDE_DELAY_MS); - mPendingHide = true; - } - - private void cancelAnyPendingHide() { - if (mPendingHide) { - mHandler.removeCallbacks(mHideTrackRunnable); - } - } - - private void hide(boolean animate) { - final int hiddenTranslationX = mPosRight ? mTrackWidth : -mTrackWidth; - if (animate) { - // Slide the scrollbar off to the side - ObjectAnimator trackSlide = ObjectAnimator.ofFloat(mTrackImageView, View.TRANSLATION_X, - hiddenTranslationX); - ObjectAnimator thumbSlide = ObjectAnimator.ofFloat(mThumbImageView, View.TRANSLATION_X, - hiddenTranslationX); - mHideAnimation = new AnimatorSet(); - mHideAnimation.playTogether(trackSlide, thumbSlide); - mHideAnimation.setDuration(HIDE_ANIMATION_DURATION_MS); - mHideAnimation.start(); - } else { - mTrackImageView.setTranslationX(hiddenTranslationX); - mThumbImageView.setTranslationX(hiddenTranslationX); - } - - mVisible = false; - } - - private void showPreview() { - if (mHidePreviewAnimation != null && mHidePreviewAnimation.isRunning()) { - mHidePreviewAnimation.cancel(); - } - mPreviewTextView.setAlpha(1f); - } - - private void hidePreview() { - mHidePreviewAnimation = ObjectAnimator.ofFloat(mPreviewTextView, View.ALPHA, 0f); - mHidePreviewAnimation.setDuration(HIDE_ANIMATION_DURATION_MS); - mHidePreviewAnimation.start(); - } - - @Override - public void onScrolled(final RecyclerView view, final int dx, final int dy) { - updateScrollPos(); - } - - private void updateScrollPos() { - if (!mVisible) { - return; - } - final int verticalScrollLength = mContainer.height() - mThumbHeight; - final int verticalScrollStart = mContainer.top + mThumbHeight / 2; - - final float scrollRatio = computeScrollRatio(); - final int thumbCenterY = verticalScrollStart + (int)(verticalScrollLength * scrollRatio); - layoutThumb(thumbCenterY); - - if (mDragging) { - updatePreviewText(); - layoutPreview(thumbCenterY); - } - } - - /** - * Returns the current position in the conversation, as a value between 0 and 1, inclusive. - * The top of the conversation is 0, the bottom is 1, the exact middle is 0.5, and so on. - */ - private float computeScrollRatio() { - final int range = mRv.computeVerticalScrollRange(); - final int extent = mRv.computeVerticalScrollExtent(); - int offset = mRv.computeVerticalScrollOffset(); - - if (range == 0 || extent == 0) { - // If the conversation doesn't scroll, we're at the bottom. - return 1.0f; - } - final int scrollRange = range - extent; - offset = Math.min(offset, scrollRange); - return offset / (float) scrollRange; - } - - private void updatePreviewText() { - final LinearLayoutManager lm = (LinearLayoutManager) mRv.getLayoutManager(); - final int pos = lm.findFirstVisibleItemPosition(); - if (pos == RecyclerView.NO_POSITION) { - return; - } - final ViewHolder vh = mRv.findViewHolderForAdapterPosition(pos); - if (vh == null) { - // This can happen if the messages update while we're dragging the thumb. - return; - } - final ConversationMessageView messageView = (ConversationMessageView) vh.itemView; - final ConversationMessageData messageData = messageView.getData(); - final long timestamp = messageData.getReceivedTimeStamp(); - final CharSequence timestampText = Dates.getFastScrollPreviewTimeString(timestamp); - mPreviewTextView.setText(timestampText); - } - - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - if (!mVisible) { - return false; - } - // If the user presses down on the scroll thumb, we'll start intercepting events from the - // RecyclerView so we can handle the move events while they're dragging it up/down. - final int action = e.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - if (isInsideThumb(e.getX(), e.getY())) { - startDrag(); - return true; - } - break; - case MotionEvent.ACTION_MOVE: - if (mDragging) { - return true; - } - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - if (mDragging) { - cancelDrag(); - } - return false; - } - return false; - } - - private boolean isInsideThumb(float x, float y) { - final int hitTargetLeft = mThumbImageView.getLeft() - mTouchSlop; - final int hitTargetRight = mThumbImageView.getRight() + mTouchSlop; - - if (x < hitTargetLeft || x > hitTargetRight) { - return false; - } - if (y < mThumbImageView.getTop() || y > mThumbImageView.getBottom()) { - return false; - } - return true; - } - - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent e) { - if (!mDragging) { - return; - } - final int action = e.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_MOVE: - handleDragMove(e.getY()); - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - cancelDrag(); - break; - } - } - - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - } - - private void startDrag() { - mDragging = true; - mThumbImageView.setPressed(true); - updateScrollPos(); - showPreview(); - cancelAnyPendingHide(); - } - - private void handleDragMove(float y) { - final int verticalScrollLength = mContainer.height() - mThumbHeight; - final int verticalScrollStart = mContainer.top + (mThumbHeight / 2); - - // Convert the desired position from px to a scroll position in the conversation. - float dragScrollRatio = (y - verticalScrollStart) / verticalScrollLength; - dragScrollRatio = Math.max(dragScrollRatio, 0.0f); - dragScrollRatio = Math.min(dragScrollRatio, 1.0f); - - // Scroll the RecyclerView to a new position. - final int itemCount = mRv.getAdapter().getItemCount(); - final int itemPos = (int)((itemCount - 1) * dragScrollRatio); - mRv.scrollToPosition(itemPos); - } - - private void cancelDrag() { - mDragging = false; - mThumbImageView.setPressed(false); - hidePreview(); - hideAfterDelay(); - } - - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - if (!mVisible) { - hide(false /* animate */); - } - // The container is the size of the RecyclerView that's visible on screen. We have to - // exclude the top padding, because it's usually hidden behind the conversation action bar. - mContainer.set(left, top + mRv.getPaddingTop(), right, bottom); - layoutTrack(); - updateScrollPos(); - } - - private void layoutTrack() { - int trackHeight = Math.max(0, mContainer.height()); - int widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTrackWidth, MeasureSpec.EXACTLY); - int heightMeasureSpec = MeasureSpec.makeMeasureSpec(trackHeight, MeasureSpec.EXACTLY); - mTrackImageView.measure(widthMeasureSpec, heightMeasureSpec); - - int left = mPosRight ? (mContainer.right - mTrackWidth) : mContainer.left; - int top = mContainer.top; - int right = mPosRight ? mContainer.right : (mContainer.left + mTrackWidth); - int bottom = mContainer.bottom; - mTrackImageView.layout(left, top, right, bottom); - } - - private void layoutThumb(int centerY) { - int widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTrackWidth, MeasureSpec.EXACTLY); - int heightMeasureSpec = MeasureSpec.makeMeasureSpec(mThumbHeight, MeasureSpec.EXACTLY); - mThumbImageView.measure(widthMeasureSpec, heightMeasureSpec); - - int left = mPosRight ? (mContainer.right - mTrackWidth) : mContainer.left; - int top = centerY - (mThumbImageView.getHeight() / 2); - int right = mPosRight ? mContainer.right : (mContainer.left + mTrackWidth); - int bottom = top + mThumbHeight; - mThumbImageView.layout(left, top, right, bottom); - } - - private void layoutPreview(int centerY) { - int widthMeasureSpec = MeasureSpec.makeMeasureSpec(mContainer.width(), MeasureSpec.AT_MOST); - int heightMeasureSpec = MeasureSpec.makeMeasureSpec(mPreviewHeight, MeasureSpec.EXACTLY); - mPreviewTextView.measure(widthMeasureSpec, heightMeasureSpec); - - // Ensure that the preview bubble is at least as wide as it is tall - if (mPreviewTextView.getMeasuredWidth() < mPreviewMinWidth) { - widthMeasureSpec = MeasureSpec.makeMeasureSpec(mPreviewMinWidth, MeasureSpec.EXACTLY); - mPreviewTextView.measure(widthMeasureSpec, heightMeasureSpec); - } - final int previewMinY = mContainer.top + mPreviewMarginTop; - - final int left, right; - if (mPosRight) { - right = mContainer.right - mTrackWidth - mPreviewMarginLeftRight; - left = right - mPreviewTextView.getMeasuredWidth(); - } else { - left = mContainer.left + mTrackWidth + mPreviewMarginLeftRight; - right = left + mPreviewTextView.getMeasuredWidth(); - } - - int bottom = centerY; - int top = bottom - mPreviewTextView.getMeasuredHeight(); - if (top < previewMinY) { - top = previewMinY; - bottom = top + mPreviewTextView.getMeasuredHeight(); - } - mPreviewTextView.layout(left, top, right, bottom); - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationFragment.java b/src/com/android/messaging/ui/conversation/ConversationFragment.java deleted file mode 100644 index a6a191a..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationFragment.java +++ /dev/null @@ -1,1662 +0,0 @@ -/* - * 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.conversation; - -import android.Manifest; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.DownloadManager; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.BroadcastReceiver; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnDismissListener; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.database.Cursor; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.ColorDrawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.Parcelable; -import android.support.v4.content.LocalBroadcastManager; -import android.support.v4.text.BidiFormatter; -import android.support.v4.text.TextDirectionHeuristicsCompat; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.DefaultItemAnimator; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.ViewHolder; -import android.text.TextUtils; -import android.view.ActionMode; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.action.InsertNewMessageAction; -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.ConversationData; -import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener; -import com.android.messaging.datamodel.data.ConversationMessageData; -import com.android.messaging.datamodel.data.ConversationParticipantsData; -import com.android.messaging.datamodel.data.DraftMessageData; -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; -import com.android.messaging.ui.AttachmentPreview; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.ConversationDrawables; -import com.android.messaging.ui.SnackBar; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.animation.PopupTransitionAnimation; -import com.android.messaging.ui.contact.AddContactsConfirmationDialog; -import com.android.messaging.ui.conversation.ComposeMessageView.IComposeMessageViewHost; -import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputHost; -import com.android.messaging.ui.conversation.ConversationMessageView.ConversationMessageViewHost; -import com.android.messaging.ui.mediapicker.MediaPicker; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.Assert; -import com.android.messaging.util.AvatarUriUtil; -import com.android.messaging.util.ChangeDefaultSmsAppHelper; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.ImeUtil; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.SafeAsyncTask; -import com.android.messaging.util.TextUtil; -import com.android.messaging.util.UiUtils; -import com.android.messaging.util.UriUtil; -import com.google.common.annotations.VisibleForTesting; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -/** - * Shows a list of messages/parts comprising a conversation. - */ -public class ConversationFragment extends Fragment implements ConversationDataListener, - IComposeMessageViewHost, ConversationMessageViewHost, ConversationInputHost, - DraftMessageDataListener { - - public interface ConversationFragmentHost extends ImeUtil.ImeStateHost { - void onStartComposeMessage(); - void onConversationMetadataUpdated(); - boolean shouldResumeComposeMessage(); - void onFinishCurrentConversation(); - void invalidateActionBar(); - ActionMode startActionMode(ActionMode.Callback callback); - void dismissActionMode(); - ActionMode getActionMode(); - void onConversationMessagesUpdated(int numberOfMessages); - void onConversationParticipantDataLoaded(int numberOfParticipants); - boolean isActiveAndFocused(); - } - - public static final String FRAGMENT_TAG = "conversation"; - - static final int REQUEST_CHOOSE_ATTACHMENTS = 2; - private static final int JUMP_SCROLL_THRESHOLD = 15; - // We animate the message from draft to message list, if we the message doesn't show up in the - // list within this time limit, then we just do a fade in animation instead - public static final int MESSAGE_ANIMATION_MAX_WAIT = 500; - - private ComposeMessageView mComposeMessageView; - private RecyclerView mRecyclerView; - private ConversationMessageAdapter mAdapter; - private ConversationFastScroller mFastScroller; - - private View mConversationComposeDivider; - private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper; - - private String mConversationId; - // If the fragment receives a draft as part of the invocation this is set - private MessageData mIncomingDraft; - - // This binding keeps track of our associated ConversationData instance - // A binding should have the lifetime of the owning component, - // don't recreate, unbind and bind if you need new data - @VisibleForTesting - final Binding<ConversationData> mBinding = BindingBase.createBinding(this); - - // Saved Instance State Data - only for temporal data which is nice to maintain but not - // critical for correctness. - private static final String SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY = "conversationViewState"; - private Parcelable mListState; - - private ConversationFragmentHost mHost; - - protected List<Integer> mFilterResults; - - // The minimum scrolling distance between RecyclerView's scroll change event beyong which - // a fling motion is considered fast, in which case we'll delay load image attachments for - // perf optimization. - private int mFastFlingThreshold; - - // ConversationMessageView that is currently selected - private ConversationMessageView mSelectedMessage; - - // Attachment data for the attachment within the selected message that was long pressed - private MessagePartData mSelectedAttachment; - - // Normally, as soon as draft message is loaded, we trust the UI state held in - // ComposeMessageView to be the only source of truth (incl. the conversation self id). However, - // there can be external events that forces the UI state to change, such as SIM state changes - // or SIM auto-switching on receiving a message. This receiver is used to receive such - // local broadcast messages and reflect the change in the UI. - private final BroadcastReceiver mConversationSelfIdChangeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String conversationId = - intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID); - final String selfId = - intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_SELF_ID); - Assert.notNull(conversationId); - Assert.notNull(selfId); - if (TextUtils.equals(mBinding.getData().getConversationId(), conversationId)) { - mComposeMessageView.updateConversationSelfIdOnExternalChange(selfId); - } - } - }; - - // Flag to prevent writing draft to DB on pause - private boolean mSuppressWriteDraft; - - // Indicates whether local draft should be cleared due to external draft changes that must - // be reloaded from db - private boolean mClearLocalDraft; - private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel; - - private boolean isScrolledToBottom() { - if (mRecyclerView.getChildCount() == 0) { - return true; - } - final View lastView = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1); - int lastVisibleItem = ((LinearLayoutManager) mRecyclerView - .getLayoutManager()).findLastVisibleItemPosition(); - if (lastVisibleItem < 0) { - // If the recyclerView height is 0, then the last visible item position is -1 - // Try to compute the position of the last item, even though it's not visible - final long id = mRecyclerView.getChildItemId(lastView); - final RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForItemId(id); - if (holder != null) { - lastVisibleItem = holder.getAdapterPosition(); - } - } - final int totalItemCount = mRecyclerView.getAdapter().getItemCount(); - final boolean isAtBottom = (lastVisibleItem + 1 == totalItemCount); - return isAtBottom && lastView.getBottom() <= mRecyclerView.getHeight(); - } - - private void scrollToBottom(final boolean smoothScroll) { - if (mAdapter.getItemCount() > 0) { - scrollToPosition(mAdapter.getItemCount() - 1, smoothScroll); - } - } - - private int mScrollToDismissThreshold; - private final RecyclerView.OnScrollListener mListScrollListener = - new RecyclerView.OnScrollListener() { - // Keeps track of cumulative scroll delta during a scroll event, which we may use to - // hide the media picker & co. - private int mCumulativeScrollDelta; - private boolean mScrollToDismissHandled; - private boolean mWasScrolledToBottom = true; - private int mScrollState = RecyclerView.SCROLL_STATE_IDLE; - - @Override - public void onScrollStateChanged(final RecyclerView view, final int newState) { - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - // Reset scroll states. - mCumulativeScrollDelta = 0; - mScrollToDismissHandled = false; - } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { - mRecyclerView.getItemAnimator().endAnimations(); - } - mScrollState = newState; - } - - @Override - public void onScrolled(final RecyclerView view, final int dx, final int dy) { - if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING && - !mScrollToDismissHandled) { - mCumulativeScrollDelta += dy; - // Dismiss the keyboard only when the user scroll up (into the past). - if (mCumulativeScrollDelta < -mScrollToDismissThreshold) { - mComposeMessageView.hideAllComposeInputs(false /* animate */); - mScrollToDismissHandled = true; - } - } - if (mWasScrolledToBottom != isScrolledToBottom()) { - mConversationComposeDivider.animate().alpha(isScrolledToBottom() ? 0 : 1); - mWasScrolledToBottom = isScrolledToBottom(); - } - } - }; - - private final ActionMode.Callback mMessageActionModeCallback = new ActionMode.Callback() { - @Override - public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) { - if (mSelectedMessage == null) { - return false; - } - final ConversationMessageData data = mSelectedMessage.getData(); - final MenuInflater menuInflater = getActivity().getMenuInflater(); - menuInflater.inflate(R.menu.conversation_fragment_select_menu, menu); - menu.findItem(R.id.action_download).setVisible(data.getShowDownloadMessage()); - menu.findItem(R.id.action_send).setVisible(data.getShowResendMessage()); - - // ShareActionProvider does not work with ActionMode. So we use a normal menu item. - menu.findItem(R.id.share_message_menu).setVisible(data.getCanForwardMessage()); - menu.findItem(R.id.save_attachment).setVisible(mSelectedAttachment != null); - menu.findItem(R.id.forward_message_menu).setVisible(data.getCanForwardMessage()); - - // TODO: We may want to support copying attachments in the future, but it's - // unclear which attachment to pick when we make this context menu at the message level - // instead of the part level - menu.findItem(R.id.copy_text).setVisible(data.getCanCopyMessageToClipboard()); - - return true; - } - - @Override - public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) { - return true; - } - - @Override - public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) { - final ConversationMessageData data = mSelectedMessage.getData(); - final String messageId = data.getMessageId(); - switch (menuItem.getItemId()) { - case R.id.save_attachment: - if (OsUtil.hasStoragePermission()) { - final SaveAttachmentTask saveAttachmentTask = new SaveAttachmentTask( - getActivity()); - for (final MessagePartData part : data.getAttachments()) { - saveAttachmentTask.addAttachmentToSave(part.getContentUri(), - part.getContentType()); - } - if (saveAttachmentTask.getAttachmentCount() > 0) { - saveAttachmentTask.executeOnThreadPool(); - mHost.dismissActionMode(); - } - } else { - getActivity().requestPermissions( - new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0); - } - return true; - case R.id.action_delete_message: - if (mSelectedMessage != null) { - deleteMessage(messageId); - } - return true; - case R.id.action_download: - if (mSelectedMessage != null) { - retryDownload(messageId); - mHost.dismissActionMode(); - } - return true; - case R.id.action_send: - if (mSelectedMessage != null) { - retrySend(messageId); - mHost.dismissActionMode(); - } - return true; - case R.id.copy_text: - Assert.isTrue(data.hasText()); - final ClipboardManager clipboard = (ClipboardManager) getActivity() - .getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip( - ClipData.newPlainText(null /* label */, data.getText())); - mHost.dismissActionMode(); - return true; - case R.id.details_menu: - MessageDetailsDialog.show( - getActivity(), data, mBinding.getData().getParticipants(), - mBinding.getData().getSelfParticipantById(data.getSelfParticipantId())); - mHost.dismissActionMode(); - return true; - case R.id.share_message_menu: - shareMessage(data); - mHost.dismissActionMode(); - return true; - case R.id.forward_message_menu: - // TODO: Currently we are forwarding one part at a time, instead of - // the entire message. Change this to forwarding the entire message when we - // use message-based cursor in conversation. - final MessageData message = mBinding.getData().createForwardedMessage(data); - UIIntents.get().launchForwardMessageActivity(getActivity(), message); - mHost.dismissActionMode(); - return true; - } - return false; - } - - private void shareMessage(final ConversationMessageData data) { - // Figure out what to share. - MessagePartData attachmentToShare = mSelectedAttachment; - // If the user long-pressed on the background, we will share the text (if any) - // or the first attachment. - if (mSelectedAttachment == null - && TextUtil.isAllWhitespace(data.getText())) { - final List<MessagePartData> attachments = data.getAttachments(); - if (attachments.size() > 0) { - attachmentToShare = attachments.get(0); - } - } - - final Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - if (attachmentToShare == null) { - shareIntent.putExtra(Intent.EXTRA_TEXT, data.getText()); - shareIntent.setType("text/plain"); - } else { - shareIntent.putExtra( - Intent.EXTRA_STREAM, attachmentToShare.getContentUri()); - shareIntent.setType(attachmentToShare.getContentType()); - } - final CharSequence title = getResources().getText(R.string.action_share); - startActivity(Intent.createChooser(shareIntent, title)); - } - - @Override - public void onDestroyActionMode(final ActionMode actionMode) { - selectMessage(null); - } - }; - - /** - * {@inheritDoc} from Fragment - */ - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mFastFlingThreshold = getResources().getDimensionPixelOffset( - R.dimen.conversation_fast_fling_threshold); - mAdapter = new ConversationMessageAdapter(getActivity(), null, this, - null, - // Sets the item click listener on the Recycler item views. - new View.OnClickListener() { - @Override - public void onClick(final View v) { - final ConversationMessageView messageView = (ConversationMessageView) v; - handleMessageClick(messageView); - } - }, - new View.OnLongClickListener() { - @Override - public boolean onLongClick(final View view) { - selectMessage((ConversationMessageView) view); - return true; - } - } - ); - } - - /** - * setConversationInfo() may be called before or after onCreate(). When a user initiate a - * conversation from compose, the ConversationActivity creates this fragment and calls - * setConversationInfo(), so it happens before onCreate(). However, when the activity is - * restored from saved instance state, the ConversationFragment is created automatically by - * the fragment, before ConversationActivity has a chance to call setConversationInfo(). Since - * the ability to start loading data depends on both methods being called, we need to start - * loading when onActivityCreated() is called, which is guaranteed to happen after both. - */ - @Override - public void onActivityCreated(final Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - // Delay showing the message list until the participant list is loaded. - mRecyclerView.setVisibility(View.INVISIBLE); - mBinding.ensureBound(); - mBinding.getData().init(getLoaderManager(), mBinding); - - // Build the input manager with all its required dependencies and pass it along to the - // compose message view. - final ConversationInputManager inputManager = new ConversationInputManager( - getActivity(), this, mComposeMessageView, mHost, getFragmentManagerToUse(), - mBinding, mComposeMessageView.getDraftDataModel(), savedInstanceState); - mComposeMessageView.setInputManager(inputManager); - mComposeMessageView.setConversationDataModel(BindingBase.createBindingReference(mBinding)); - mHost.invalidateActionBar(); - - mDraftMessageDataModel = - BindingBase.createBindingReference(mComposeMessageView.getDraftDataModel()); - mDraftMessageDataModel.getData().addListener(this); - } - - public void onAttachmentChoosen() { - // Attachment has been choosen in the AttachmentChooserActivity, so clear local draft - // and reload draft on resume. - mClearLocalDraft = true; - } - - private int getScrollToMessagePosition() { - final Activity activity = getActivity(); - if (activity == null) { - return -1; - } - - final Intent intent = activity.getIntent(); - if (intent == null) { - return -1; - } - - return intent.getIntExtra(UIIntents.UI_INTENT_EXTRA_MESSAGE_POSITION, -1); - } - - private void clearScrollToMessagePosition() { - final Activity activity = getActivity(); - if (activity == null) { - return; - } - - final Intent intent = activity.getIntent(); - if (intent == null) { - return; - } - intent.putExtra(UIIntents.UI_INTENT_EXTRA_MESSAGE_POSITION, -1); - } - - private final Handler mHandler = new Handler(); - - /** - * {@inheritDoc} from Fragment - */ - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.conversation_fragment, container, false); - mRecyclerView = (RecyclerView) view.findViewById(android.R.id.list); - final LinearLayoutManager manager = new LinearLayoutManager(getActivity()); - manager.setStackFromEnd(true); - manager.setReverseLayout(false); - mRecyclerView.setHasFixedSize(true); - mRecyclerView.setLayoutManager(manager); - mRecyclerView.setItemAnimator(new DefaultItemAnimator() { - private final List<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>(); - private PopupTransitionAnimation mPopupTransitionAnimation; - - @Override - public boolean animateAdd(final ViewHolder holder) { - final ConversationMessageView view = - (ConversationMessageView) holder.itemView; - final ConversationMessageData data = view.getData(); - endAnimation(holder); - final long timeSinceSend = System.currentTimeMillis() - data.getReceivedTimeStamp(); - if (data.getReceivedTimeStamp() == - InsertNewMessageAction.getLastSentMessageTimestamp() && - !data.getIsIncoming() && - timeSinceSend < MESSAGE_ANIMATION_MAX_WAIT) { - final ConversationMessageBubbleView messageBubble = - (ConversationMessageBubbleView) view - .findViewById(R.id.message_content); - final Rect startRect = UiUtils.getMeasuredBoundsOnScreen(mComposeMessageView); - final View composeBubbleView = mComposeMessageView.findViewById( - R.id.compose_message_text); - final Rect composeBubbleRect = - UiUtils.getMeasuredBoundsOnScreen(composeBubbleView); - final AttachmentPreview attachmentView = - (AttachmentPreview) mComposeMessageView.findViewById( - R.id.attachment_draft_view); - final Rect attachmentRect = UiUtils.getMeasuredBoundsOnScreen(attachmentView); - if (attachmentView.getVisibility() == View.VISIBLE) { - startRect.top = attachmentRect.top; - } else { - startRect.top = composeBubbleRect.top; - } - startRect.top -= view.getPaddingTop(); - startRect.bottom = - composeBubbleRect.bottom; - startRect.left += view.getPaddingRight(); - - view.setAlpha(0); - mPopupTransitionAnimation = new PopupTransitionAnimation(startRect, view); - mPopupTransitionAnimation.setOnStartCallback(new Runnable() { - @Override - public void run() { - final int startWidth = composeBubbleRect.width(); - attachmentView.onMessageAnimationStart(); - messageBubble.kickOffMorphAnimation(startWidth, - messageBubble.findViewById(R.id.message_text_and_info) - .getMeasuredWidth()); - } - }); - mPopupTransitionAnimation.setOnStopCallback(new Runnable() { - @Override - public void run() { - view.setAlpha(1); - } - }); - mPopupTransitionAnimation.startAfterLayoutComplete(); - mAddAnimations.add(holder); - return true; - } else { - return super.animateAdd(holder); - } - } - - @Override - public void endAnimation(final ViewHolder holder) { - if (mAddAnimations.remove(holder)) { - holder.itemView.clearAnimation(); - } - super.endAnimation(holder); - } - - @Override - public void endAnimations() { - for (final ViewHolder holder : mAddAnimations) { - holder.itemView.clearAnimation(); - } - mAddAnimations.clear(); - if (mPopupTransitionAnimation != null) { - mPopupTransitionAnimation.cancel(); - } - super.endAnimations(); - } - }); - mRecyclerView.setAdapter(mAdapter); - - if (savedInstanceState != null) { - mListState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY); - } - - mConversationComposeDivider = view.findViewById(R.id.conversation_compose_divider); - mScrollToDismissThreshold = ViewConfiguration.get(getActivity()).getScaledTouchSlop(); - mRecyclerView.addOnScrollListener(mListScrollListener); - mFastScroller = ConversationFastScroller.addTo(mRecyclerView, - UiUtils.isRtlMode() ? ConversationFastScroller.POSITION_LEFT_SIDE : - ConversationFastScroller.POSITION_RIGHT_SIDE); - - mComposeMessageView = (ComposeMessageView) - view.findViewById(R.id.message_compose_view_container); - // Bind the compose message view to the DraftMessageData - mComposeMessageView.bind(DataModel.get().createDraftMessageData( - mBinding.getData().getConversationId()), this); - - return view; - } - - private void scrollToPosition(final int targetPosition, final boolean smoothScroll) { - if (smoothScroll) { - final int maxScrollDelta = JUMP_SCROLL_THRESHOLD; - - final LinearLayoutManager layoutManager = - (LinearLayoutManager) mRecyclerView.getLayoutManager(); - final int firstVisibleItemPosition = - layoutManager.findFirstVisibleItemPosition(); - final int delta = targetPosition - firstVisibleItemPosition; - final int intermediatePosition; - - if (delta > maxScrollDelta) { - intermediatePosition = Math.max(0, targetPosition - maxScrollDelta); - } else if (delta < -maxScrollDelta) { - final int count = layoutManager.getItemCount(); - intermediatePosition = Math.min(count - 1, targetPosition + maxScrollDelta); - } else { - intermediatePosition = -1; - } - if (intermediatePosition != -1) { - mRecyclerView.scrollToPosition(intermediatePosition); - } - mRecyclerView.smoothScrollToPosition(targetPosition); - } else { - mRecyclerView.scrollToPosition(targetPosition); - } - } - - private int getScrollPositionFromBottom() { - final LinearLayoutManager layoutManager = - (LinearLayoutManager) mRecyclerView.getLayoutManager(); - final int lastVisibleItem = - layoutManager.findLastVisibleItemPosition(); - return Math.max(mAdapter.getItemCount() - 1 - lastVisibleItem, 0); - } - - /** - * Display a photo using the Photoviewer component. - */ - @Override - public void displayPhoto(final Uri photoUri, final Rect imageBounds, final boolean isDraft) { - displayPhoto(photoUri, imageBounds, isDraft, mConversationId, getActivity()); - } - - public static void displayPhoto(final Uri photoUri, final Rect imageBounds, - final boolean isDraft, final String conversationId, final Activity activity) { - final Uri imagesUri = - isDraft ? MessagingContentProvider.buildDraftImagesUri(conversationId) - : MessagingContentProvider.buildConversationImagesUri(conversationId); - UIIntents.get().launchFullScreenPhotoViewer( - activity, photoUri, imageBounds, imagesUri); - } - - private void selectMessage(final ConversationMessageView messageView) { - selectMessage(messageView, null /* attachment */); - } - - private void selectMessage(final ConversationMessageView messageView, - final MessagePartData attachment) { - mSelectedMessage = messageView; - if (mSelectedMessage == null) { - mAdapter.setSelectedMessage(null); - mHost.dismissActionMode(); - mSelectedAttachment = null; - return; - } - mSelectedAttachment = attachment; - mAdapter.setSelectedMessage(messageView.getData().getMessageId()); - mHost.startActionMode(mMessageActionModeCallback); - } - - @Override - public void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - if (mListState != null) { - outState.putParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY, mListState); - } - mComposeMessageView.saveInputState(outState); - } - - @Override - public void onResume() { - super.onResume(); - - if (mIncomingDraft == null) { - mComposeMessageView.requestDraftMessage(mClearLocalDraft); - } else { - mComposeMessageView.setDraftMessage(mIncomingDraft); - mIncomingDraft = null; - } - mClearLocalDraft = false; - - // On resume, check if there's a pending request for resuming message compose. This - // may happen when the user commits the contact selection for a group conversation and - // goes from compose back to the conversation fragment. - if (mHost.shouldResumeComposeMessage()) { - mComposeMessageView.resumeComposeMessage(); - } - - setConversationFocus(); - - // On resume, invalidate all message views to show the updated timestamp. - mAdapter.notifyDataSetChanged(); - - LocalBroadcastManager.getInstance(getActivity()).registerReceiver( - mConversationSelfIdChangeReceiver, - new IntentFilter(UIIntents.CONVERSATION_SELF_ID_CHANGE_BROADCAST_ACTION)); - } - - void setConversationFocus() { - if (mHost.isActiveAndFocused()) { - mBinding.getData().setFocus(); - } - } - - @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - if (mHost.getActionMode() != null) { - return; - } - - inflater.inflate(R.menu.conversation_menu, menu); - - final ConversationData data = mBinding.getData(); - - // Disable the "people & options" item if we haven't loaded participants yet. - menu.findItem(R.id.action_people_and_options).setEnabled(data.getParticipantsLoaded()); - - // See if we can show add contact action. - final ParticipantData participant = data.getOtherParticipant(); - final boolean addContactActionVisible = (participant != null - && TextUtils.isEmpty(participant.getLookupKey())); - menu.findItem(R.id.action_add_contact).setVisible(addContactActionVisible); - - // See if we should show archive or unarchive. - final boolean isArchived = data.getIsArchived(); - menu.findItem(R.id.action_archive).setVisible(!isArchived); - menu.findItem(R.id.action_unarchive).setVisible(isArchived); - - // Conditionally enable the phone call button. - final boolean supportCallAction = (PhoneUtils.getDefault().isVoiceCapable() && - data.getParticipantPhoneNumber() != null); - menu.findItem(R.id.action_call).setVisible(supportCallAction); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_people_and_options: - Assert.isTrue(mBinding.getData().getParticipantsLoaded()); - UIIntents.get().launchPeopleAndOptionsActivity(getActivity(), mConversationId); - return true; - - case R.id.action_call: - final String phoneNumber = mBinding.getData().getParticipantPhoneNumber(); - Assert.notNull(phoneNumber); - final View targetView = getActivity().findViewById(R.id.action_call); - Point centerPoint; - if (targetView != null) { - final int screenLocation[] = new int[2]; - targetView.getLocationOnScreen(screenLocation); - final int centerX = screenLocation[0] + targetView.getWidth() / 2; - final int centerY = screenLocation[1] + targetView.getHeight() / 2; - centerPoint = new Point(centerX, centerY); - } else { - // In the overflow menu, just use the center of the screen. - final Display display = getActivity().getWindowManager().getDefaultDisplay(); - centerPoint = new Point(display.getWidth() / 2, display.getHeight() / 2); - } - UIIntents.get().launchPhoneCallActivity(getActivity(), phoneNumber, centerPoint); - return true; - - case R.id.action_archive: - mBinding.getData().archiveConversation(mBinding); - closeConversation(mConversationId); - return true; - - case R.id.action_unarchive: - mBinding.getData().unarchiveConversation(mBinding); - return true; - - case R.id.action_settings: - return true; - - case R.id.action_add_contact: - final ParticipantData participant = mBinding.getData().getOtherParticipant(); - Assert.notNull(participant); - final String destination = participant.getNormalizedDestination(); - final Uri avatarUri = AvatarUriUtil.createAvatarUri(participant); - (new AddContactsConfirmationDialog(getActivity(), avatarUri, destination)).show(); - return true; - - case R.id.action_delete: - if (isReadyForAction()) { - new AlertDialog.Builder(getActivity()) - .setTitle(getResources().getQuantityString( - R.plurals.delete_conversations_confirmation_dialog_title, 1)) - .setPositiveButton(R.string.delete_conversation_confirmation_button, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int button) { - deleteConversation(); - } - }) - .setNegativeButton(R.string.delete_conversation_decline_button, null) - .show(); - } else { - warnOfMissingActionConditions(false /*sending*/, - null /*commandToRunAfterActionConditionResolved*/); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * {@inheritDoc} from ConversationDataListener - */ - @Override - public void onConversationMessagesCursorUpdated(final ConversationData data, - final Cursor cursor, final ConversationMessageData newestMessage, - final boolean isSync) { - mBinding.ensureBound(data); - - // This needs to be determined before swapping cursor, which may change the scroll state. - final boolean scrolledToBottom = isScrolledToBottom(); - final int positionFromBottom = getScrollPositionFromBottom(); - - // If participants not loaded, assume 1:1 since that's the 99% case - final boolean oneOnOne = - !data.getParticipantsLoaded() || data.getOtherParticipant() != null; - mAdapter.setOneOnOne(oneOnOne, false /* invalidate */); - - // Ensure that the action bar is updated with the current data. - invalidateOptionsMenu(); - final Cursor oldCursor = mAdapter.swapCursor(cursor); - - if (cursor != null && oldCursor == null) { - if (mListState != null) { - mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState); - // RecyclerView restores scroll states without triggering scroll change events, so - // we need to manually ensure that they are correctly handled. - mListScrollListener.onScrolled(mRecyclerView, 0, 0); - } - } - - if (isSync) { - // This is a message sync. Syncing messages changes cursor item count, which would - // implicitly change RV's scroll position. We'd like the RV to keep scrolled to the same - // relative position from the bottom (because RV is stacked from bottom), so that it - // stays relatively put as we sync. - final int position = Math.max(mAdapter.getItemCount() - 1 - positionFromBottom, 0); - scrollToPosition(position, false /* smoothScroll */); - } else if (newestMessage != null) { - // Show a snack bar notification if we are not scrolled to the bottom and the new - // message is an incoming message. - if (!scrolledToBottom && newestMessage.getIsIncoming()) { - // If the conversation activity is started but not resumed (if another dialog - // activity was in the foregrond), we will show a system notification instead of - // the snack bar. - if (mBinding.getData().isFocused()) { - UiUtils.showSnackBarWithCustomAction(getActivity(), - getView().getRootView(), - getString(R.string.in_conversation_notify_new_message_text), - SnackBar.Action.createCustomAction(new Runnable() { - @Override - public void run() { - scrollToBottom(true /* smoothScroll */); - mComposeMessageView.hideAllComposeInputs(false /* animate */); - } - }, - getString(R.string.in_conversation_notify_new_message_action)), - null /* interactions */, - SnackBar.Placement.above(mComposeMessageView)); - } - } else { - // We are either already scrolled to the bottom or this is an outgoing message, - // scroll to the bottom to reveal it. - // Don't smooth scroll if we were already at the bottom; instead, we scroll - // immediately so RecyclerView's view animation will take place. - scrollToBottom(!scrolledToBottom); - } - } - - if (cursor != null) { - mHost.onConversationMessagesUpdated(cursor.getCount()); - - // Are we coming from a widget click where we're told to scroll to a particular item? - final int scrollToPos = getScrollToMessagePosition(); - if (scrollToPos >= 0) { - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) { - LogUtil.v(LogUtil.BUGLE_TAG, "onConversationMessagesCursorUpdated " + - " scrollToPos: " + scrollToPos + - " cursorCount: " + cursor.getCount()); - } - scrollToPosition(scrollToPos, true /*smoothScroll*/); - clearScrollToMessagePosition(); - } - } - - mHost.invalidateActionBar(); - } - - /** - * {@inheritDoc} from ConversationDataListener - */ - @Override - public void onConversationMetadataUpdated(final ConversationData conversationData) { - mBinding.ensureBound(conversationData); - - if (mSelectedMessage != null && mSelectedAttachment != null) { - // We may have just sent a message and the temp attachment we selected is now gone. - // and it was replaced with some new attachment. Since we don't know which one it - // is we shouldn't reselect it (unless there is just one) In the multi-attachment - // case we would just deselect the message and allow the user to reselect, otherwise we - // may act on old temp data and may crash. - final List<MessagePartData> currentAttachments = mSelectedMessage.getData().getAttachments(); - if (currentAttachments.size() == 1) { - mSelectedAttachment = currentAttachments.get(0); - } else if (!currentAttachments.contains(mSelectedAttachment)) { - selectMessage(null); - } - } - // Ensure that the action bar is updated with the current data. - invalidateOptionsMenu(); - mHost.onConversationMetadataUpdated(); - mAdapter.notifyDataSetChanged(); - } - - public void setConversationInfo(final Context context, final String conversationId, - final MessageData draftData) { - // TODO: Eventually I would like the Factory to implement - // Factory.get().bindConversationData(mBinding, getActivity(), this, conversationId)); - if (!mBinding.isBound()) { - mConversationId = conversationId; - mIncomingDraft = draftData; - mBinding.bind(DataModel.get().createConversationData(context, this, conversationId)); - } else { - Assert.isTrue(TextUtils.equals(mBinding.getData().getConversationId(), conversationId)); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - // Unbind all the views that we bound to data - if (mComposeMessageView != null) { - mComposeMessageView.unbind(); - } - - // And unbind this fragment from its data - mBinding.unbind(); - mConversationId = null; - } - - void suppressWriteDraft() { - mSuppressWriteDraft = true; - } - - @Override - public void onPause() { - super.onPause(); - if (mComposeMessageView != null && !mSuppressWriteDraft) { - mComposeMessageView.writeDraftMessage(); - } - mSuppressWriteDraft = false; - mBinding.getData().unsetFocus(); - mListState = mRecyclerView.getLayoutManager().onSaveInstanceState(); - - LocalBroadcastManager.getInstance(getActivity()) - .unregisterReceiver(mConversationSelfIdChangeReceiver); - } - - @Override - public void onConfigurationChanged(final Configuration newConfig) { - super.onConfigurationChanged(newConfig); - mRecyclerView.getItemAnimator().endAnimations(); - } - - // TODO: Remove isBound and replace it with ensureBound after b/15704674. - public boolean isBound() { - return mBinding.isBound(); - } - - private FragmentManager getFragmentManagerToUse() { - return OsUtil.isAtLeastJB_MR1() ? getChildFragmentManager() : getFragmentManager(); - } - - public MediaPicker getMediaPicker() { - return (MediaPicker) getFragmentManagerToUse().findFragmentByTag( - MediaPicker.FRAGMENT_TAG); - } - - @Override - public void sendMessage(final MessageData message) { - if (isReadyForAction()) { - if (ensureKnownRecipients()) { - // Merge the caption text from attachments into the text body of the messages - message.consolidateText(); - - mBinding.getData().sendMessage(mBinding, message); - mComposeMessageView.resetMediaPickerState(); - } else { - LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: conv participants not loaded"); - } - } else { - warnOfMissingActionConditions(true /*sending*/, - new Runnable() { - @Override - public void run() { - sendMessage(message); - } - }); - } - } - - public void setHost(final ConversationFragmentHost host) { - mHost = host; - } - - public String getConversationName() { - return mBinding.getData().getConversationName(); - } - - @Override - public void onComposeEditTextFocused() { - mHost.onStartComposeMessage(); - } - - @Override - public void onAttachmentsCleared() { - // When attachments are removed, reset transient media picker state such as image selection. - mComposeMessageView.resetMediaPickerState(); - } - - /** - * Called to check if all conditions are nominal and a "go" for some action, such as deleting - * a message, that requires this app to be the default app. This is also a precondition - * required for sending a draft. - * @return true if all conditions are nominal and we're ready to send a message - */ - @Override - public boolean isReadyForAction() { - return UiUtils.isReadyForAction(); - } - - /** - * When there's some condition that prevents an operation, such as sending a message, - * call warnOfMissingActionConditions to put up a snackbar and allow the user to repair - * that condition. - * @param sending - true if we're called during a sending operation - * @param commandToRunAfterActionConditionResolved - a runnable to run after the user responds - * positively to the condition prompt and resolves the condition. If null, - * the user will be shown a toast to tap the send button again. - */ - @Override - public void warnOfMissingActionConditions(final boolean sending, - final Runnable commandToRunAfterActionConditionResolved) { - if (mChangeDefaultSmsAppHelper == null) { - mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper(); - } - mChangeDefaultSmsAppHelper.warnOfMissingActionConditions(sending, - commandToRunAfterActionConditionResolved, mComposeMessageView, - getView().getRootView(), - getActivity(), this); - } - - private boolean ensureKnownRecipients() { - final ConversationData conversationData = mBinding.getData(); - - if (!conversationData.getParticipantsLoaded()) { - // We can't tell yet whether or not we have an unknown recipient - return false; - } - - final ConversationParticipantsData participants = conversationData.getParticipants(); - for (final ParticipantData participant : participants) { - - - if (participant.isUnknownSender()) { - UiUtils.showToast(R.string.unknown_sender); - return false; - } - } - - return true; - } - - public void retryDownload(final String messageId) { - if (isReadyForAction()) { - mBinding.getData().downloadMessage(mBinding, messageId); - } else { - warnOfMissingActionConditions(false /*sending*/, - null /*commandToRunAfterActionConditionResolved*/); - } - } - - public void retrySend(final String messageId) { - if (isReadyForAction()) { - if (ensureKnownRecipients()) { - mBinding.getData().resendMessage(mBinding, messageId); - } - } else { - warnOfMissingActionConditions(true /*sending*/, - new Runnable() { - @Override - public void run() { - retrySend(messageId); - } - - }); - } - } - - void deleteMessage(final String messageId) { - if (isReadyForAction()) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.delete_message_confirmation_dialog_title) - .setMessage(R.string.delete_message_confirmation_dialog_text) - .setPositiveButton(R.string.delete_message_confirmation_button, - new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - mBinding.getData().deleteMessage(mBinding, messageId); - } - }) - .setNegativeButton(android.R.string.cancel, null); - if (OsUtil.isAtLeastJB_MR1()) { - builder.setOnDismissListener(new OnDismissListener() { - @Override - public void onDismiss(final DialogInterface dialog) { - mHost.dismissActionMode(); - } - }); - } else { - builder.setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(final DialogInterface dialog) { - mHost.dismissActionMode(); - } - }); - } - builder.create().show(); - } else { - warnOfMissingActionConditions(false /*sending*/, - null /*commandToRunAfterActionConditionResolved*/); - mHost.dismissActionMode(); - } - } - - public void deleteConversation() { - if (isReadyForAction()) { - final Context context = getActivity(); - mBinding.getData().deleteConversation(mBinding); - closeConversation(mConversationId); - } else { - warnOfMissingActionConditions(false /*sending*/, - null /*commandToRunAfterActionConditionResolved*/); - } - } - - @Override - public void closeConversation(final String conversationId) { - if (TextUtils.equals(conversationId, mConversationId)) { - mHost.onFinishCurrentConversation(); - // TODO: Explicitly transition to ConversationList (or just go back)? - } - } - - @Override - public void onConversationParticipantDataLoaded(final ConversationData data) { - mBinding.ensureBound(data); - if (mBinding.getData().getParticipantsLoaded()) { - final boolean oneOnOne = mBinding.getData().getOtherParticipant() != null; - mAdapter.setOneOnOne(oneOnOne, true /* invalidate */); - - // refresh the options menu which will enable the "people & options" item. - invalidateOptionsMenu(); - - mHost.invalidateActionBar(); - - mRecyclerView.setVisibility(View.VISIBLE); - mHost.onConversationParticipantDataLoaded - (mBinding.getData().getNumberOfParticipantsExcludingSelf()); - } - } - - @Override - public void onSubscriptionListDataLoaded(final ConversationData data) { - mBinding.ensureBound(data); - mAdapter.notifyDataSetChanged(); - } - - @Override - public void promptForSelfPhoneNumber() { - if (mComposeMessageView != null) { - // Avoid bug in system which puts soft keyboard over dialog after orientation change - ImeUtil.hideSoftInput(getActivity(), mComposeMessageView); - } - - final FragmentTransaction ft = getActivity().getFragmentManager().beginTransaction(); - final EnterSelfPhoneNumberDialog dialog = EnterSelfPhoneNumberDialog - .newInstance(getConversationSelfSubId()); - dialog.setTargetFragment(this, 0/*requestCode*/); - dialog.show(ft, null/*tag*/); - } - - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - if (mChangeDefaultSmsAppHelper == null) { - mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper(); - } - mChangeDefaultSmsAppHelper.handleChangeDefaultSmsResult(requestCode, resultCode, null); - } - - public boolean hasMessages() { - return mAdapter != null && mAdapter.getItemCount() > 0; - } - - public boolean onBackPressed() { - if (mComposeMessageView.onBackPressed()) { - return true; - } - return false; - } - - public boolean onNavigationUpPressed() { - return mComposeMessageView.onNavigationUpPressed(); - } - - @Override - public boolean onAttachmentClick(final ConversationMessageView messageView, - final MessagePartData attachment, final Rect imageBounds, final boolean longPress) { - if (longPress) { - selectMessage(messageView, attachment); - return true; - } else if (messageView.getData().getOneClickResendMessage()) { - handleMessageClick(messageView); - return true; - } - - if (attachment.isImage()) { - displayPhoto(attachment.getContentUri(), imageBounds, false /* isDraft */); - } - - if (attachment.isVCard()) { - UIIntents.get().launchVCardDetailActivity(getActivity(), attachment.getContentUri()); - } - - return false; - } - - private void handleMessageClick(final ConversationMessageView messageView) { - if (messageView != mSelectedMessage) { - final ConversationMessageData data = messageView.getData(); - final boolean isReadyToSend = isReadyForAction(); - if (data.getOneClickResendMessage()) { - // Directly resend the message on tap if it's failed - retrySend(data.getMessageId()); - selectMessage(null); - } else if (data.getShowResendMessage() && isReadyToSend) { - // Select the message to show the resend/download/delete options - selectMessage(messageView); - } else if (data.getShowDownloadMessage() && isReadyToSend) { - // Directly download the message on tap - retryDownload(data.getMessageId()); - } else { - // Let the toast from warnOfMissingActionConditions show and skip - // selecting - warnOfMissingActionConditions(false /*sending*/, - null /*commandToRunAfterActionConditionResolved*/); - selectMessage(null); - } - } else { - selectMessage(null); - } - } - - private static class AttachmentToSave { - public final Uri uri; - public final String contentType; - public Uri persistedUri; - - AttachmentToSave(final Uri uri, final String contentType) { - this.uri = uri; - this.contentType = contentType; - } - } - - public static class SaveAttachmentTask extends SafeAsyncTask<Void, Void, Void> { - private final Context mContext; - private final List<AttachmentToSave> mAttachmentsToSave = new ArrayList<>(); - - public SaveAttachmentTask(final Context context, final Uri contentUri, - final String contentType) { - mContext = context; - addAttachmentToSave(contentUri, contentType); - } - - public SaveAttachmentTask(final Context context) { - mContext = context; - } - - public void addAttachmentToSave(final Uri contentUri, final String contentType) { - mAttachmentsToSave.add(new AttachmentToSave(contentUri, contentType)); - } - - public int getAttachmentCount() { - return mAttachmentsToSave.size(); - } - - @Override - protected Void doInBackgroundTimed(final Void... arg) { - final File appDir = new File(Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES), - mContext.getResources().getString(R.string.app_name)); - final File downloadDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOWNLOADS); - for (final AttachmentToSave attachment : mAttachmentsToSave) { - final boolean isImageOrVideo = ContentType.isImageType(attachment.contentType) - || ContentType.isVideoType(attachment.contentType); - attachment.persistedUri = UriUtil.persistContent(attachment.uri, - isImageOrVideo ? appDir : downloadDir, attachment.contentType); - } - return null; - } - - @Override - protected void onPostExecute(final Void result) { - int failCount = 0; - int imageCount = 0; - int videoCount = 0; - int otherCount = 0; - for (final AttachmentToSave attachment : mAttachmentsToSave) { - if (attachment.persistedUri == null) { - failCount++; - continue; - } - - // Inform MediaScanner about the new file - final Intent scanFileIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - scanFileIntent.setData(attachment.persistedUri); - mContext.sendBroadcast(scanFileIntent); - - if (ContentType.isImageType(attachment.contentType)) { - imageCount++; - } else if (ContentType.isVideoType(attachment.contentType)) { - videoCount++; - } else { - otherCount++; - // Inform DownloadManager of the file so it will show in the "downloads" app - final DownloadManager downloadManager = - (DownloadManager) mContext.getSystemService( - Context.DOWNLOAD_SERVICE); - final String filePath = attachment.persistedUri.getPath(); - final File file = new File(filePath); - - if (file.exists()) { - downloadManager.addCompletedDownload( - file.getName() /* title */, - mContext.getString( - R.string.attachment_file_description) /* description */, - true /* isMediaScannerScannable */, - attachment.contentType, - file.getAbsolutePath(), - file.length(), - false /* showNotification */); - } - } - } - - String message; - if (failCount > 0) { - message = mContext.getResources().getQuantityString( - R.plurals.attachment_save_error, failCount, failCount); - } else { - int messageId = R.plurals.attachments_saved; - if (otherCount > 0) { - if (imageCount + videoCount == 0) { - messageId = R.plurals.attachments_saved_to_downloads; - } - } else { - if (videoCount == 0) { - messageId = R.plurals.photos_saved_to_album; - } else if (imageCount == 0) { - messageId = R.plurals.videos_saved_to_album; - } else { - messageId = R.plurals.attachments_saved_to_album; - } - } - final String appName = mContext.getResources().getString(R.string.app_name); - final int count = imageCount + videoCount + otherCount; - message = mContext.getResources().getQuantityString( - messageId, count, count, appName); - } - UiUtils.showToastAtBottom(message); - } - } - - private void invalidateOptionsMenu() { - final Activity activity = getActivity(); - // TODO: Add the supportInvalidateOptionsMenu call to the host activity. - if (activity == null || !(activity instanceof BugleActionBarActivity)) { - return; - } - ((BugleActionBarActivity) activity).supportInvalidateOptionsMenu(); - } - - @Override - public void setOptionsMenuVisibility(final boolean visible) { - setHasOptionsMenu(visible); - } - - @Override - public int getConversationSelfSubId() { - final String selfParticipantId = mComposeMessageView.getConversationSelfId(); - final ParticipantData self = mBinding.getData().getSelfParticipantById(selfParticipantId); - // If the self id or the self participant data hasn't been loaded yet, fallback to - // the default setting. - return self == null ? ParticipantData.DEFAULT_SELF_SUB_ID : self.getSubId(); - } - - @Override - public void invalidateActionBar() { - mHost.invalidateActionBar(); - } - - @Override - public void dismissActionMode() { - mHost.dismissActionMode(); - } - - @Override - public void selectSim(final SubscriptionListEntry subscriptionData) { - mComposeMessageView.selectSim(subscriptionData); - mHost.onStartComposeMessage(); - } - - @Override - public void onStartComposeMessage() { - mHost.onStartComposeMessage(); - } - - @Override - public SubscriptionListEntry getSubscriptionEntryForSelfParticipant( - final String selfParticipantId, final boolean excludeDefault) { - // TODO: ConversationMessageView is the only one using this. We should probably - // inject this into the view during binding in the ConversationMessageAdapter. - return mBinding.getData().getSubscriptionEntryForSelfParticipant(selfParticipantId, - excludeDefault); - } - - @Override - public SimSelectorView getSimSelectorView() { - return (SimSelectorView) getView().findViewById(R.id.sim_selector); - } - - @Override - public MediaPicker createMediaPicker() { - return new MediaPicker(getActivity()); - } - - @Override - public void notifyOfAttachmentLoadFailed() { - UiUtils.showToastAtBottom(R.string.attachment_load_failed_dialog_message); - } - - @Override - public void warnOfExceedingMessageLimit(final boolean sending, final boolean tooManyVideos) { - warnOfExceedingMessageLimit(sending, mComposeMessageView, mConversationId, - getActivity(), tooManyVideos); - } - - public static void warnOfExceedingMessageLimit(final boolean sending, - final ComposeMessageView composeMessageView, final String conversationId, - final Activity activity, final boolean tooManyVideos) { - final AlertDialog.Builder builder = - new AlertDialog.Builder(activity) - .setTitle(R.string.mms_attachment_limit_reached); - - if (sending) { - if (tooManyVideos) { - builder.setMessage(R.string.video_attachment_limit_exceeded_when_sending); - } else { - builder.setMessage(R.string.attachment_limit_reached_dialog_message_when_sending) - .setNegativeButton(R.string.attachment_limit_reached_send_anyway, - new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int which) { - composeMessageView.sendMessageIgnoreMessageSizeLimit(); - } - }); - } - builder.setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - showAttachmentChooser(conversationId, activity); - }}); - } else { - builder.setMessage(R.string.attachment_limit_reached_dialog_message_when_composing) - .setPositiveButton(android.R.string.ok, null); - } - builder.show(); - } - - @Override - public void showAttachmentChooser() { - showAttachmentChooser(mConversationId, getActivity()); - } - - public static void showAttachmentChooser(final String conversationId, - final Activity activity) { - UIIntents.get().launchAttachmentChooserActivity(activity, - conversationId, REQUEST_CHOOSE_ATTACHMENTS); - } - - private void updateActionAndStatusBarColor(final ActionBar actionBar) { - final int themeColor = ConversationDrawables.get().getConversationThemeColor(); - actionBar.setBackgroundDrawable(new ColorDrawable(themeColor)); - UiUtils.setStatusBarColor(getActivity(), themeColor); - } - - public void updateActionBar(final ActionBar actionBar) { - if (mComposeMessageView == null || !mComposeMessageView.updateActionBar(actionBar)) { - updateActionAndStatusBarColor(actionBar); - // We update this regardless of whether or not the action bar is showing so that we - // don't get a race when it reappears. - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - actionBar.setDisplayHomeAsUpEnabled(true); - // Reset the back arrow to its default - actionBar.setHomeAsUpIndicator(0); - View customView = actionBar.getCustomView(); - if (customView == null || customView.getId() != R.id.conversation_title_container) { - final LayoutInflater inflator = (LayoutInflater) - getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - customView = inflator.inflate(R.layout.action_bar_conversation_name, null); - customView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View v) { - onBackPressed(); - } - }); - actionBar.setCustomView(customView); - } - - final TextView conversationNameView = - (TextView) customView.findViewById(R.id.conversation_title); - final String conversationName = getConversationName(); - if (!TextUtils.isEmpty(conversationName)) { - // RTL : To format conversation title if it happens to be phone numbers. - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - final String formattedName = bidiFormatter.unicodeWrap( - UiUtils.commaEllipsize( - conversationName, - conversationNameView.getPaint(), - conversationNameView.getWidth(), - getString(R.string.plus_one), - getString(R.string.plus_n)).toString(), - TextDirectionHeuristicsCompat.LTR); - conversationNameView.setText(formattedName); - // In case phone numbers are mixed in the conversation name, we need to vocalize it. - final String vocalizedConversationName = - AccessibilityUtil.getVocalizedPhoneNumber(getResources(), conversationName); - conversationNameView.setContentDescription(vocalizedConversationName); - getActivity().setTitle(conversationName); - } else { - final String appName = getString(R.string.app_name); - conversationNameView.setText(appName); - getActivity().setTitle(appName); - } - - // When conversation is showing and media picker is not showing, then hide the action - // bar only when we are in landscape mode, with IME open. - if (mHost.isImeOpen() && UiUtils.isLandscapeMode()) { - actionBar.hide(); - } else { - actionBar.show(); - } - } - } - - @Override - public boolean shouldShowSubjectEditor() { - return true; - } - - @Override - public boolean shouldHideAttachmentsWhenSimSelectorShown() { - return false; - } - - @Override - public void showHideSimSelector(final boolean show) { - // no-op for now - } - - @Override - public int getSimSelectorItemLayoutId() { - return R.layout.sim_selector_item_view; - } - - @Override - public Uri getSelfSendButtonIconUri() { - return null; // use default button icon uri - } - - @Override - public int overrideCounterColor() { - return -1; // don't override the color - } - - @Override - public void onAttachmentsChanged(final boolean haveAttachments) { - // no-op for now - } - - @Override - public void onDraftChanged(final DraftMessageData data, final int changeFlags) { - mDraftMessageDataModel.ensureBound(data); - // We're specifically only interested in ATTACHMENTS_CHANGED from the widget. Ignore - // other changes. When the widget changes an attachment, we need to reload the draft. - if (changeFlags == - (DraftMessageData.WIDGET_CHANGED | DraftMessageData.ATTACHMENTS_CHANGED)) { - mClearLocalDraft = true; // force a reload of the draft in onResume - } - } - - @Override - public void onDraftAttachmentLimitReached(final DraftMessageData data) { - // no-op for now - } - - @Override - public void onDraftAttachmentLoadFailed() { - // no-op for now - } - - @Override - public int getAttachmentsClearedFlags() { - return DraftMessageData.ATTACHMENTS_CHANGED; - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationInput.java b/src/com/android/messaging/ui/conversation/ConversationInput.java deleted file mode 100644 index bf60aa8..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationInput.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.conversation; - -import android.os.Bundle; -import android.support.v7.app.ActionBar; - -/** - * The base class for a method of user input, e.g. media picker. - */ -public abstract class ConversationInput { - /** - * The host component where all input components are contained. This is typically the - * conversation fragment but may be mocked in test code. - */ - public interface ConversationInputBase { - boolean showHideInternal(final ConversationInput target, final boolean show, - final boolean animate); - String getInputStateKey(final ConversationInput input); - void beginUpdate(); - void handleOnShow(final ConversationInput target); - void endUpdate(); - } - - protected boolean mShowing; - protected ConversationInputBase mConversationInputBase; - - public abstract boolean show(boolean animate); - public abstract boolean hide(boolean animate); - - public ConversationInput(ConversationInputBase baseHost, final boolean isShowing) { - mConversationInputBase = baseHost; - mShowing = isShowing; - } - - public boolean onBackPressed() { - if (mShowing) { - mConversationInputBase.showHideInternal(this, false /* show */, true /* animate */); - return true; - } - return false; - } - - public boolean onNavigationUpPressed() { - return false; - } - - /** - * Toggle the visibility of this view. - * @param animate - * @return true if the view is now shown, false if it now hidden - */ - public boolean toggle(final boolean animate) { - mConversationInputBase.showHideInternal(this, !mShowing /* show */, true /* animate */); - return mShowing; - } - - public void saveState(final Bundle savedState) { - savedState.putBoolean(mConversationInputBase.getInputStateKey(this), mShowing); - } - - public void restoreState(final Bundle savedState) { - // Things are hidden by default, so only handle show. - if (savedState.getBoolean(mConversationInputBase.getInputStateKey(this))) { - mConversationInputBase.showHideInternal(this, true /* show */, false /* animate */); - } - } - - public boolean updateActionBar(final ActionBar actionBar) { - return false; - } - - /** - * Update our visibility flag in response to visibility change, both for actions - * initiated by this class (through the show/hide methods), and for external changes - * tracked by event listeners (e.g. ImeStateObserver, MediaPickerListener). As part of - * handling an input showing, we will hide all other inputs to ensure they are mutually - * exclusive. - */ - protected void onVisibilityChanged(final boolean visible) { - if (mShowing != visible) { - mConversationInputBase.beginUpdate(); - mShowing = visible; - if (visible) { - mConversationInputBase.handleOnShow(this); - } - mConversationInputBase.endUpdate(); - } - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationInputManager.java b/src/com/android/messaging/ui/conversation/ConversationInputManager.java deleted file mode 100644 index e10abe7..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationInputManager.java +++ /dev/null @@ -1,550 +0,0 @@ -/* - * 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.conversation; - -import android.app.FragmentManager; -import android.content.Context; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.widget.EditText; - -import com.android.messaging.R; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.binding.ImmutableBindingRef; -import com.android.messaging.datamodel.data.ConversationData; -import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener; -import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener; -import com.android.messaging.datamodel.data.DraftMessageData; -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.PendingAttachmentData; -import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; -import com.android.messaging.ui.ConversationDrawables; -import com.android.messaging.ui.mediapicker.MediaPicker; -import com.android.messaging.ui.mediapicker.MediaPicker.MediaPickerListener; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ImeUtil; -import com.android.messaging.util.ImeUtil.ImeStateHost; -import com.google.common.annotations.VisibleForTesting; - -import java.util.Collection; - -/** - * Manages showing/hiding/persisting different mutually exclusive UI components nested in - * ConversationFragment that take user inputs, i.e. media picker, SIM selector and - * IME keyboard (the IME keyboard is not owned by Bugle, but we try to model it the same way - * as the other components). - */ -public class ConversationInputManager implements ConversationInput.ConversationInputBase { - /** - * The host component where all input components are contained. This is typically the - * conversation fragment but may be mocked in test code. - */ - public interface ConversationInputHost extends DraftMessageSubscriptionDataProvider { - void invalidateActionBar(); - void setOptionsMenuVisibility(boolean visible); - void dismissActionMode(); - void selectSim(SubscriptionListEntry subscriptionData); - void onStartComposeMessage(); - SimSelectorView getSimSelectorView(); - MediaPicker createMediaPicker(); - void showHideSimSelector(boolean show); - int getSimSelectorItemLayoutId(); - } - - /** - * The "sink" component where all inputs components will direct the user inputs to. This is - * typically the ComposeMessageView but may be mocked in test code. - */ - public interface ConversationInputSink { - void onMediaItemsSelected(Collection<MessagePartData> items); - void onMediaItemsUnselected(MessagePartData item); - void onPendingAttachmentAdded(PendingAttachmentData pendingItem); - void resumeComposeMessage(); - EditText getComposeEditText(); - void setAccessibility(boolean enabled); - } - - private final ConversationInputHost mHost; - private final ConversationInputSink mSink; - - /** Dependencies injected from the host during construction */ - private final FragmentManager mFragmentManager; - private final Context mContext; - private final ImeStateHost mImeStateHost; - private final ImmutableBindingRef<ConversationData> mConversationDataModel; - private final ImmutableBindingRef<DraftMessageData> mDraftDataModel; - - private final ConversationInput[] mInputs; - private final ConversationMediaPicker mMediaInput; - private final ConversationSimSelector mSimInput; - private final ConversationImeKeyboard mImeInput; - private int mUpdateCount; - - private final ImeUtil.ImeStateObserver mImeStateObserver = new ImeUtil.ImeStateObserver() { - @Override - public void onImeStateChanged(final boolean imeOpen) { - mImeInput.onVisibilityChanged(imeOpen); - } - }; - - private final ConversationDataListener mDataListener = new SimpleConversationDataListener() { - @Override - public void onConversationParticipantDataLoaded(ConversationData data) { - mConversationDataModel.ensureBound(data); - } - - @Override - public void onSubscriptionListDataLoaded(ConversationData data) { - mConversationDataModel.ensureBound(data); - mSimInput.onSubscriptionListDataLoaded(data.getSubscriptionListData()); - } - }; - - public ConversationInputManager( - final Context context, - final ConversationInputHost host, - final ConversationInputSink sink, - final ImeStateHost imeStateHost, - final FragmentManager fm, - final BindingBase<ConversationData> conversationDataModel, - final BindingBase<DraftMessageData> draftDataModel, - final Bundle savedState) { - mHost = host; - mSink = sink; - mFragmentManager = fm; - mContext = context; - mImeStateHost = imeStateHost; - mConversationDataModel = BindingBase.createBindingReference(conversationDataModel); - mDraftDataModel = BindingBase.createBindingReference(draftDataModel); - - // Register listeners on dependencies. - mImeStateHost.registerImeStateObserver(mImeStateObserver); - mConversationDataModel.getData().addConversationDataListener(mDataListener); - - // Initialize the inputs - mMediaInput = new ConversationMediaPicker(this); - mSimInput = new SimSelector(this); - mImeInput = new ConversationImeKeyboard(this, mImeStateHost.isImeOpen()); - mInputs = new ConversationInput[] { mMediaInput, mSimInput, mImeInput }; - - if (savedState != null) { - for (int i = 0; i < mInputs.length; i++) { - mInputs[i].restoreState(savedState); - } - } - updateHostOptionsMenu(); - } - - public void onDetach() { - mImeStateHost.unregisterImeStateObserver(mImeStateObserver); - // Don't need to explicitly unregister for data model events. It will unregister all - // listeners automagically on unbind. - } - - public void onSaveInputState(final Bundle savedState) { - for (int i = 0; i < mInputs.length; i++) { - mInputs[i].saveState(savedState); - } - } - - @Override - public String getInputStateKey(final ConversationInput input) { - return input.getClass().getCanonicalName() + "_savedstate_"; - } - - public boolean onBackPressed() { - for (int i = 0; i < mInputs.length; i++) { - if (mInputs[i].onBackPressed()) { - return true; - } - } - return false; - } - - public boolean onNavigationUpPressed() { - for (int i = 0; i < mInputs.length; i++) { - if (mInputs[i].onNavigationUpPressed()) { - return true; - } - } - return false; - } - - public void resetMediaPickerState() { - mMediaInput.resetViewHolderState(); - } - - public void showHideMediaPicker(final boolean show, final boolean animate) { - showHideInternal(mMediaInput, show, animate); - } - - /** - * Show or hide the sim selector - * @param show visibility - * @param animate whether to animate the change in visibility - * @return true if the state of the visibility was changed - */ - public boolean showHideSimSelector(final boolean show, final boolean animate) { - return showHideInternal(mSimInput, show, animate); - } - - public void showHideImeKeyboard(final boolean show, final boolean animate) { - showHideInternal(mImeInput, show, animate); - } - - public void hideAllInputs(final boolean animate) { - beginUpdate(); - for (int i = 0; i < mInputs.length; i++) { - showHideInternal(mInputs[i], false, animate); - } - endUpdate(); - } - - /** - * Toggle the visibility of the sim selector. - * @param animate - * @param subEntry - * @return true if the view is now shown, false if it now hidden - */ - public boolean toggleSimSelector(final boolean animate, final SubscriptionListEntry subEntry) { - mSimInput.setSelected(subEntry); - return mSimInput.toggle(animate); - } - - public boolean updateActionBar(final ActionBar actionBar) { - for (int i = 0; i < mInputs.length; i++) { - if (mInputs[i].mShowing) { - return mInputs[i].updateActionBar(actionBar); - } - } - return false; - } - - @VisibleForTesting - boolean isMediaPickerVisible() { - return mMediaInput.mShowing; - } - - @VisibleForTesting - boolean isSimSelectorVisible() { - return mSimInput.mShowing; - } - - @VisibleForTesting - boolean isImeKeyboardVisible() { - return mImeInput.mShowing; - } - - @VisibleForTesting - void testNotifyImeStateChanged(final boolean imeOpen) { - mImeStateObserver.onImeStateChanged(imeOpen); - } - - /** - * returns true if the state of the visibility was actually changed - */ - @Override - public boolean showHideInternal(final ConversationInput target, final boolean show, - final boolean animate) { - if (!mConversationDataModel.isBound()) { - return false; - } - - if (target.mShowing == show) { - return false; - } - beginUpdate(); - boolean success; - if (!show) { - success = target.hide(animate); - } else { - success = target.show(animate); - } - - if (success) { - target.onVisibilityChanged(show); - } - endUpdate(); - return true; - } - - @Override - public void handleOnShow(final ConversationInput target) { - if (!mConversationDataModel.isBound()) { - return; - } - beginUpdate(); - - // All inputs are mutually exclusive. Showing one will hide everything else. - // The one exception, is that the keyboard and location media chooser can be open at the - // time to enable searching within that chooser - for (int i = 0; i < mInputs.length; i++) { - final ConversationInput currInput = mInputs[i]; - if (currInput != target) { - // TODO : If there's more exceptions we will want to make this more - // generic - if (currInput instanceof ConversationMediaPicker && - target instanceof ConversationImeKeyboard && - mMediaInput.getExistingOrCreateMediaPicker() != null && - mMediaInput.getExistingOrCreateMediaPicker().canShowIme()) { - // Allow the keyboard and location mediaPicker to be open at the same time, - // but ensure the media picker is full screen to allow enough room - mMediaInput.getExistingOrCreateMediaPicker().setFullScreen(true); - continue; - } - showHideInternal(currInput, false /* show */, false /* animate */); - } - } - // Always dismiss action mode on show. - mHost.dismissActionMode(); - // Invoking any non-keyboard input UI is treated as starting message compose. - if (target != mImeInput) { - mHost.onStartComposeMessage(); - } - endUpdate(); - } - - @Override - public void beginUpdate() { - mUpdateCount++; - } - - @Override - public void endUpdate() { - Assert.isTrue(mUpdateCount > 0); - if (--mUpdateCount == 0) { - // Always try to update the host action bar after every update cycle. - mHost.invalidateActionBar(); - } - } - - private void updateHostOptionsMenu() { - mHost.setOptionsMenuVisibility(!mMediaInput.isOpen()); - } - - /** - * Manages showing/hiding the media picker in conversation. - */ - private class ConversationMediaPicker extends ConversationInput { - public ConversationMediaPicker(ConversationInputBase baseHost) { - super(baseHost, false); - } - - private MediaPicker mMediaPicker; - - @Override - public boolean show(boolean animate) { - if (mMediaPicker == null) { - mMediaPicker = getExistingOrCreateMediaPicker(); - setConversationThemeColor(ConversationDrawables.get().getConversationThemeColor()); - mMediaPicker.setSubscriptionDataProvider(mHost); - mMediaPicker.setDraftMessageDataModel(mDraftDataModel); - mMediaPicker.setListener(new MediaPickerListener() { - @Override - public void onOpened() { - handleStateChange(); - } - - @Override - public void onFullScreenChanged(boolean fullScreen) { - // When we're full screen, we want to disable accessibility on the - // ComposeMessageView controls (attach button, message input, sim chooser) - // that are hiding underneath the action bar. - mSink.setAccessibility(!fullScreen /*enabled*/); - handleStateChange(); - } - - @Override - public void onDismissed() { - // Re-enable accessibility on all controls now that the media picker is - // going away. - mSink.setAccessibility(true /*enabled*/); - handleStateChange(); - } - - private void handleStateChange() { - onVisibilityChanged(isOpen()); - mHost.invalidateActionBar(); - updateHostOptionsMenu(); - } - - @Override - public void onItemsSelected(final Collection<MessagePartData> items, - final boolean resumeCompose) { - mSink.onMediaItemsSelected(items); - mHost.invalidateActionBar(); - if (resumeCompose) { - mSink.resumeComposeMessage(); - } - } - - @Override - public void onItemUnselected(final MessagePartData item) { - mSink.onMediaItemsUnselected(item); - mHost.invalidateActionBar(); - } - - @Override - public void onConfirmItemSelection() { - mSink.resumeComposeMessage(); - } - - @Override - public void onPendingItemAdded(final PendingAttachmentData pendingItem) { - mSink.onPendingAttachmentAdded(pendingItem); - } - - @Override - public void onChooserSelected(final int chooserIndex) { - mHost.invalidateActionBar(); - mHost.dismissActionMode(); - } - }); - } - - mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate); - - return isOpen(); - } - - @Override - public boolean hide(boolean animate) { - if (mMediaPicker != null) { - mMediaPicker.dismiss(animate); - } - return !isOpen(); - } - - public void resetViewHolderState() { - if (mMediaPicker != null) { - mMediaPicker.resetViewHolderState(); - } - } - - public void setConversationThemeColor(final int themeColor) { - if (mMediaPicker != null) { - mMediaPicker.setConversationThemeColor(themeColor); - } - } - - private boolean isOpen() { - return (mMediaPicker != null && mMediaPicker.isOpen()); - } - - private MediaPicker getExistingOrCreateMediaPicker() { - if (mMediaPicker != null) { - return mMediaPicker; - } - MediaPicker mediaPicker = (MediaPicker) - mFragmentManager.findFragmentByTag(MediaPicker.FRAGMENT_TAG); - if (mediaPicker == null) { - mediaPicker = mHost.createMediaPicker(); - if (mediaPicker == null) { - return null; // this use of ComposeMessageView doesn't support media picking - } - mFragmentManager.beginTransaction().replace( - R.id.mediapicker_container, - mediaPicker, - MediaPicker.FRAGMENT_TAG).commit(); - } - return mediaPicker; - } - - @Override - public boolean updateActionBar(ActionBar actionBar) { - if (isOpen()) { - mMediaPicker.updateActionBar(actionBar); - return true; - } - return false; - } - - @Override - public boolean onNavigationUpPressed() { - if (isOpen() && mMediaPicker.isFullScreen()) { - return onBackPressed(); - } - return super.onNavigationUpPressed(); - } - - public boolean onBackPressed() { - if (mMediaPicker != null && mMediaPicker.onBackPressed()) { - return true; - } - return super.onBackPressed(); - } - } - - /** - * Manages showing/hiding the SIM selector in conversation. - */ - private class SimSelector extends ConversationSimSelector { - public SimSelector(ConversationInputBase baseHost) { - super(baseHost); - } - - @Override - protected SimSelectorView getSimSelectorView() { - return mHost.getSimSelectorView(); - } - - @Override - public int getSimSelectorItemLayoutId() { - return mHost.getSimSelectorItemLayoutId(); - } - - @Override - protected void selectSim(SubscriptionListEntry item) { - mHost.selectSim(item); - } - - @Override - public boolean show(boolean animate) { - final boolean result = super.show(animate); - mHost.showHideSimSelector(true /*show*/); - return result; - } - - @Override - public boolean hide(boolean animate) { - final boolean result = super.hide(animate); - mHost.showHideSimSelector(false /*show*/); - return result; - } - } - - /** - * Manages showing/hiding the IME keyboard in conversation. - */ - private class ConversationImeKeyboard extends ConversationInput { - public ConversationImeKeyboard(ConversationInputBase baseHost, final boolean isShowing) { - super(baseHost, isShowing); - } - - @Override - public boolean show(boolean animate) { - ImeUtil.get().showImeKeyboard(mContext, mSink.getComposeEditText()); - return true; - } - - @Override - public boolean hide(boolean animate) { - ImeUtil.get().hideImeKeyboard(mContext, mSink.getComposeEditText()); - return true; - } - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationMessageAdapter.java b/src/com/android/messaging/ui/conversation/ConversationMessageAdapter.java deleted file mode 100644 index 2748fff..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationMessageAdapter.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.database.Cursor; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.messaging.R; -import com.android.messaging.ui.AsyncImageView; -import com.android.messaging.ui.CursorRecyclerAdapter; -import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader; -import com.android.messaging.ui.conversation.ConversationMessageView.ConversationMessageViewHost; -import com.android.messaging.util.Assert; - -import java.util.HashSet; -import java.util.List; - -/** - * Provides an interface to expose Conversation Message Cursor data to a UI widget like a - * RecyclerView. - */ -public class ConversationMessageAdapter extends - CursorRecyclerAdapter<ConversationMessageAdapter.ConversationMessageViewHolder> { - - private final ConversationMessageViewHost mHost; - private final AsyncImageViewDelayLoader mImageViewDelayLoader; - private final View.OnClickListener mViewClickListener; - private final View.OnLongClickListener mViewLongClickListener; - private boolean mOneOnOne; - private String mSelectedMessageId; - - public ConversationMessageAdapter(final Context context, final Cursor cursor, - final ConversationMessageViewHost host, - final AsyncImageViewDelayLoader imageViewDelayLoader, - final View.OnClickListener viewClickListener, - final View.OnLongClickListener longClickListener) { - super(context, cursor, 0); - mHost = host; - mViewClickListener = viewClickListener; - mViewLongClickListener = longClickListener; - mImageViewDelayLoader = imageViewDelayLoader; - setHasStableIds(true); - } - - @Override - public void bindViewHolder(final ConversationMessageViewHolder holder, - final Context context, final Cursor cursor) { - Assert.isTrue(holder.mView instanceof ConversationMessageView); - final ConversationMessageView conversationMessageView = - (ConversationMessageView) holder.mView; - conversationMessageView.bind(cursor, mOneOnOne, mSelectedMessageId); - } - - @Override - public ConversationMessageViewHolder createViewHolder(final Context context, - final ViewGroup parent, final int viewType) { - final LayoutInflater layoutInflater = LayoutInflater.from(context); - final ConversationMessageView conversationMessageView = (ConversationMessageView) - layoutInflater.inflate(R.layout.conversation_message_view, null); - conversationMessageView.setHost(mHost); - conversationMessageView.setImageViewDelayLoader(mImageViewDelayLoader); - return new ConversationMessageViewHolder(conversationMessageView, - mViewClickListener, mViewLongClickListener); - } - - public void setSelectedMessage(final String messageId) { - mSelectedMessageId = messageId; - notifyDataSetChanged(); - } - - public void setOneOnOne(final boolean oneOnOne, final boolean invalidate) { - if (mOneOnOne != oneOnOne) { - mOneOnOne = oneOnOne; - if (invalidate) { - notifyDataSetChanged(); - } - } - } - - /** - * ViewHolder that holds a ConversationMessageView. - */ - public static class ConversationMessageViewHolder extends RecyclerView.ViewHolder { - final View mView; - - /** - * @param viewClickListener a View.OnClickListener that should define the interaction when - * an item in the RecyclerView is clicked. - */ - public ConversationMessageViewHolder(final View itemView, - final View.OnClickListener viewClickListener, - final View.OnLongClickListener viewLongClickListener) { - super(itemView); - mView = itemView; - - mView.setOnClickListener(viewClickListener); - mView.setOnLongClickListener(viewLongClickListener); - } - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationMessageBubbleView.java b/src/com/android/messaging/ui/conversation/ConversationMessageBubbleView.java deleted file mode 100644 index ef6aeb4..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationMessageBubbleView.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.conversation; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import com.android.messaging.R; -import com.android.messaging.annotation.VisibleForAnimation; -import com.android.messaging.datamodel.data.ConversationMessageBubbleData; -import com.android.messaging.datamodel.data.ConversationMessageData; -import com.android.messaging.util.UiUtils; - -/** - * Shows the message bubble for one conversation message. It is able to animate size changes - * by morphing when the message content changes size. - */ -// TODO: Move functionality from ConversationMessageView into this class as appropriate -public class ConversationMessageBubbleView extends LinearLayout { - private int mIntrinsicWidth; - private int mMorphedWidth; - private ObjectAnimator mAnimator; - private boolean mShouldAnimateWidthChange; - private final ConversationMessageBubbleData mData; - private int mRunningStartWidth; - private ViewGroup mBubbleBackground; - - public ConversationMessageBubbleView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mData = new ConversationMessageBubbleData(); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mBubbleBackground = (ViewGroup) findViewById(R.id.message_text_and_info); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - final int newIntrinsicWidth = getMeasuredWidth(); - if (mIntrinsicWidth == 0 && newIntrinsicWidth != mIntrinsicWidth) { - if (mShouldAnimateWidthChange) { - kickOffMorphAnimation(mIntrinsicWidth, newIntrinsicWidth); - } - mIntrinsicWidth = newIntrinsicWidth; - } - - if (mMorphedWidth > 0) { - mBubbleBackground.getLayoutParams().width = mMorphedWidth; - } else { - mBubbleBackground.getLayoutParams().width = LayoutParams.WRAP_CONTENT; - } - mBubbleBackground.requestLayout(); - } - - @VisibleForAnimation - public void setMorphWidth(final int width) { - mMorphedWidth = width; - requestLayout(); - } - - public void bind(final ConversationMessageData data) { - final boolean changed = mData.bind(data); - // Animate width change only when we are binding to the same message, so that we may - // animate view size changes on the same message bubble due to things like status text - // change. - // Don't animate width change when the bubble contains attachments. Width animation is - // only suitable for text-only messages (where the bubble size change due to status or - // time stamp changes). - mShouldAnimateWidthChange = !changed && !data.hasAttachments(); - if (mAnimator == null) { - mMorphedWidth = 0; - } - } - - public void kickOffMorphAnimation(final int oldWidth, final int newWidth) { - if (mAnimator != null) { - mAnimator.setIntValues(mRunningStartWidth, newWidth); - return; - } - mRunningStartWidth = oldWidth; - mAnimator = ObjectAnimator.ofInt(this, "morphWidth", oldWidth, newWidth); - mAnimator.setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION); - mAnimator.addListener(new AnimatorListener() { - @Override - public void onAnimationStart(Animator animator) { - } - - @Override - public void onAnimationEnd(Animator animator) { - mAnimator = null; - mMorphedWidth = 0; - // Allow the bubble to resize if, for example, the status text changed during - // the animation. This will snap to the bigger size if needed. This is intentional - // as animating immediately after looks really bad and switching layout params - // during the original animation does not achieve the desired effect. - mBubbleBackground.getLayoutParams().width = LayoutParams.WRAP_CONTENT; - mBubbleBackground.requestLayout(); - } - - @Override - public void onAnimationCancel(Animator animator) { - } - - @Override - public void onAnimationRepeat(Animator animator) { - } - }); - mAnimator.start(); - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationMessageView.java b/src/com/android/messaging/ui/conversation/ConversationMessageView.java deleted file mode 100644 index e22e2c7..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationMessageView.java +++ /dev/null @@ -1,1206 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.format.Formatter; -import android.text.style.URLSpan; -import android.text.util.Linkify; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.FrameLayout; -import android.widget.ImageView.ScaleType; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.data.ConversationMessageData; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; -import com.android.messaging.datamodel.media.ImageRequestDescriptor; -import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor; -import com.android.messaging.datamodel.media.UriImageRequestDescriptor; -import com.android.messaging.sms.MmsUtils; -import com.android.messaging.ui.AsyncImageView; -import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader; -import com.android.messaging.ui.AudioAttachmentView; -import com.android.messaging.ui.ContactIconView; -import com.android.messaging.ui.ConversationDrawables; -import com.android.messaging.ui.MultiAttachmentLayout; -import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener; -import com.android.messaging.ui.PersonItemView; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.VideoThumbnailView; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.Assert; -import com.android.messaging.util.AvatarUriUtil; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.ImageUtils; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.UiUtils; -import com.android.messaging.util.YouTubeUtil; -import com.google.common.base.Predicate; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * The view for a single entry in a conversation. - */ -public class ConversationMessageView extends FrameLayout implements View.OnClickListener, - View.OnLongClickListener, OnAttachmentClickListener { - public interface ConversationMessageViewHost { - boolean onAttachmentClick(ConversationMessageView view, MessagePartData attachment, - Rect imageBounds, boolean longPress); - SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId, - boolean excludeDefault); - } - - private final ConversationMessageData mData; - - private LinearLayout mMessageAttachmentsView; - private MultiAttachmentLayout mMultiAttachmentView; - private AsyncImageView mMessageImageView; - private TextView mMessageTextView; - private boolean mMessageTextHasLinks; - private boolean mMessageHasYouTubeLink; - private TextView mStatusTextView; - private TextView mTitleTextView; - private TextView mMmsInfoTextView; - private LinearLayout mMessageTitleLayout; - private TextView mSenderNameTextView; - private ContactIconView mContactIconView; - private ConversationMessageBubbleView mMessageBubble; - private View mSubjectView; - private TextView mSubjectLabel; - private TextView mSubjectText; - private View mDeliveredBadge; - private ViewGroup mMessageMetadataView; - private ViewGroup mMessageTextAndInfoView; - private TextView mSimNameView; - - private boolean mOneOnOne; - private ConversationMessageViewHost mHost; - - public ConversationMessageView(final Context context, final AttributeSet attrs) { - super(context, attrs); - // TODO: we should switch to using Binding and DataModel factory methods. - mData = new ConversationMessageData(); - } - - @Override - protected void onFinishInflate() { - mContactIconView = (ContactIconView) findViewById(R.id.conversation_icon); - mContactIconView.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(final View view) { - ConversationMessageView.this.performLongClick(); - return true; - } - }); - - mMessageAttachmentsView = (LinearLayout) findViewById(R.id.message_attachments); - mMultiAttachmentView = (MultiAttachmentLayout) findViewById(R.id.multiple_attachments); - mMultiAttachmentView.setOnAttachmentClickListener(this); - - mMessageImageView = (AsyncImageView) findViewById(R.id.message_image); - mMessageImageView.setOnClickListener(this); - mMessageImageView.setOnLongClickListener(this); - - mMessageTextView = (TextView) findViewById(R.id.message_text); - mMessageTextView.setOnClickListener(this); - IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this); - - mStatusTextView = (TextView) findViewById(R.id.message_status); - mTitleTextView = (TextView) findViewById(R.id.message_title); - mMmsInfoTextView = (TextView) findViewById(R.id.mms_info); - mMessageTitleLayout = (LinearLayout) findViewById(R.id.message_title_layout); - mSenderNameTextView = (TextView) findViewById(R.id.message_sender_name); - mMessageBubble = (ConversationMessageBubbleView) findViewById(R.id.message_content); - mSubjectView = findViewById(R.id.subject_container); - mSubjectLabel = (TextView) mSubjectView.findViewById(R.id.subject_label); - mSubjectText = (TextView) mSubjectView.findViewById(R.id.subject_text); - mDeliveredBadge = findViewById(R.id.smsDeliveredBadge); - mMessageMetadataView = (ViewGroup) findViewById(R.id.message_metadata); - mMessageTextAndInfoView = (ViewGroup) findViewById(R.id.message_text_and_info); - mSimNameView = (TextView) findViewById(R.id.sim_name); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - final int horizontalSpace = MeasureSpec.getSize(widthMeasureSpec); - final int iconSize = getResources() - .getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size); - - final int unspecifiedMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - final int iconMeasureSpec = MeasureSpec.makeMeasureSpec(iconSize, MeasureSpec.EXACTLY); - - mContactIconView.measure(iconMeasureSpec, iconMeasureSpec); - - final int arrowWidth = - getResources().getDimensionPixelSize(R.dimen.message_bubble_arrow_width); - - // We need to subtract contact icon width twice from the horizontal space to get - // the max leftover space because we want the message bubble to extend no further than the - // starting position of the message bubble in the opposite direction. - final int maxLeftoverSpace = horizontalSpace - mContactIconView.getMeasuredWidth() * 2 - - arrowWidth - getPaddingLeft() - getPaddingRight(); - final int messageContentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxLeftoverSpace, - MeasureSpec.AT_MOST); - - mMessageBubble.measure(messageContentWidthMeasureSpec, unspecifiedMeasureSpec); - - final int maxHeight = Math.max(mContactIconView.getMeasuredHeight(), - mMessageBubble.getMeasuredHeight()); - setMeasuredDimension(horizontalSpace, maxHeight + getPaddingBottom() + getPaddingTop()); - } - - @Override - protected void onLayout(final boolean changed, final int left, final int top, final int right, - final int bottom) { - final boolean isRtl = AccessibilityUtil.isLayoutRtl(this); - - final int iconWidth = mContactIconView.getMeasuredWidth(); - final int iconHeight = mContactIconView.getMeasuredHeight(); - final int iconTop = getPaddingTop(); - final int contentWidth = (right -left) - iconWidth - getPaddingLeft() - getPaddingRight(); - final int contentHeight = mMessageBubble.getMeasuredHeight(); - final int contentTop = iconTop; - - final int iconLeft; - final int contentLeft; - if (mData.getIsIncoming()) { - if (isRtl) { - iconLeft = (right - left) - getPaddingRight() - iconWidth; - contentLeft = iconLeft - contentWidth; - } else { - iconLeft = getPaddingLeft(); - contentLeft = iconLeft + iconWidth; - } - } else { - if (isRtl) { - iconLeft = getPaddingLeft(); - contentLeft = iconLeft + iconWidth; - } else { - iconLeft = (right - left) - getPaddingRight() - iconWidth; - contentLeft = iconLeft - contentWidth; - } - } - - mContactIconView.layout(iconLeft, iconTop, iconLeft + iconWidth, iconTop + iconHeight); - - mMessageBubble.layout(contentLeft, contentTop, contentLeft + contentWidth, - contentTop + contentHeight); - } - - /** - * Fills in the data associated with this view. - * - * @param cursor The cursor from a MessageList that this view is in, pointing to its entry. - */ - public void bind(final Cursor cursor) { - bind(cursor, true, null); - } - - /** - * Fills in the data associated with this view. - * - * @param cursor The cursor from a MessageList that this view is in, pointing to its entry. - * @param oneOnOne Whether this is a 1:1 conversation - */ - public void bind(final Cursor cursor, - final boolean oneOnOne, final String selectedMessageId) { - mOneOnOne = oneOnOne; - - // Update our UI model - mData.bind(cursor); - setSelected(TextUtils.equals(mData.getMessageId(), selectedMessageId)); - - // Update text and image content for the view. - updateViewContent(); - - // Update colors and layout parameters for the view. - updateViewAppearance(); - - updateContentDescription(); - } - - public void setHost(final ConversationMessageViewHost host) { - mHost = host; - } - - /** - * Sets a delay loader instance to manage loading / resuming of image attachments. - */ - public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) { - Assert.notNull(mMessageImageView); - mMessageImageView.setDelayLoader(delayLoader); - mMultiAttachmentView.setImageViewDelayLoader(delayLoader); - } - - public ConversationMessageData getData() { - return mData; - } - - /** - * Returns whether we should show simplified visual style for the message view (i.e. hide the - * avatar and bubble arrow, reduce padding). - */ - private boolean shouldShowSimplifiedVisualStyle() { - return mData.getCanClusterWithPreviousMessage(); - } - - /** - * Returns whether we need to show message bubble arrow. We don't show arrow if the message - * contains media attachments or if shouldShowSimplifiedVisualStyle() is true. - */ - private boolean shouldShowMessageBubbleArrow() { - return !shouldShowSimplifiedVisualStyle() - && !(mData.hasAttachments() || mMessageHasYouTubeLink); - } - - /** - * Returns whether we need to show a message bubble for text content. - */ - private boolean shouldShowMessageTextBubble() { - if (mData.hasText()) { - return true; - } - final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), - mData.getMmsSubject()); - if (!TextUtils.isEmpty(subjectText)) { - return true; - } - return false; - } - - private void updateViewContent() { - updateMessageContent(); - int titleResId = -1; - int statusResId = -1; - String statusText = null; - switch(mData.getStatus()) { - case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: - case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: - case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: - case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: - titleResId = R.string.message_title_downloading; - statusResId = R.string.message_status_downloading; - break; - - case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: - if (!OsUtil.isSecondaryUser()) { - titleResId = R.string.message_title_manual_download; - if (isSelected()) { - statusResId = R.string.message_status_download_action; - } else { - statusResId = R.string.message_status_download; - } - } - break; - - case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: - if (!OsUtil.isSecondaryUser()) { - titleResId = R.string.message_title_download_failed; - statusResId = R.string.message_status_download_error; - } - break; - - case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: - if (!OsUtil.isSecondaryUser()) { - titleResId = R.string.message_title_download_failed; - if (isSelected()) { - statusResId = R.string.message_status_download_action; - } else { - statusResId = R.string.message_status_download; - } - } - break; - - case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: - case MessageData.BUGLE_STATUS_OUTGOING_SENDING: - statusResId = R.string.message_status_sending; - break; - - case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: - case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: - statusResId = R.string.message_status_send_retrying; - break; - - case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: - statusResId = R.string.message_status_send_failed_emergency_number; - break; - - case MessageData.BUGLE_STATUS_OUTGOING_FAILED: - // don't show the error state unless we're the default sms app - if (PhoneUtils.getDefault().isDefaultSmsApp()) { - if (isSelected()) { - statusResId = R.string.message_status_resend; - } else { - statusResId = MmsUtils.mapRawStatusToErrorResourceId( - mData.getStatus(), mData.getRawTelephonyStatus()); - } - break; - } - // FALL THROUGH HERE - - case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: - case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: - default: - if (!mData.getCanClusterWithNextMessage()) { - statusText = mData.getFormattedReceivedTimeStamp(); - } - break; - } - - final boolean titleVisible = (titleResId >= 0); - if (titleVisible) { - final String titleText = getResources().getString(titleResId); - mTitleTextView.setText(titleText); - - final String mmsInfoText = getResources().getString( - R.string.mms_info, - Formatter.formatFileSize(getContext(), mData.getSmsMessageSize()), - DateUtils.formatDateTime( - getContext(), - mData.getMmsExpiry(), - DateUtils.FORMAT_SHOW_DATE | - DateUtils.FORMAT_SHOW_TIME | - DateUtils.FORMAT_NUMERIC_DATE | - DateUtils.FORMAT_NO_YEAR)); - mMmsInfoTextView.setText(mmsInfoText); - mMessageTitleLayout.setVisibility(View.VISIBLE); - } else { - mMessageTitleLayout.setVisibility(View.GONE); - } - - final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), - mData.getMmsSubject()); - final boolean subjectVisible = !TextUtils.isEmpty(subjectText); - - final boolean senderNameVisible = !mOneOnOne && !mData.getCanClusterWithNextMessage() - && mData.getIsIncoming(); - if (senderNameVisible) { - mSenderNameTextView.setText(mData.getSenderDisplayName()); - mSenderNameTextView.setVisibility(View.VISIBLE); - } else { - mSenderNameTextView.setVisibility(View.GONE); - } - - if (statusResId >= 0) { - statusText = getResources().getString(statusResId); - } - - // We set the text even if the view will be GONE for accessibility - mStatusTextView.setText(statusText); - final boolean statusVisible = !TextUtils.isEmpty(statusText); - if (statusVisible) { - mStatusTextView.setVisibility(View.VISIBLE); - } else { - mStatusTextView.setVisibility(View.GONE); - } - - final boolean deliveredBadgeVisible = - mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED; - mDeliveredBadge.setVisibility(deliveredBadgeVisible ? View.VISIBLE : View.GONE); - - // Update the sim indicator. - final boolean showSimIconAsIncoming = mData.getIsIncoming() && - (!mData.hasAttachments() || shouldShowMessageTextBubble()); - final SubscriptionListEntry subscriptionEntry = - mHost.getSubscriptionEntryForSelfParticipant(mData.getSelfParticipantId(), - true /* excludeDefault */); - final boolean simNameVisible = subscriptionEntry != null && - !TextUtils.isEmpty(subscriptionEntry.displayName) && - !mData.getCanClusterWithNextMessage(); - if (simNameVisible) { - final String simNameText = mData.getIsIncoming() ? getResources().getString( - R.string.incoming_sim_name_text, subscriptionEntry.displayName) : - subscriptionEntry.displayName; - mSimNameView.setText(simNameText); - mSimNameView.setTextColor(showSimIconAsIncoming ? getResources().getColor( - R.color.timestamp_text_incoming) : subscriptionEntry.displayColor); - mSimNameView.setVisibility(VISIBLE); - } else { - mSimNameView.setText(null); - mSimNameView.setVisibility(GONE); - } - - final boolean metadataVisible = senderNameVisible || statusVisible - || deliveredBadgeVisible || simNameVisible; - mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE); - - final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible - || mData.hasText() || metadataVisible; - mMessageTextAndInfoView.setVisibility( - messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE); - - if (shouldShowSimplifiedVisualStyle()) { - mContactIconView.setVisibility(View.GONE); - mContactIconView.setImageResourceUri(null); - } else { - mContactIconView.setVisibility(View.VISIBLE); - final Uri avatarUri = AvatarUriUtil.createAvatarUri( - mData.getSenderProfilePhotoUri(), - mData.getSenderFullName(), - mData.getSenderNormalizedDestination(), - mData.getSenderContactLookupKey()); - mContactIconView.setImageResourceUri(avatarUri, mData.getSenderContactId(), - mData.getSenderContactLookupKey(), mData.getSenderNormalizedDestination()); - } - } - - private void updateMessageContent() { - // We must update the text before the attachments since we search the text to see if we - // should make a preview youtube image in the attachments - updateMessageText(); - updateMessageAttachments(); - updateMessageSubject(); - mMessageBubble.bind(mData); - } - - private void updateMessageAttachments() { - // Bind video, audio, and VCard attachments. If there are multiple, they stack vertically. - bindAttachmentsOfSameType(sVideoFilter, - R.layout.message_video_attachment, mVideoViewBinder, VideoThumbnailView.class); - bindAttachmentsOfSameType(sAudioFilter, - R.layout.message_audio_attachment, mAudioViewBinder, AudioAttachmentView.class); - bindAttachmentsOfSameType(sVCardFilter, - R.layout.message_vcard_attachment, mVCardViewBinder, PersonItemView.class); - - // Bind image attachments. If there are multiple, they are shown in a collage view. - final List<MessagePartData> imageParts = mData.getAttachments(sImageFilter); - if (imageParts.size() > 1) { - Collections.sort(imageParts, sImageComparator); - mMultiAttachmentView.bindAttachments(imageParts, null, imageParts.size()); - mMultiAttachmentView.setVisibility(View.VISIBLE); - } else { - mMultiAttachmentView.setVisibility(View.GONE); - } - - // In the case that we have no image attachments and exactly one youtube link in a message - // then we will show a preview. - String youtubeThumbnailUrl = null; - String originalYoutubeLink = null; - if (mMessageTextHasLinks && imageParts.size() == 0) { - CharSequence messageTextWithSpans = mMessageTextView.getText(); - final URLSpan[] spans = ((Spanned) messageTextWithSpans).getSpans(0, - messageTextWithSpans.length(), URLSpan.class); - for (URLSpan span : spans) { - String url = span.getURL(); - String youtubeLinkForUrl = YouTubeUtil.getYoutubePreviewImageLink(url); - if (!TextUtils.isEmpty(youtubeLinkForUrl)) { - if (TextUtils.isEmpty(youtubeThumbnailUrl)) { - // Save the youtube link if we don't already have one - youtubeThumbnailUrl = youtubeLinkForUrl; - originalYoutubeLink = url; - } else { - // We already have a youtube link. This means we have two youtube links so - // we shall show none. - youtubeThumbnailUrl = null; - originalYoutubeLink = null; - break; - } - } - } - } - // We need to keep track if we have a youtube link in the message so that we will not show - // the arrow - mMessageHasYouTubeLink = !TextUtils.isEmpty(youtubeThumbnailUrl); - - // We will show the message image view if there is one attachment or one youtube link - if (imageParts.size() == 1 || mMessageHasYouTubeLink) { - // Get the display metrics for a hint for how large to pull the image data into - final WindowManager windowManager = (WindowManager) getContext(). - getSystemService(Context.WINDOW_SERVICE); - final DisplayMetrics displayMetrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(displayMetrics); - - final int iconSize = getResources() - .getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size); - final int desiredWidth = displayMetrics.widthPixels - iconSize - iconSize; - - if (imageParts.size() == 1) { - final MessagePartData imagePart = imageParts.get(0); - // If the image is big, we want to scale it down to save memory since we're going to - // scale it down to fit into the bubble width. We don't constrain the height. - final ImageRequestDescriptor imageRequest = - new MessagePartImageRequestDescriptor(imagePart, - desiredWidth, - MessagePartData.UNSPECIFIED_SIZE, - false); - adjustImageViewBounds(imagePart); - mMessageImageView.setImageResourceId(imageRequest); - mMessageImageView.setTag(imagePart); - } else { - // Youtube Thumbnail image - final ImageRequestDescriptor imageRequest = - new UriImageRequestDescriptor(Uri.parse(youtubeThumbnailUrl), desiredWidth, - MessagePartData.UNSPECIFIED_SIZE, true /* allowCompression */, - true /* isStatic */, false /* cropToCircle */, - ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */, - ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */); - mMessageImageView.setImageResourceId(imageRequest); - mMessageImageView.setTag(originalYoutubeLink); - } - mMessageImageView.setVisibility(View.VISIBLE); - } else { - mMessageImageView.setImageResourceId(null); - mMessageImageView.setVisibility(View.GONE); - } - - // Show the message attachments container if any of its children are visible - boolean attachmentsVisible = false; - for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { - final View attachmentView = mMessageAttachmentsView.getChildAt(i); - if (attachmentView.getVisibility() == View.VISIBLE) { - attachmentsVisible = true; - break; - } - } - mMessageAttachmentsView.setVisibility(attachmentsVisible ? View.VISIBLE : View.GONE); - } - - private void bindAttachmentsOfSameType(final Predicate<MessagePartData> attachmentTypeFilter, - final int attachmentViewLayoutRes, final AttachmentViewBinder viewBinder, - final Class<?> attachmentViewClass) { - final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); - - // Iterate through all attachments of a particular type (video, audio, etc). - // Find the first attachment index that matches the given type if possible. - int attachmentViewIndex = -1; - View existingAttachmentView; - do { - existingAttachmentView = mMessageAttachmentsView.getChildAt(++attachmentViewIndex); - } while (existingAttachmentView != null && - !(attachmentViewClass.isInstance(existingAttachmentView))); - - for (final MessagePartData attachment : mData.getAttachments(attachmentTypeFilter)) { - View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex); - if (!attachmentViewClass.isInstance(attachmentView)) { - attachmentView = layoutInflater.inflate(attachmentViewLayoutRes, - mMessageAttachmentsView, false /* attachToRoot */); - attachmentView.setOnClickListener(this); - attachmentView.setOnLongClickListener(this); - mMessageAttachmentsView.addView(attachmentView, attachmentViewIndex); - } - viewBinder.bindView(attachmentView, attachment); - attachmentView.setTag(attachment); - attachmentView.setVisibility(View.VISIBLE); - attachmentViewIndex++; - } - // If there are unused views left over, unbind or remove them. - while (attachmentViewIndex < mMessageAttachmentsView.getChildCount()) { - final View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex); - if (attachmentViewClass.isInstance(attachmentView)) { - mMessageAttachmentsView.removeViewAt(attachmentViewIndex); - } else { - // No more views of this type; we're done. - break; - } - } - } - - private void updateMessageSubject() { - final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), - mData.getMmsSubject()); - final boolean subjectVisible = !TextUtils.isEmpty(subjectText); - - if (subjectVisible) { - mSubjectText.setText(subjectText); - mSubjectView.setVisibility(View.VISIBLE); - } else { - mSubjectView.setVisibility(View.GONE); - } - } - - private void updateMessageText() { - final String text = mData.getText(); - if (!TextUtils.isEmpty(text)) { - mMessageTextView.setText(text); - // Linkify phone numbers, web urls, emails, and map addresses to allow users to - // click on them and take the default intent. - mMessageTextHasLinks = Linkify.addLinks(mMessageTextView, Linkify.ALL); - mMessageTextView.setVisibility(View.VISIBLE); - } else { - mMessageTextView.setVisibility(View.GONE); - mMessageTextHasLinks = false; - } - } - - private void updateViewAppearance() { - final Resources res = getResources(); - final ConversationDrawables drawableProvider = ConversationDrawables.get(); - final boolean incoming = mData.getIsIncoming(); - final boolean outgoing = !incoming; - final boolean showArrow = shouldShowMessageBubbleArrow(); - - final int messageTopPaddingClustered = - res.getDimensionPixelSize(R.dimen.message_padding_same_author); - final int messageTopPaddingDefault = - res.getDimensionPixelSize(R.dimen.message_padding_default); - final int arrowWidth = res.getDimensionPixelOffset(R.dimen.message_bubble_arrow_width); - final int messageTextMinHeightDefault = res.getDimensionPixelSize( - R.dimen.conversation_message_contact_icon_size); - final int messageTextLeftRightPadding = res.getDimensionPixelOffset( - R.dimen.message_text_left_right_padding); - final int textTopPaddingDefault = res.getDimensionPixelOffset( - R.dimen.message_text_top_padding); - final int textBottomPaddingDefault = res.getDimensionPixelOffset( - R.dimen.message_text_bottom_padding); - - // These values depend on whether the message has text, attachments, or both. - // We intentionally don't set defaults, so the compiler will tell us if we forget - // to set one of them, or if we set one more than once. - final int contentLeftPadding, contentRightPadding; - final Drawable textBackground; - final int textMinHeight; - final int textTopMargin; - final int textTopPadding, textBottomPadding; - final int textLeftPadding, textRightPadding; - - if (mData.hasAttachments()) { - if (shouldShowMessageTextBubble()) { - // Text and attachment(s) - contentLeftPadding = incoming ? arrowWidth : 0; - contentRightPadding = outgoing ? arrowWidth : 0; - textBackground = drawableProvider.getBubbleDrawable( - isSelected(), - incoming, - false /* needArrow */, - mData.hasIncomingErrorStatus()); - textMinHeight = messageTextMinHeightDefault; - textTopMargin = messageTopPaddingClustered; - textTopPadding = textTopPaddingDefault; - textBottomPadding = textBottomPaddingDefault; - textLeftPadding = messageTextLeftRightPadding; - textRightPadding = messageTextLeftRightPadding; - } else { - // Attachment(s) only - contentLeftPadding = incoming ? arrowWidth : 0; - contentRightPadding = outgoing ? arrowWidth : 0; - textBackground = null; - textMinHeight = 0; - textTopMargin = 0; - textTopPadding = 0; - textBottomPadding = 0; - textLeftPadding = 0; - textRightPadding = 0; - } - } else { - // Text only - contentLeftPadding = (!showArrow && incoming) ? arrowWidth : 0; - contentRightPadding = (!showArrow && outgoing) ? arrowWidth : 0; - textBackground = drawableProvider.getBubbleDrawable( - isSelected(), - incoming, - shouldShowMessageBubbleArrow(), - mData.hasIncomingErrorStatus()); - textMinHeight = messageTextMinHeightDefault; - textTopMargin = 0; - textTopPadding = textTopPaddingDefault; - textBottomPadding = textBottomPaddingDefault; - if (showArrow && incoming) { - textLeftPadding = messageTextLeftRightPadding + arrowWidth; - } else { - textLeftPadding = messageTextLeftRightPadding; - } - if (showArrow && outgoing) { - textRightPadding = messageTextLeftRightPadding + arrowWidth; - } else { - textRightPadding = messageTextLeftRightPadding; - } - } - - // These values do not depend on whether the message includes attachments - final int gravity = incoming ? (Gravity.START | Gravity.CENTER_VERTICAL) : - (Gravity.END | Gravity.CENTER_VERTICAL); - final int messageTopPadding = shouldShowSimplifiedVisualStyle() ? - messageTopPaddingClustered : messageTopPaddingDefault; - final int metadataTopPadding = res.getDimensionPixelOffset( - R.dimen.message_metadata_top_padding); - - // Update the message text/info views - ImageUtils.setBackgroundDrawableOnView(mMessageTextAndInfoView, textBackground); - mMessageTextAndInfoView.setMinimumHeight(textMinHeight); - final LinearLayout.LayoutParams textAndInfoLayoutParams = - (LinearLayout.LayoutParams) mMessageTextAndInfoView.getLayoutParams(); - textAndInfoLayoutParams.topMargin = textTopMargin; - - if (UiUtils.isRtlMode()) { - // Need to switch right and left padding in RtL mode - mMessageTextAndInfoView.setPadding(textRightPadding, textTopPadding, textLeftPadding, - textBottomPadding); - mMessageBubble.setPadding(contentRightPadding, 0, contentLeftPadding, 0); - } else { - mMessageTextAndInfoView.setPadding(textLeftPadding, textTopPadding, textRightPadding, - textBottomPadding); - mMessageBubble.setPadding(contentLeftPadding, 0, contentRightPadding, 0); - } - - // Update the message row and message bubble views - setPadding(getPaddingLeft(), messageTopPadding, getPaddingRight(), 0); - mMessageBubble.setGravity(gravity); - updateMessageAttachmentsAppearance(gravity); - - mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0); - - updateTextAppearance(); - - requestLayout(); - } - - private void updateContentDescription() { - StringBuilder description = new StringBuilder(); - - Resources res = getResources(); - String separator = res.getString(R.string.enumeration_comma); - - // Sender information - boolean hasPlainTextMessage = !(TextUtils.isEmpty(mData.getText()) || - mMessageTextHasLinks); - if (mData.getIsIncoming()) { - int senderResId = hasPlainTextMessage - ? R.string.incoming_text_sender_content_description - : R.string.incoming_sender_content_description; - description.append(res.getString(senderResId, mData.getSenderDisplayName())); - } else { - int senderResId = hasPlainTextMessage - ? R.string.outgoing_text_sender_content_description - : R.string.outgoing_sender_content_description; - description.append(res.getString(senderResId)); - } - - if (mSubjectView.getVisibility() == View.VISIBLE) { - description.append(separator); - description.append(mSubjectText.getText()); - } - - if (mMessageTextView.getVisibility() == View.VISIBLE) { - // If the message has hyperlinks, we will let the user navigate to the text message so - // that the hyperlink can be clicked. Otherwise, the text message does not need to - // be reachable. - if (mMessageTextHasLinks) { - mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } else { - mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - description.append(separator); - description.append(mMessageTextView.getText()); - } - } - - if (mMessageTitleLayout.getVisibility() == View.VISIBLE) { - description.append(separator); - description.append(mTitleTextView.getText()); - - description.append(separator); - description.append(mMmsInfoTextView.getText()); - } - - if (mStatusTextView.getVisibility() == View.VISIBLE) { - description.append(separator); - description.append(mStatusTextView.getText()); - } - - if (mSimNameView.getVisibility() == View.VISIBLE) { - description.append(separator); - description.append(mSimNameView.getText()); - } - - if (mDeliveredBadge.getVisibility() == View.VISIBLE) { - description.append(separator); - description.append(res.getString(R.string.delivered_status_content_description)); - } - - setContentDescription(description); - } - - private void updateMessageAttachmentsAppearance(final int gravity) { - mMessageAttachmentsView.setGravity(gravity); - - // Tint image/video attachments when selected - final int selectedImageTint = getResources().getColor(R.color.message_image_selected_tint); - if (mMessageImageView.getVisibility() == View.VISIBLE) { - if (isSelected()) { - mMessageImageView.setColorFilter(selectedImageTint); - } else { - mMessageImageView.clearColorFilter(); - } - } - if (mMultiAttachmentView.getVisibility() == View.VISIBLE) { - if (isSelected()) { - mMultiAttachmentView.setColorFilter(selectedImageTint); - } else { - mMultiAttachmentView.clearColorFilter(); - } - } - for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { - final View attachmentView = mMessageAttachmentsView.getChildAt(i); - if (attachmentView instanceof VideoThumbnailView - && attachmentView.getVisibility() == View.VISIBLE) { - final VideoThumbnailView videoView = (VideoThumbnailView) attachmentView; - if (isSelected()) { - videoView.setColorFilter(selectedImageTint); - } else { - videoView.clearColorFilter(); - } - } - } - - // If there are multiple attachment bubbles in a single message, add some separation. - final int multipleAttachmentPadding = - getResources().getDimensionPixelSize(R.dimen.message_padding_same_author); - - boolean previousVisibleView = false; - for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { - final View attachmentView = mMessageAttachmentsView.getChildAt(i); - if (attachmentView.getVisibility() == View.VISIBLE) { - final int margin = previousVisibleView ? multipleAttachmentPadding : 0; - ((LinearLayout.LayoutParams) attachmentView.getLayoutParams()).topMargin = margin; - // updateViewAppearance calls requestLayout() at the end, so we don't need to here - previousVisibleView = true; - } - } - } - - private void updateTextAppearance() { - int messageColorResId; - int statusColorResId = -1; - int infoColorResId = -1; - int timestampColorResId; - int subjectLabelColorResId; - if (isSelected()) { - messageColorResId = R.color.message_text_color_incoming; - statusColorResId = R.color.message_action_status_text; - infoColorResId = R.color.message_action_info_text; - if (shouldShowMessageTextBubble()) { - timestampColorResId = R.color.message_action_timestamp_text; - subjectLabelColorResId = R.color.message_action_timestamp_text; - } else { - // If there's no text, the timestamp will be shown below the attachments, - // against the conversation view background. - timestampColorResId = R.color.timestamp_text_outgoing; - subjectLabelColorResId = R.color.timestamp_text_outgoing; - } - } else { - messageColorResId = (mData.getIsIncoming() ? - R.color.message_text_color_incoming : R.color.message_text_color_outgoing); - statusColorResId = messageColorResId; - infoColorResId = R.color.timestamp_text_incoming; - switch(mData.getStatus()) { - - case MessageData.BUGLE_STATUS_OUTGOING_FAILED: - case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: - timestampColorResId = R.color.message_failed_timestamp_text; - subjectLabelColorResId = R.color.timestamp_text_outgoing; - break; - - case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: - case MessageData.BUGLE_STATUS_OUTGOING_SENDING: - case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: - case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: - case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: - case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: - timestampColorResId = R.color.timestamp_text_outgoing; - subjectLabelColorResId = R.color.timestamp_text_outgoing; - break; - - case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: - case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: - messageColorResId = R.color.message_text_color_incoming_download_failed; - timestampColorResId = R.color.message_download_failed_timestamp_text; - subjectLabelColorResId = R.color.message_text_color_incoming_download_failed; - statusColorResId = R.color.message_download_failed_status_text; - infoColorResId = R.color.message_info_text_incoming_download_failed; - break; - - case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: - case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: - case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: - case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: - case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: - timestampColorResId = R.color.message_text_color_incoming; - subjectLabelColorResId = R.color.message_text_color_incoming; - infoColorResId = R.color.timestamp_text_incoming; - break; - - case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: - default: - timestampColorResId = R.color.timestamp_text_incoming; - subjectLabelColorResId = R.color.timestamp_text_incoming; - infoColorResId = -1; // Not used - break; - } - } - final int messageColor = getResources().getColor(messageColorResId); - mMessageTextView.setTextColor(messageColor); - mMessageTextView.setLinkTextColor(messageColor); - mSubjectText.setTextColor(messageColor); - if (statusColorResId >= 0) { - mTitleTextView.setTextColor(getResources().getColor(statusColorResId)); - } - if (infoColorResId >= 0) { - mMmsInfoTextView.setTextColor(getResources().getColor(infoColorResId)); - } - if (timestampColorResId == R.color.timestamp_text_incoming && - mData.hasAttachments() && !shouldShowMessageTextBubble()) { - timestampColorResId = R.color.timestamp_text_outgoing; - } - mStatusTextView.setTextColor(getResources().getColor(timestampColorResId)); - - mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId)); - mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId)); - } - - /** - * If we don't know the size of the image, we want to show it in a fixed-sized frame to - * avoid janks when the image is loaded and resized. Otherwise, we can set the imageview to - * take on normal layout params. - */ - private void adjustImageViewBounds(final MessagePartData imageAttachment) { - Assert.isTrue(ContentType.isImageType(imageAttachment.getContentType())); - final ViewGroup.LayoutParams layoutParams = mMessageImageView.getLayoutParams(); - if (imageAttachment.getWidth() == MessagePartData.UNSPECIFIED_SIZE || - imageAttachment.getHeight() == MessagePartData.UNSPECIFIED_SIZE) { - // We don't know the size of the image attachment, enable letterboxing on the image - // and show a fixed sized attachment. This should happen at most once per image since - // after the image is loaded we then save the image dimensions to the db so that the - // next time we can display the full size. - layoutParams.width = getResources() - .getDimensionPixelSize(R.dimen.image_attachment_fallback_width); - layoutParams.height = getResources() - .getDimensionPixelSize(R.dimen.image_attachment_fallback_height); - mMessageImageView.setScaleType(ScaleType.CENTER_CROP); - } else { - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; - // ScaleType.CENTER_INSIDE and FIT_CENTER behave similarly for most images. However, - // FIT_CENTER works better for small images as it enlarges the image such that the - // minimum size ("android:minWidth" etc) is honored. - mMessageImageView.setScaleType(ScaleType.FIT_CENTER); - } - } - - @Override - public void onClick(final View view) { - final Object tag = view.getTag(); - if (tag instanceof MessagePartData) { - final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); - onAttachmentClick((MessagePartData) tag, bounds, false /* longPress */); - } else if (tag instanceof String) { - // Currently the only object that would make a tag of a string is a youtube preview - // image - UIIntents.get().launchBrowserForUrl(getContext(), (String) tag); - } - } - - @Override - public boolean onLongClick(final View view) { - if (view == mMessageTextView) { - // Preemptively handle the long click event on message text so it's not handled by - // the link spans. - return performLongClick(); - } - - final Object tag = view.getTag(); - if (tag instanceof MessagePartData) { - final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); - return onAttachmentClick((MessagePartData) tag, bounds, true /* longPress */); - } - - return false; - } - - @Override - public boolean onAttachmentClick(final MessagePartData attachment, - final Rect viewBoundsOnScreen, final boolean longPress) { - return mHost.onAttachmentClick(this, attachment, viewBoundsOnScreen, longPress); - } - - public ContactIconView getContactIconView() { - return mContactIconView; - } - - // Sort photos in MultiAttachLayout in the same order as the ConversationImagePartsView - static final Comparator<MessagePartData> sImageComparator = new Comparator<MessagePartData>(){ - @Override - public int compare(final MessagePartData x, final MessagePartData y) { - return x.getPartId().compareTo(y.getPartId()); - } - }; - - static final Predicate<MessagePartData> sVideoFilter = new Predicate<MessagePartData>() { - @Override - public boolean apply(final MessagePartData part) { - return part.isVideo(); - } - }; - - static final Predicate<MessagePartData> sAudioFilter = new Predicate<MessagePartData>() { - @Override - public boolean apply(final MessagePartData part) { - return part.isAudio(); - } - }; - - static final Predicate<MessagePartData> sVCardFilter = new Predicate<MessagePartData>() { - @Override - public boolean apply(final MessagePartData part) { - return part.isVCard(); - } - }; - - static final Predicate<MessagePartData> sImageFilter = new Predicate<MessagePartData>() { - @Override - public boolean apply(final MessagePartData part) { - return part.isImage(); - } - }; - - interface AttachmentViewBinder { - void bindView(View view, MessagePartData attachment); - void unbind(View view); - } - - final AttachmentViewBinder mVideoViewBinder = new AttachmentViewBinder() { - @Override - public void bindView(final View view, final MessagePartData attachment) { - ((VideoThumbnailView) view).setSource(attachment, mData.getIsIncoming()); - } - - @Override - public void unbind(final View view) { - ((VideoThumbnailView) view).setSource((Uri) null, mData.getIsIncoming()); - } - }; - - final AttachmentViewBinder mAudioViewBinder = new AttachmentViewBinder() { - @Override - public void bindView(final View view, final MessagePartData attachment) { - final AudioAttachmentView audioView = (AudioAttachmentView) view; - audioView.bindMessagePartData(attachment, isSelected() || mData.getIsIncoming()); - audioView.setBackground(ConversationDrawables.get().getBubbleDrawable( - isSelected(), mData.getIsIncoming(), false /* needArrow */, - mData.hasIncomingErrorStatus())); - } - - @Override - public void unbind(final View view) { - ((AudioAttachmentView) view).bindMessagePartData(null, mData.getIsIncoming()); - } - }; - - final AttachmentViewBinder mVCardViewBinder = new AttachmentViewBinder() { - @Override - public void bindView(final View view, final MessagePartData attachment) { - final PersonItemView personView = (PersonItemView) view; - personView.bind(DataModel.get().createVCardContactItemData(getContext(), - attachment)); - personView.setBackground(ConversationDrawables.get().getBubbleDrawable( - isSelected(), mData.getIsIncoming(), false /* needArrow */, - mData.hasIncomingErrorStatus())); - final int nameTextColorRes; - final int detailsTextColorRes; - if (isSelected()) { - nameTextColorRes = R.color.message_text_color_incoming; - detailsTextColorRes = R.color.message_text_color_incoming; - } else { - nameTextColorRes = mData.getIsIncoming() ? R.color.message_text_color_incoming - : R.color.message_text_color_outgoing; - detailsTextColorRes = mData.getIsIncoming() ? R.color.timestamp_text_incoming - : R.color.timestamp_text_outgoing; - } - personView.setNameTextColor(getResources().getColor(nameTextColorRes)); - personView.setDetailsTextColor(getResources().getColor(detailsTextColorRes)); - } - - @Override - public void unbind(final View view) { - ((PersonItemView) view).bind(null); - } - }; - - /** - * A helper class that allows us to handle long clicks on linkified message text view (i.e. to - * select the message) so it's not handled by the link spans to launch apps for the links. - */ - private static class IgnoreLinkLongClickHelper implements OnLongClickListener, OnTouchListener { - private boolean mIsLongClick; - private final OnLongClickListener mDelegateLongClickListener; - - /** - * Ignore long clicks on linkified texts for a given text view. - * @param textView the TextView to ignore long clicks on - * @param longClickListener a delegate OnLongClickListener to be called when the view is - * long clicked. - */ - public static void ignoreLinkLongClick(final TextView textView, - @Nullable final OnLongClickListener longClickListener) { - final IgnoreLinkLongClickHelper helper = - new IgnoreLinkLongClickHelper(longClickListener); - textView.setOnLongClickListener(helper); - textView.setOnTouchListener(helper); - } - - private IgnoreLinkLongClickHelper(@Nullable final OnLongClickListener longClickListener) { - mDelegateLongClickListener = longClickListener; - } - - @Override - public boolean onLongClick(final View v) { - // Record that this click is a long click. - mIsLongClick = true; - if (mDelegateLongClickListener != null) { - return mDelegateLongClickListener.onLongClick(v); - } - return false; - } - - @Override - public boolean onTouch(final View v, final MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_UP && mIsLongClick) { - // This touch event is a long click, preemptively handle this touch event so that - // the link span won't get a onClicked() callback. - mIsLongClick = false; - return true; - } - - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mIsLongClick = false; - } - return false; - } - } -} diff --git a/src/com/android/messaging/ui/conversation/ConversationSimSelector.java b/src/com/android/messaging/ui/conversation/ConversationSimSelector.java deleted file mode 100644 index fc43a46..0000000 --- a/src/com/android/messaging/ui/conversation/ConversationSimSelector.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.support.v4.util.Pair; -import android.text.TextUtils; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.data.SubscriptionListData; -import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; -import com.android.messaging.ui.conversation.SimSelectorView.SimSelectorViewListener; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.Assert; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.ThreadUtil; - -/** - * Manages showing/hiding the SIM selector in conversation. - */ -abstract class ConversationSimSelector extends ConversationInput { - private SimSelectorView mSimSelectorView; - private Pair<Boolean /* show */, Boolean /* animate */> mPendingShow; - private boolean mDataReady; - private String mSelectedSimText; - - public ConversationSimSelector(ConversationInputBase baseHost) { - super(baseHost, false); - } - - public void onSubscriptionListDataLoaded(final SubscriptionListData subscriptionListData) { - ensureSimSelectorView(); - mSimSelectorView.bind(subscriptionListData); - mDataReady = subscriptionListData != null && subscriptionListData.hasData(); - if (mPendingShow != null && mDataReady) { - Assert.isTrue(OsUtil.isAtLeastL_MR1()); - final boolean show = mPendingShow.first; - final boolean animate = mPendingShow.second; - ThreadUtil.getMainThreadHandler().post(new Runnable() { - @Override - public void run() { - // This will No-Op if we are no longer attached to the host. - mConversationInputBase.showHideInternal(ConversationSimSelector.this, - show, animate); - } - }); - mPendingShow = null; - } - } - - private void announcedSelectedSim() { - final Context context = Factory.get().getApplicationContext(); - if (AccessibilityUtil.isTouchExplorationEnabled(context) && - !TextUtils.isEmpty(mSelectedSimText)) { - AccessibilityUtil.announceForAccessibilityCompat( - mSimSelectorView, null, - context.getString(R.string.selected_sim_content_message, mSelectedSimText)); - } - } - - public void setSelected(final SubscriptionListEntry subEntry) { - mSelectedSimText = subEntry == null ? null : subEntry.displayName; - } - - @Override - public boolean show(boolean animate) { - announcedSelectedSim(); - return showHide(true, animate); - } - - @Override - public boolean hide(boolean animate) { - return showHide(false, animate); - } - - private boolean showHide(final boolean show, final boolean animate) { - if (!OsUtil.isAtLeastL_MR1()) { - return false; - } - - if (mDataReady) { - mSimSelectorView.showOrHide(show, animate); - return mSimSelectorView.isOpen() == show; - } else { - mPendingShow = Pair.create(show, animate); - return false; - } - } - - private void ensureSimSelectorView() { - if (mSimSelectorView == null) { - // Grab the SIM selector view from the host. This class assumes ownership of it. - mSimSelectorView = getSimSelectorView(); - mSimSelectorView.setItemLayoutId(getSimSelectorItemLayoutId()); - mSimSelectorView.setListener(new SimSelectorViewListener() { - - @Override - public void onSimSelectorVisibilityChanged(boolean visible) { - onVisibilityChanged(visible); - } - - @Override - public void onSimItemClicked(SubscriptionListEntry item) { - selectSim(item); - } - }); - } - } - - protected abstract SimSelectorView getSimSelectorView(); - protected abstract void selectSim(final SubscriptionListEntry item); - protected abstract int getSimSelectorItemLayoutId(); - -} diff --git a/src/com/android/messaging/ui/conversation/EnterSelfPhoneNumberDialog.java b/src/com/android/messaging/ui/conversation/EnterSelfPhoneNumberDialog.java deleted file mode 100644 index e3ad601..0000000 --- a/src/com/android/messaging/ui/conversation/EnterSelfPhoneNumberDialog.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.conversation; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.widget.EditText; - -import com.android.messaging.R; -import com.android.messaging.datamodel.ParticipantRefresh; -import com.android.messaging.util.BuglePrefs; -import com.android.messaging.util.UiUtils; - -/** - * The dialog for the user to enter the phone number of their sim. - */ -public class EnterSelfPhoneNumberDialog extends DialogFragment { - private EditText mEditText; - private int mSubId; - - public static EnterSelfPhoneNumberDialog newInstance(final int subId) { - final EnterSelfPhoneNumberDialog dialog = new EnterSelfPhoneNumberDialog(); - dialog.mSubId = subId; - return dialog; - } - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final Context context = getActivity(); - final LayoutInflater inflater = LayoutInflater.from(context); - mEditText = (EditText) inflater.inflate(R.layout.enter_phone_number_view, null, false); - - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.enter_phone_number_title) - .setMessage(R.string.enter_phone_number_text) - .setView(mEditText) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int button) { - dismiss(); - } - }) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int button) { - final String newNumber = mEditText.getText().toString(); - dismiss(); - if (!TextUtils.isEmpty(newNumber)) { - savePhoneNumberInPrefs(newNumber); - // TODO: Remove this toast and just auto-send - // the message instead - UiUtils.showToast( - R.string - .toast_after_setting_default_sms_app_for_message_send); - } - } - }); - return builder.create(); - } - - private void savePhoneNumberInPrefs(final String newPhoneNumber) { - final BuglePrefs subPrefs = BuglePrefs.getSubscriptionPrefs(mSubId); - subPrefs.putString(getString(R.string.mms_phone_number_pref_key), - newPhoneNumber); - // Update the self participants so the new phone number will be reflected - // everywhere in the UI. - ParticipantRefresh.refreshSelfParticipants(); - } -} diff --git a/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java b/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java deleted file mode 100644 index 8af9f75..0000000 --- a/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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.conversation; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; - -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.data.LaunchConversationData; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.UiUtils; -import com.android.messaging.util.UriUtil; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; - -/** - * Launches ConversationActivity for sending a message to, or viewing messages from, a specific - * recipient. - * <p> - * (This activity should be marked noHistory="true" in AndroidManifest.xml) - */ -public class LaunchConversationActivity extends Activity implements - LaunchConversationData.LaunchConversationDataListener { - static final String SMS_BODY = "sms_body"; - static final String ADDRESS = "address"; - final Binding<LaunchConversationData> mBinding = BindingBase.createBinding(this); - String mSmsBody; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (UiUtils.redirectToPermissionCheckIfNeeded(this)) { - return; - } - - final Intent intent = getIntent(); - final String action = intent.getAction(); - if (Intent.ACTION_SENDTO.equals(action) || Intent.ACTION_VIEW.equals(action)) { - String[] recipients = UriUtil.parseRecipientsFromSmsMmsUri(intent.getData()); - final boolean haveAddress = !TextUtils.isEmpty(intent.getStringExtra(ADDRESS)); - final boolean haveEmail = !TextUtils.isEmpty(intent.getStringExtra(Intent.EXTRA_EMAIL)); - if (recipients == null && (haveAddress || haveEmail)) { - if (haveAddress) { - recipients = new String[] { intent.getStringExtra(ADDRESS) }; - } else { - recipients = new String[] { intent.getStringExtra(Intent.EXTRA_EMAIL) }; - } - } - mSmsBody = intent.getStringExtra(SMS_BODY); - if (TextUtils.isEmpty(mSmsBody)) { - // Used by intents sent from the web YouTube (and perhaps others). - mSmsBody = getBody(intent.getData()); - if (TextUtils.isEmpty(mSmsBody)) { - // If that fails, try yet another method apps use to share text - if (ContentType.TEXT_PLAIN.equals(intent.getType())) { - mSmsBody = intent.getStringExtra(Intent.EXTRA_TEXT); - } - } - } - if (recipients != null) { - mBinding.bind(DataModel.get().createLaunchConversationData(this)); - mBinding.getData().getOrCreateConversation(mBinding, recipients); - } else { - // No recipients were specified in the intent. - // Start a new conversation with contact picker. The new conversation will be - // primed with the (optional) message in mSmsBody. - onGetOrCreateNewConversation(null); - } - } else { - LogUtil.w(LogUtil.BUGLE_TAG, "Unsupported conversation intent action : " + action); - } - // As of M, activities without a visible window must finish before onResume completes. - finish(); - } - - private String getBody(final Uri uri) { - if (uri == null) { - return null; - } - String urlStr = uri.getSchemeSpecificPart(); - if (!urlStr.contains("?")) { - return null; - } - urlStr = urlStr.substring(urlStr.indexOf('?') + 1); - final String[] params = urlStr.split("&"); - for (final String p : params) { - if (p.startsWith("body=")) { - try { - return URLDecoder.decode(p.substring(5), "UTF-8"); - } catch (final UnsupportedEncodingException e) { - // Invalid URL, ignore - } - } - } - return null; - } - - @Override - public void onGetOrCreateNewConversation(final String conversationId) { - final Context context = Factory.get().getApplicationContext(); - UIIntents.get().launchConversationActivityWithParentStack(context, conversationId, - mSmsBody); - } - - @Override - public void onGetOrCreateNewConversationFailed() { - UiUtils.showToastAtBottom(R.string.conversation_creation_failure); - } -} diff --git a/src/com/android/messaging/ui/conversation/MessageBubbleBackground.java b/src/com/android/messaging/ui/conversation/MessageBubbleBackground.java deleted file mode 100644 index 4c22970..0000000 --- a/src/com/android/messaging/ui/conversation/MessageBubbleBackground.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.LinearLayout; - -import com.android.messaging.R; - -public class MessageBubbleBackground extends LinearLayout { - private final int mSnapWidthPixels; - - public MessageBubbleBackground(Context context, AttributeSet attrs) { - super(context, attrs); - mSnapWidthPixels = context.getResources().getDimensionPixelSize( - R.dimen.conversation_bubble_width_snap); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - final int widthPadding = getPaddingLeft() + getPaddingRight(); - int bubbleWidth = getMeasuredWidth() - widthPadding; - final int maxWidth = MeasureSpec.getSize(widthMeasureSpec) - widthPadding; - // Round up to next snapWidthPixels - bubbleWidth = Math.min(maxWidth, - (int) (Math.ceil(bubbleWidth / (float) mSnapWidthPixels) * mSnapWidthPixels)); - super.onMeasure( - MeasureSpec.makeMeasureSpec(bubbleWidth + widthPadding, MeasureSpec.EXACTLY), - heightMeasureSpec); - } -} diff --git a/src/com/android/messaging/ui/conversation/MessageDetailsDialog.java b/src/com/android/messaging/ui/conversation/MessageDetailsDialog.java deleted file mode 100644 index 89b9148..0000000 --- a/src/com/android/messaging/ui/conversation/MessageDetailsDialog.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 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.conversation; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.res.Resources; -import android.net.Uri; -import android.text.TextUtils; -import android.text.format.Formatter; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.BugleDatabaseOperations; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.data.ConversationMessageData; -import com.android.messaging.datamodel.data.ConversationParticipantsData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.mmslib.pdu.PduHeaders; -import com.android.messaging.sms.DatabaseMessages.MmsMessage; -import com.android.messaging.sms.MmsUtils; -import com.android.messaging.util.Assert; -import com.android.messaging.util.Assert.DoesNotRunOnMainThread; -import com.android.messaging.util.Dates; -import com.android.messaging.util.DebugUtils; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.SafeAsyncTask; - -import java.util.List; - -public class MessageDetailsDialog { - private static final String RECIPIENT_SEPARATOR = ", "; - - // All methods are static, no creating this class - private MessageDetailsDialog() { - } - - public static void show(final Context context, final ConversationMessageData data, - final ConversationParticipantsData participants, final ParticipantData self) { - if (DebugUtils.isDebugEnabled()) { - new SafeAsyncTask<Void, Void, String>() { - @Override - protected String doInBackgroundTimed(Void... params) { - return getMessageDetails(context, data, participants, self); - } - - @Override - protected void onPostExecute(String messageDetails) { - showDialog(context, messageDetails); - } - }.executeOnThreadPool(null, null, null); - } else { - String messageDetails = getMessageDetails(context, data, participants, self); - showDialog(context, messageDetails); - } - } - - private static String getMessageDetails(final Context context, - final ConversationMessageData data, - final ConversationParticipantsData participants, final ParticipantData self) { - String messageDetails = null; - if (data.getIsSms()) { - messageDetails = getSmsMessageDetails(data, participants, self); - } else { - // TODO: Handle SMS_TYPE_MMS_PUSH_NOTIFICATION type differently? - messageDetails = getMmsMessageDetails(context, data, participants, self); - } - - return messageDetails; - } - - private static void showDialog(final Context context, String messageDetails) { - if (!TextUtils.isEmpty(messageDetails)) { - new AlertDialog.Builder(context) - .setTitle(R.string.message_details_title) - .setMessage(messageDetails) - .setCancelable(true) - .show(); - } - } - - /** - * Return a string, separated by newlines, that contains a number of labels and values - * for this sms message. The string will be displayed in a modal dialog. - * @return string list of various message properties - */ - private static String getSmsMessageDetails(final ConversationMessageData data, - final ConversationParticipantsData participants, final ParticipantData self) { - final Resources res = Factory.get().getApplicationContext().getResources(); - final StringBuilder details = new StringBuilder(); - - // Type: Text message - details.append(res.getString(R.string.message_type_label)); - details.append(res.getString(R.string.text_message)); - - // From: +1425xxxxxxx - // or To: +1425xxxxxxx - final String rawSender = data.getSenderNormalizedDestination(); - if (!TextUtils.isEmpty(rawSender)) { - details.append('\n'); - details.append(res.getString(R.string.from_label)); - details.append(rawSender); - } - final String rawRecipients = getRecipientParticipantString(participants, - data.getParticipantId(), data.getIsIncoming(), data.getSelfParticipantId()); - if (!TextUtils.isEmpty(rawRecipients)) { - details.append('\n'); - details.append(res.getString(R.string.to_address_label)); - details.append(rawRecipients); - } - - // Sent: Mon 11:42AM - if (data.getIsIncoming()) { - if (data.getSentTimeStamp() != MmsUtils.INVALID_TIMESTAMP) { - details.append('\n'); - details.append(res.getString(R.string.sent_label)); - details.append( - Dates.getMessageDetailsTimeString(data.getSentTimeStamp()).toString()); - } - } - - // Sent: Mon 11:43AM - // or Received: Mon 11:43AM - appendSentOrReceivedTimestamp(res, details, data); - - appendSimInfo(res, self, details); - - if (DebugUtils.isDebugEnabled()) { - appendDebugInfo(details, data); - } - - return details.toString(); - } - - /** - * Return a string, separated by newlines, that contains a number of labels and values - * for this mms message. The string will be displayed in a modal dialog. - * @return string list of various message properties - */ - private static String getMmsMessageDetails(Context context, final ConversationMessageData data, - final ConversationParticipantsData participants, final ParticipantData self) { - final Resources res = Factory.get().getApplicationContext().getResources(); - // TODO: when we support non-auto-download of mms messages, we'll have to handle - // the case when the message is a PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND and display - // something different. See the Messaging app's MessageUtils.getNotificationIndDetails() - - final StringBuilder details = new StringBuilder(); - - // Type: Multimedia message. - details.append(res.getString(R.string.message_type_label)); - details.append(res.getString(R.string.multimedia_message)); - - // From: +1425xxxxxxx - final String rawSender = data.getSenderNormalizedDestination(); - details.append('\n'); - details.append(res.getString(R.string.from_label)); - details.append(!TextUtils.isEmpty(rawSender) ? rawSender : - res.getString(R.string.hidden_sender_address)); - - // To: +1425xxxxxxx - final String rawRecipients = getRecipientParticipantString(participants, - data.getParticipantId(), data.getIsIncoming(), data.getSelfParticipantId()); - if (!TextUtils.isEmpty(rawRecipients)) { - details.append('\n'); - details.append(res.getString(R.string.to_address_label)); - details.append(rawRecipients); - } - - // Sent: Tue 3:05PM - // or Received: Tue 3:05PM - appendSentOrReceivedTimestamp(res, details, data); - - // Subject: You're awesome - details.append('\n'); - details.append(res.getString(R.string.subject_label)); - if (!TextUtils.isEmpty(MmsUtils.cleanseMmsSubject(res, data.getMmsSubject()))) { - details.append(data.getMmsSubject()); - } - - // Priority: High/Normal/Low - details.append('\n'); - details.append(res.getString(R.string.priority_label)); - details.append(getPriorityDescription(res, data.getSmsPriority())); - - // Message size: 30 KB - if (data.getSmsMessageSize() > 0) { - details.append('\n'); - details.append(res.getString(R.string.message_size_label)); - details.append(Formatter.formatFileSize(context, data.getSmsMessageSize())); - } - - appendSimInfo(res, self, details); - - if (DebugUtils.isDebugEnabled()) { - appendDebugInfo(details, data); - } - - return details.toString(); - } - - private static void appendSentOrReceivedTimestamp(Resources res, StringBuilder details, - ConversationMessageData data) { - int labelId = -1; - if (data.getIsIncoming()) { - labelId = R.string.received_label; - } else if (data.getIsSendComplete()) { - labelId = R.string.sent_label; - } - if (labelId >= 0) { - details.append('\n'); - details.append(res.getString(labelId)); - details.append( - Dates.getMessageDetailsTimeString(data.getReceivedTimeStamp()).toString()); - } - } - - @DoesNotRunOnMainThread - private static void appendDebugInfo(StringBuilder details, ConversationMessageData data) { - // We grab the thread id from the database, so this needs to run in the background - Assert.isNotMainThread(); - details.append("\n\n"); - details.append("DEBUG"); - - details.append('\n'); - details.append("Message id: "); - details.append(data.getMessageId()); - - final String telephonyUri = data.getSmsMessageUri(); - details.append('\n'); - details.append("Telephony uri: "); - details.append(telephonyUri); - - final String conversationId = data.getConversationId(); - - if (conversationId == null) { - return; - } - - details.append('\n'); - details.append("Conversation id: "); - details.append(conversationId); - - final long threadId = BugleDatabaseOperations.getThreadId(DataModel.get().getDatabase(), - conversationId); - - details.append('\n'); - details.append("Conversation telephony thread id: "); - details.append(threadId); - - MmsMessage mms = null; - - if (data.getIsMms()) { - if (telephonyUri == null) { - return; - } - mms = MmsUtils.loadMms(Uri.parse(telephonyUri)); - if (mms == null) { - return; - } - - // We log the thread id again to check that they are internally consistent - final long mmsThreadId = mms.mThreadId; - details.append('\n'); - details.append("Telephony thread id: "); - details.append(mmsThreadId); - - // Log the MMS content location - final String mmsContentLocation = mms.mContentLocation; - details.append('\n'); - details.append("Content location URL: "); - details.append(mmsContentLocation); - } - - final String recipientsString = MmsUtils.getRawRecipientIdsForThread(threadId); - if (recipientsString != null) { - details.append('\n'); - details.append("Thread recipient ids: "); - details.append(recipientsString); - } - - final List<String> recipients = MmsUtils.getRecipientsByThread(threadId); - if (recipients != null) { - details.append('\n'); - details.append("Thread recipients: "); - details.append(recipients.toString()); - - if (mms != null) { - final String from = MmsUtils.getMmsSender(recipients, mms.getUri()); - details.append('\n'); - details.append("Sender: "); - details.append(from); - } - } - } - - private static String getRecipientParticipantString( - final ConversationParticipantsData participants, final String senderId, - final boolean addSelf, final String selfId) { - final StringBuilder recipients = new StringBuilder(); - for (final ParticipantData participant : participants) { - if (TextUtils.equals(participant.getId(), senderId)) { - // Don't add sender - continue; - } - if (participant.isSelf() && - (!participant.getId().equals(selfId) || !addSelf)) { - // For self participants, don't add the one that's not relevant to this message - // or if we are asked not to add self - continue; - } - final String phoneNumber = participant.getNormalizedDestination(); - // Don't add empty number. This should not happen. But if that happens - // we should not add it. - if (!TextUtils.isEmpty(phoneNumber)) { - if (recipients.length() > 0) { - recipients.append(RECIPIENT_SEPARATOR); - } - recipients.append(phoneNumber); - } - } - return recipients.toString(); - } - - /** - * Convert the numeric mms priority into a human-readable string - * @param res - * @param priorityValue coded PduHeader priority - * @return string representation of the priority - */ - private static String getPriorityDescription(final Resources res, final int priorityValue) { - switch(priorityValue) { - case PduHeaders.PRIORITY_HIGH: - return res.getString(R.string.priority_high); - case PduHeaders.PRIORITY_LOW: - return res.getString(R.string.priority_low); - case PduHeaders.PRIORITY_NORMAL: - default: - return res.getString(R.string.priority_normal); - } - } - - private static void appendSimInfo(final Resources res, - final ParticipantData self, final StringBuilder outString) { - if (!OsUtil.isAtLeastL_MR1() - || self == null - || PhoneUtils.getDefault().getActiveSubscriptionCount() < 2) { - return; - } - // The appended SIM info would look like: - // SIM: SUB 01 - // or SIM: SIM 1 - // or SIM: Unknown - Assert.isTrue(self.isSelf()); - outString.append('\n'); - outString.append(res.getString(R.string.sim_label)); - if (self.isActiveSubscription() && !self.isDefaultSelf()) { - final String subscriptionName = self.getSubscriptionName(); - if (TextUtils.isEmpty(subscriptionName)) { - outString.append(res.getString(R.string.sim_slot_identifier, - self.getDisplaySlotId())); - } else { - outString.append(subscriptionName); - } - } - } -} diff --git a/src/com/android/messaging/ui/conversation/SimIconView.java b/src/com/android/messaging/ui/conversation/SimIconView.java deleted file mode 100644 index e2e446c..0000000 --- a/src/com/android/messaging/ui/conversation/SimIconView.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.graphics.Outline; -import android.net.Uri; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewOutlineProvider; - -import com.android.messaging.ui.ContactIconView; -import com.android.messaging.util.Assert; -import com.android.messaging.util.AvatarUriUtil; -import com.android.messaging.util.OsUtil; - -/** - * Shows SIM avatar icon in the SIM switcher / Self-send button. - */ -public class SimIconView extends ContactIconView { - public SimIconView(Context context, AttributeSet attrs) { - super(context, attrs); - if (OsUtil.isAtLeastL()) { - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View v, Outline outline) { - outline.setOval(0, 0, v.getWidth(), v.getHeight()); - } - }); - } - } - - @Override - protected void maybeInitializeOnClickListener() { - // TODO: SIM icon view shouldn't consume or handle clicks, but it should if - // this is the send button for the only SIM in the device or if MSIM is not supported. - } -} diff --git a/src/com/android/messaging/ui/conversation/SimSelectorItemView.java b/src/com/android/messaging/ui/conversation/SimSelectorItemView.java deleted file mode 100644 index 3058d31..0000000 --- a/src/com/android/messaging/ui/conversation/SimSelectorItemView.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; -import com.android.messaging.util.Assert; - -/** - * Shows a view for a SIM in the SIM selector. - */ -public class SimSelectorItemView extends LinearLayout { - public interface HostInterface { - void onSimItemClicked(SubscriptionListEntry item); - } - - private SubscriptionListEntry mData; - private TextView mNameTextView; - private TextView mDetailsTextView; - private SimIconView mSimIconView; - private HostInterface mHost; - - public SimSelectorItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - mNameTextView = (TextView) findViewById(R.id.name); - mDetailsTextView = (TextView) findViewById(R.id.details); - mSimIconView = (SimIconView) findViewById(R.id.sim_icon); - setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mHost.onSimItemClicked(mData); - } - }); - } - - public void bind(final SubscriptionListEntry simEntry) { - Assert.notNull(simEntry); - mData = simEntry; - updateViewAppearance(); - } - - public void setHostInterface(final HostInterface host) { - mHost = host; - } - - private void updateViewAppearance() { - Assert.notNull(mData); - final String displayName = mData.displayName; - if (TextUtils.isEmpty(displayName)) { - mNameTextView.setVisibility(GONE); - } else { - mNameTextView.setVisibility(VISIBLE); - mNameTextView.setText(displayName); - } - - final String details = mData.displayDestination; - if (TextUtils.isEmpty(details)) { - mDetailsTextView.setVisibility(GONE); - } else { - mDetailsTextView.setVisibility(VISIBLE); - mDetailsTextView.setText(details); - } - - mSimIconView.setImageResourceUri(mData.iconUri); - } -} diff --git a/src/com/android/messaging/ui/conversation/SimSelectorView.java b/src/com/android/messaging/ui/conversation/SimSelectorView.java deleted file mode 100644 index b07ff19..0000000 --- a/src/com/android/messaging/ui/conversation/SimSelectorView.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.conversation; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.TranslateAnimation; -import android.widget.ArrayAdapter; -import android.widget.FrameLayout; -import android.widget.ListView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.SubscriptionListData; -import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; -import com.android.messaging.util.UiUtils; - -import java.util.ArrayList; -import java.util.List; - -/** - * Displays a SIM selector above the compose message view and overlays the message list. - */ -public class SimSelectorView extends FrameLayout implements SimSelectorItemView.HostInterface { - public interface SimSelectorViewListener { - void onSimItemClicked(SubscriptionListEntry item); - void onSimSelectorVisibilityChanged(boolean visible); - } - - private ListView mSimListView; - private final SimSelectorAdapter mAdapter; - private boolean mShow; - private SimSelectorViewListener mListener; - private int mItemLayoutId; - - public SimSelectorView(Context context, AttributeSet attrs) { - super(context, attrs); - mAdapter = new SimSelectorAdapter(getContext()); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mSimListView = (ListView) findViewById(R.id.sim_list); - mSimListView.setAdapter(mAdapter); - - // Clicking anywhere outside the switcher list should dismiss. - setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - showOrHide(false, true); - } - }); - } - - public void bind(final SubscriptionListData data) { - mAdapter.bindData(data.getActiveSubscriptionEntriesExcludingDefault()); - } - - public void setItemLayoutId(final int layoutId) { - mItemLayoutId = layoutId; - } - - public void setListener(final SimSelectorViewListener listener) { - mListener = listener; - } - - public void toggleVisibility() { - showOrHide(!mShow, true); - } - - public void showOrHide(final boolean show, final boolean animate) { - final boolean oldShow = mShow; - mShow = show && mAdapter.getCount() > 1; - if (oldShow != mShow) { - if (mListener != null) { - mListener.onSimSelectorVisibilityChanged(mShow); - } - - if (animate) { - // Fade in the background pane. - setVisibility(VISIBLE); - setAlpha(mShow ? 0.0f : 1.0f); - animate().alpha(mShow ? 1.0f : 0.0f) - .setDuration(UiUtils.REVEAL_ANIMATION_DURATION) - .withEndAction(new Runnable() { - @Override - public void run() { - setAlpha(1.0f); - setVisibility(mShow ? VISIBLE : GONE); - } - }); - } else { - setVisibility(mShow ? VISIBLE : GONE); - } - - // Slide in the SIM selector list via a translate animation. - mSimListView.setVisibility(mShow ? VISIBLE : GONE); - if (animate) { - mSimListView.clearAnimation(); - final TranslateAnimation translateAnimation = new TranslateAnimation( - Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0, - Animation.RELATIVE_TO_SELF, mShow ? 1.0f : 0.0f, - Animation.RELATIVE_TO_SELF, mShow ? 0.0f : 1.0f); - translateAnimation.setInterpolator(UiUtils.EASE_OUT_INTERPOLATOR); - translateAnimation.setDuration(UiUtils.REVEAL_ANIMATION_DURATION); - mSimListView.startAnimation(translateAnimation); - } - } - } - - /** - * An adapter that takes a list of SubscriptionListEntry and displays them as a list of - * available SIMs in the SIM selector. - */ - private class SimSelectorAdapter extends ArrayAdapter<SubscriptionListEntry> { - public SimSelectorAdapter(final Context context) { - super(context, R.layout.sim_selector_item_view, new ArrayList<SubscriptionListEntry>()); - } - - public void bindData(final List<SubscriptionListEntry> newList) { - clear(); - addAll(newList); - notifyDataSetChanged(); - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) { - SimSelectorItemView itemView; - if (convertView != null && convertView instanceof SimSelectorItemView) { - itemView = (SimSelectorItemView) convertView; - } else { - final LayoutInflater inflater = (LayoutInflater) getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - itemView = (SimSelectorItemView) inflater.inflate(mItemLayoutId, - parent, false); - itemView.setHostInterface(SimSelectorView.this); - } - itemView.bind(getItem(position)); - return itemView; - } - } - - @Override - public void onSimItemClicked(SubscriptionListEntry item) { - mListener.onSimItemClicked(item); - showOrHide(false, true); - } - - public boolean isOpen() { - return mShow; - } -} diff --git a/src/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java b/src/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java deleted file mode 100644 index dbbbb15..0000000 --- a/src/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * 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.conversationlist; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Fragment; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.res.Resources; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.view.View; - -import com.android.messaging.R; -import com.android.messaging.datamodel.action.DeleteConversationAction; -import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction; -import com.android.messaging.datamodel.action.UpdateConversationOptionsAction; -import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction; -import com.android.messaging.datamodel.data.ConversationListData; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.SnackBar; -import com.android.messaging.ui.SnackBarInteraction; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.contact.AddContactsConfirmationDialog; -import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost; -import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation; -import com.android.messaging.util.BugleGservices; -import com.android.messaging.util.BugleGservicesKeys; -import com.android.messaging.util.DebugUtils; -import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.Trace; -import com.android.messaging.util.UiUtils; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import javax.annotation.Nullable; - -/** - * Base class for many Conversation List activities. This will handle the common actions of multi - * select and common launching of intents. - */ -public abstract class AbstractConversationListActivity extends BugleActionBarActivity - implements ConversationListFragmentHost, MultiSelectActionModeCallback.Listener { - - private static final int REQUEST_SET_DEFAULT_SMS_APP = 1; - - protected ConversationListFragment mConversationListFragment; - - @Override - public void onAttachFragment(final Fragment fragment) { - Trace.beginSection("AbstractConversationListActivity.onAttachFragment"); - // Fragment could be debug dialog - if (fragment instanceof ConversationListFragment) { - mConversationListFragment = (ConversationListFragment) fragment; - mConversationListFragment.setHost(this); - } - Trace.endSection(); - } - - @Override - public void onBackPressed() { - // If action mode is active dismiss it - if (getActionMode() != null) { - dismissActionMode(); - return; - } - super.onBackPressed(); - } - - protected void startMultiSelectActionMode() { - startActionMode(new MultiSelectActionModeCallback(this)); - } - - protected void exitMultiSelectState() { - mConversationListFragment.showFab(); - dismissActionMode(); - mConversationListFragment.updateUi(); - } - - protected boolean isInConversationListSelectMode() { - return getActionModeCallback() instanceof MultiSelectActionModeCallback; - } - - @Override - public boolean isSelectionMode() { - return isInConversationListSelectMode(); - } - - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - } - - @Override - public void onActionBarDelete(final Collection<SelectedConversation> conversations) { - if (!PhoneUtils.getDefault().isDefaultSmsApp()) { - // TODO: figure out a good way to combine this with the implementation in - // ConversationFragment doing similar things - final Activity activity = this; - UiUtils.showSnackBarWithCustomAction(this, - getWindow().getDecorView().getRootView(), - getString(R.string.requires_default_sms_app), - SnackBar.Action.createCustomAction(new Runnable() { - @Override - public void run() { - final Intent intent = - UIIntents.get().getChangeDefaultSmsAppIntent(activity); - startActivityForResult(intent, REQUEST_SET_DEFAULT_SMS_APP); - } - }, - getString(R.string.requires_default_sms_change_button)), - null /* interactions */, - null /* placement */); - return; - } - - new AlertDialog.Builder(this) - .setTitle(getResources().getQuantityString( - R.plurals.delete_conversations_confirmation_dialog_title, - conversations.size())) - .setPositiveButton(R.string.delete_conversation_confirmation_button, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int button) { - for (final SelectedConversation conversation : conversations) { - DeleteConversationAction.deleteConversation( - conversation.conversationId, - conversation.timestamp); - } - exitMultiSelectState(); - } - }) - .setNegativeButton(R.string.delete_conversation_decline_button, null) - .show(); - } - - @Override - public void onActionBarArchive(final Iterable<SelectedConversation> conversations, - final boolean isToArchive) { - final ArrayList<String> conversationIds = new ArrayList<String>(); - for (final SelectedConversation conversation : conversations) { - final String conversationId = conversation.conversationId; - conversationIds.add(conversationId); - if (isToArchive) { - UpdateConversationArchiveStatusAction.archiveConversation(conversationId); - } else { - UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId); - } - } - - final Runnable undoRunnable = new Runnable() { - @Override - public void run() { - for (final String conversationId : conversationIds) { - if (isToArchive) { - UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId); - } else { - UpdateConversationArchiveStatusAction.archiveConversation(conversationId); - } - } - } - }; - - final int textId = - isToArchive ? R.string.archived_toast_message : R.string.unarchived_toast_message; - final String message = getResources().getString(textId, conversationIds.size()); - UiUtils.showSnackBar(this, findViewById(android.R.id.list), message, undoRunnable, - SnackBar.Action.SNACK_BAR_UNDO, - mConversationListFragment.getSnackBarInteractions()); - exitMultiSelectState(); - } - - @Override - public void onActionBarNotification(final Iterable<SelectedConversation> conversations, - final boolean isNotificationOn) { - for (final SelectedConversation conversation : conversations) { - UpdateConversationOptionsAction.enableConversationNotifications( - conversation.conversationId, isNotificationOn); - } - - final int textId = isNotificationOn ? - R.string.notification_on_toast_message : R.string.notification_off_toast_message; - final String message = getResources().getString(textId, 1); - UiUtils.showSnackBar(this, findViewById(android.R.id.list), message, - null /* undoRunnable */, - SnackBar.Action.SNACK_BAR_UNDO, mConversationListFragment.getSnackBarInteractions()); - exitMultiSelectState(); - } - - @Override - public void onActionBarAddContact(final SelectedConversation conversation) { - final Uri avatarUri; - if (conversation.icon != null) { - avatarUri = Uri.parse(conversation.icon); - } else { - avatarUri = null; - } - final AddContactsConfirmationDialog dialog = new AddContactsConfirmationDialog( - this, avatarUri, conversation.otherParticipantNormalizedDestination); - dialog.show(); - exitMultiSelectState(); - } - - @Override - public void onActionBarBlock(final SelectedConversation conversation) { - final Resources res = getResources(); - new AlertDialog.Builder(this) - .setTitle(res.getString(R.string.block_confirmation_title, - conversation.otherParticipantNormalizedDestination)) - .setMessage(res.getString(R.string.block_confirmation_message)) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface arg0, final int arg1) { - final Context context = AbstractConversationListActivity.this; - final View listView = findViewById(android.R.id.list); - final List<SnackBarInteraction> interactions = - mConversationListFragment.getSnackBarInteractions(); - final UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener - undoListener = - new UpdateDestinationBlockedActionSnackBar( - context, listView, null /* undoRunnable */, - interactions); - final Runnable undoRunnable = new Runnable() { - @Override - public void run() { - UpdateDestinationBlockedAction.updateDestinationBlocked( - conversation.otherParticipantNormalizedDestination, false, - conversation.conversationId, - undoListener); - } - }; - final UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener - listener = new UpdateDestinationBlockedActionSnackBar( - context, listView, undoRunnable, interactions); - UpdateDestinationBlockedAction.updateDestinationBlocked( - conversation.otherParticipantNormalizedDestination, true, - conversation.conversationId, - listener); - exitMultiSelectState(); - } - }) - .create() - .show(); - } - - @Override - public void onConversationClick(final ConversationListData listData, - final ConversationListItemData conversationListItemData, - final boolean isLongClick, - final ConversationListItemView conversationView) { - if (isLongClick && !isInConversationListSelectMode()) { - startMultiSelectActionMode(); - } - - if (isInConversationListSelectMode()) { - final MultiSelectActionModeCallback multiSelectActionMode = - (MultiSelectActionModeCallback) getActionModeCallback(); - multiSelectActionMode.toggleSelect(listData, conversationListItemData); - mConversationListFragment.updateUi(); - } else { - final String conversationId = conversationListItemData.getConversationId(); - Bundle sceneTransitionAnimationOptions = null; - boolean hasCustomTransitions = false; - - UIIntents.get().launchConversationActivity( - this, conversationId, null, - sceneTransitionAnimationOptions, - hasCustomTransitions); - } - } - - @Override - public void onCreateConversationClick() { - UIIntents.get().launchCreateNewConversationActivity(this, null); - } - - - @Override - public boolean isConversationSelected(final String conversationId) { - return isInConversationListSelectMode() && - ((MultiSelectActionModeCallback) getActionModeCallback()).isSelected( - conversationId); - } - - public void onActionBarDebug() { - DebugUtils.showDebugOptions(this); - } - - private static class UpdateDestinationBlockedActionSnackBar - implements UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener { - private final Context mContext; - private final View mParentView; - private final Runnable mUndoRunnable; - private final List<SnackBarInteraction> mInteractions; - - UpdateDestinationBlockedActionSnackBar(final Context context, - @NonNull final View parentView, @Nullable final Runnable undoRunnable, - @Nullable List<SnackBarInteraction> interactions) { - mContext = context; - mParentView = parentView; - mUndoRunnable = undoRunnable; - mInteractions = interactions; - } - - @Override - public void onUpdateDestinationBlockedAction( - final UpdateDestinationBlockedAction action, - final boolean success, final boolean block, - final String destination) { - if (success) { - final int messageId = block ? R.string.blocked_toast_message - : R.string.unblocked_toast_message; - final String message = mContext.getResources().getString(messageId, 1); - UiUtils.showSnackBar(mContext, mParentView, message, mUndoRunnable, - SnackBar.Action.SNACK_BAR_UNDO, mInteractions); - } - } - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java b/src/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java deleted file mode 100644 index 366c7d3..0000000 --- a/src/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.conversationlist; - -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.view.Menu; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.util.DebugUtils; - -public class ArchivedConversationListActivity extends AbstractConversationListActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final ConversationListFragment fragment = - ConversationListFragment.createArchivedConversationListFragment(); - getFragmentManager().beginTransaction().add(android.R.id.content, fragment).commit(); - invalidateActionBar(); - } - - protected void updateActionBar(ActionBar actionBar) { - actionBar.setTitle(getString(R.string.archived_activity_title)); - actionBar.setDisplayShowTitleEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setBackgroundDrawable(new ColorDrawable( - getResources().getColor( - R.color.archived_conversation_action_bar_background_color_dark))); - actionBar.show(); - super.updateActionBar(actionBar); - } - - @Override - public void onBackPressed() { - if (isInConversationListSelectMode()) { - exitMultiSelectState(); - } else { - super.onBackPressed(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - if (super.onCreateOptionsMenu(menu)) { - return true; - } - getMenuInflater().inflate(R.menu.archived_conversation_list_menu, menu); - final MenuItem item = menu.findItem(R.id.action_debug_options); - if (item != null) { - final boolean enableDebugItems = DebugUtils.isDebugEnabled(); - item.setVisible(enableDebugItems).setEnabled(enableDebugItems); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - switch(menuItem.getItemId()) { - case R.id.action_debug_options: - onActionBarDebug(); - return true; - case android.R.id.home: - onActionBarHome(); - return true; - default: - return super.onOptionsItemSelected(menuItem); - } - } - - @Override - public void onActionBarHome() { - onBackPressed(); - } - - @Override - public boolean isSwipeAnimatable() { - return false; - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ConversationListActivity.java b/src/com/android/messaging/ui/conversationlist/ConversationListActivity.java deleted file mode 100644 index f8abe81..0000000 --- a/src/com/android/messaging/ui/conversationlist/ConversationListActivity.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.conversationlist; - -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.view.Menu; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.DebugUtils; -import com.android.messaging.util.Trace; - -public class ConversationListActivity extends AbstractConversationListActivity { - @Override - protected void onCreate(final Bundle savedInstanceState) { - Trace.beginSection("ConversationListActivity.onCreate"); - super.onCreate(savedInstanceState); - setContentView(R.layout.conversation_list_activity); - Trace.endSection(); - invalidateActionBar(); - } - - @Override - protected void updateActionBar(final ActionBar actionBar) { - actionBar.setTitle(getString(R.string.app_name)); - actionBar.setDisplayShowTitleEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(false); - actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); - actionBar.setBackgroundDrawable(new ColorDrawable( - getResources().getColor(R.color.action_bar_background_color))); - actionBar.show(); - super.updateActionBar(actionBar); - } - - @Override - public void onResume() { - super.onResume(); - // Invalidate the menu as items that are based on settings may have changed - // while not in the app (e.g. Talkback enabled/disable affects new conversation - // button) - supportInvalidateOptionsMenu(); - } - - @Override - public void onBackPressed() { - if (isInConversationListSelectMode()) { - exitMultiSelectState(); - } else { - super.onBackPressed(); - } - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - if (super.onCreateOptionsMenu(menu)) { - return true; - } - getMenuInflater().inflate(R.menu.conversation_list_fragment_menu, menu); - final MenuItem item = menu.findItem(R.id.action_debug_options); - if (item != null) { - final boolean enableDebugItems = DebugUtils.isDebugEnabled(); - item.setVisible(enableDebugItems).setEnabled(enableDebugItems); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem menuItem) { - switch(menuItem.getItemId()) { - case R.id.action_start_new_conversation: - onActionBarStartNewConversation(); - return true; - case R.id.action_settings: - onActionBarSettings(); - return true; - case R.id.action_debug_options: - onActionBarDebug(); - return true; - case R.id.action_show_archived: - onActionBarArchived(); - return true; - case R.id.action_show_blocked_contacts: - onActionBarBlockedParticipants(); - return true; - } - return super.onOptionsItemSelected(menuItem); - } - - @Override - public void onActionBarHome() { - exitMultiSelectState(); - } - - public void onActionBarStartNewConversation() { - UIIntents.get().launchCreateNewConversationActivity(this, null); - } - - public void onActionBarSettings() { - UIIntents.get().launchSettingsActivity(this); - } - - public void onActionBarBlockedParticipants() { - UIIntents.get().launchBlockedParticipantsActivity(this); - } - - public void onActionBarArchived() { - UIIntents.get().launchArchivedConversationsActivity(this); - } - - @Override - public boolean isSwipeAnimatable() { - return !isInConversationListSelectMode(); - } - - @Override - public void onWindowFocusChanged(final boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - final ConversationListFragment conversationListFragment = - (ConversationListFragment) getFragmentManager().findFragmentById( - R.id.conversation_list_fragment); - // When the screen is turned on, the last used activity gets resumed, but it gets - // window focus only after the lock screen is unlocked. - if (hasFocus && conversationListFragment != null) { - conversationListFragment.setScrolledToNewestConversationIfNeeded(); - } - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ConversationListAdapter.java b/src/com/android/messaging/ui/conversationlist/ConversationListAdapter.java deleted file mode 100644 index 629c4ae..0000000 --- a/src/com/android/messaging/ui/conversationlist/ConversationListAdapter.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.conversationlist; - -import android.content.Context; -import android.database.Cursor; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import com.android.messaging.R; -import com.android.messaging.ui.CursorRecyclerAdapter; -import com.android.messaging.ui.conversationlist.ConversationListItemView.HostInterface; - -/** - * Provides an interface to expose Conversation List Cursor data to a UI widget like a ListView. - */ -public class ConversationListAdapter - extends CursorRecyclerAdapter<ConversationListAdapter.ConversationListViewHolder> { - - private final ConversationListItemView.HostInterface mClivHostInterface; - - public ConversationListAdapter(final Context context, final Cursor cursor, - final ConversationListItemView.HostInterface clivHostInterface) { - super(context, cursor, 0); - mClivHostInterface = clivHostInterface; - setHasStableIds(true); - } - - /** - * @see com.android.messaging.ui.CursorRecyclerAdapter#bindViewHolder( - * android.support.v7.widget.RecyclerView.ViewHolder, android.content.Context, - * android.database.Cursor) - */ - @Override - public void bindViewHolder(final ConversationListViewHolder holder, final Context context, - final Cursor cursor) { - final ConversationListItemView conversationListItemView = holder.mView; - conversationListItemView.bind(cursor, mClivHostInterface); - } - - @Override - public ConversationListViewHolder createViewHolder(final Context context, - final ViewGroup parent, final int viewType) { - final LayoutInflater layoutInflater = LayoutInflater.from(context); - final ConversationListItemView itemView = - (ConversationListItemView) layoutInflater.inflate( - R.layout.conversation_list_item_view, null); - return new ConversationListViewHolder(itemView); - } - - /** - * ViewHolder that holds a ConversationListItemView. - */ - public static class ConversationListViewHolder extends RecyclerView.ViewHolder { - final ConversationListItemView mView; - - public ConversationListViewHolder(final ConversationListItemView itemView) { - super(itemView); - mView = itemView; - } - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ConversationListFragment.java b/src/com/android/messaging/ui/conversationlist/ConversationListFragment.java deleted file mode 100644 index 2f868d4..0000000 --- a/src/com/android/messaging/ui/conversationlist/ConversationListFragment.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * 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.conversationlist; - -import android.app.Activity; -import android.app.Fragment; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; -import android.os.Parcelable; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewGroupCompat; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.ViewGroup.MarginLayoutParams; -import android.view.ViewPropertyAnimator; -import android.view.accessibility.AccessibilityManager; -import android.widget.AbsListView; -import android.widget.ImageView; - -import com.android.messaging.R; -import com.android.messaging.annotation.VisibleForAnimation; -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.data.ConversationListData; -import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.ui.BugleAnimationTags; -import com.android.messaging.ui.ListEmptyView; -import com.android.messaging.ui.SnackBarInteraction; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.AccessibilityUtil; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ImeUtil; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.UiUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.util.ArrayList; -import java.util.List; - -/** - * Shows a list of conversations. - */ -public class ConversationListFragment extends Fragment implements ConversationListDataListener, - ConversationListItemView.HostInterface { - private static final String BUNDLE_ARCHIVED_MODE = "archived_mode"; - private static final String BUNDLE_FORWARD_MESSAGE_MODE = "forward_message_mode"; - private static final boolean VERBOSE = false; - - private MenuItem mShowBlockedMenuItem; - private boolean mArchiveMode; - private boolean mBlockedAvailable; - private boolean mForwardMessageMode; - - public interface ConversationListFragmentHost { - public void onConversationClick(final ConversationListData listData, - final ConversationListItemData conversationListItemData, - final boolean isLongClick, - final ConversationListItemView conversationView); - public void onCreateConversationClick(); - public boolean isConversationSelected(final String conversationId); - public boolean isSwipeAnimatable(); - public boolean isSelectionMode(); - public boolean hasWindowFocus(); - } - - private ConversationListFragmentHost mHost; - private RecyclerView mRecyclerView; - private ImageView mStartNewConversationButton; - private ListEmptyView mEmptyListMessageView; - private ConversationListAdapter mAdapter; - - // Saved Instance State Data - only for temporal data which is nice to maintain but not - // critical for correctness. - private static final String SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY = - "conversationListViewState"; - private Parcelable mListState; - - @VisibleForTesting - final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this); - - public static ConversationListFragment createArchivedConversationListFragment() { - return createConversationListFragment(BUNDLE_ARCHIVED_MODE); - } - - public static ConversationListFragment createForwardMessageConversationListFragment() { - return createConversationListFragment(BUNDLE_FORWARD_MESSAGE_MODE); - } - - public static ConversationListFragment createConversationListFragment(String modeKeyName) { - final ConversationListFragment fragment = new ConversationListFragment(); - final Bundle bundle = new Bundle(); - bundle.putBoolean(modeKeyName, true); - fragment.setArguments(bundle); - return fragment; - } - - /** - * {@inheritDoc} from Fragment - */ - @Override - public void onCreate(final Bundle bundle) { - super.onCreate(bundle); - mListBinding.getData().init(getLoaderManager(), mListBinding); - mAdapter = new ConversationListAdapter(getActivity(), null, this); - } - - @Override - public void onResume() { - super.onResume(); - - Assert.notNull(mHost); - setScrolledToNewestConversationIfNeeded(); - - updateUi(); - } - - public void setScrolledToNewestConversationIfNeeded() { - if (!mArchiveMode - && !mForwardMessageMode - && isScrolledToFirstConversation() - && mHost.hasWindowFocus()) { - mListBinding.getData().setScrolledToNewestConversation(true); - } - } - - private boolean isScrolledToFirstConversation() { - int firstItemPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager()) - .findFirstCompletelyVisibleItemPosition(); - return firstItemPosition == 0; - } - - /** - * {@inheritDoc} from Fragment - */ - @Override - public void onDestroy() { - super.onDestroy(); - mListBinding.unbind(); - mHost = null; - } - - /** - * {@inheritDoc} from Fragment - */ - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.conversation_list_fragment, - container, false); - mRecyclerView = (RecyclerView) rootView.findViewById(android.R.id.list); - mEmptyListMessageView = (ListEmptyView) rootView.findViewById(R.id.no_conversations_view); - mEmptyListMessageView.setImageHint(R.drawable.ic_oobe_conv_list); - // The default behavior for default layout param generation by LinearLayoutManager is to - // provide width and height of WRAP_CONTENT, but this is not desirable for - // ConversationListFragment; the view in each row should be a width of MATCH_PARENT so that - // the entire row is tappable. - final Activity activity = getActivity(); - final LinearLayoutManager manager = new LinearLayoutManager(activity) { - @Override - public RecyclerView.LayoutParams generateDefaultLayoutParams() { - return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - } - }; - mRecyclerView.setLayoutManager(manager); - mRecyclerView.setHasFixedSize(true); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { - int mCurrentState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE; - - @Override - public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) { - if (mCurrentState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL - || mCurrentState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) { - ImeUtil.get().hideImeKeyboard(getActivity(), mRecyclerView); - } - - if (isScrolledToFirstConversation()) { - setScrolledToNewestConversationIfNeeded(); - } else { - mListBinding.getData().setScrolledToNewestConversation(false); - } - } - - @Override - public void onScrollStateChanged(final RecyclerView recyclerView, final int newState) { - mCurrentState = newState; - } - }); - mRecyclerView.addOnItemTouchListener(new ConversationListSwipeHelper(mRecyclerView)); - - if (savedInstanceState != null) { - mListState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY); - } - - mStartNewConversationButton = (ImageView) rootView.findViewById( - R.id.start_new_conversation_button); - if (mArchiveMode) { - mStartNewConversationButton.setVisibility(View.GONE); - } else { - mStartNewConversationButton.setVisibility(View.VISIBLE); - mStartNewConversationButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View clickView) { - mHost.onCreateConversationClick(); - } - }); - } - ViewCompat.setTransitionName(mStartNewConversationButton, BugleAnimationTags.TAG_FABICON); - - // The root view has a non-null background, which by default is deemed by the framework - // to be a "transition group," where all child views are animated together during an - // activity transition. However, we want each individual items in the recycler view to - // show explode animation themselves, so we explicitly tag the root view to be a non-group. - ViewGroupCompat.setTransitionGroup(rootView, false); - - setHasOptionsMenu(true); - return rootView; - } - - @Override - public void onAttach(final Activity activity) { - super.onAttach(activity); - if (VERBOSE) { - LogUtil.v(LogUtil.BUGLE_TAG, "Attaching List"); - } - final Bundle arguments = getArguments(); - if (arguments != null) { - mArchiveMode = arguments.getBoolean(BUNDLE_ARCHIVED_MODE, false); - mForwardMessageMode = arguments.getBoolean(BUNDLE_FORWARD_MESSAGE_MODE, false); - } - mListBinding.bind(DataModel.get().createConversationListData(activity, this, mArchiveMode)); - } - - - @Override - public void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - if (mListState != null) { - outState.putParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY, mListState); - } - } - - @Override - public void onPause() { - super.onPause(); - mListState = mRecyclerView.getLayoutManager().onSaveInstanceState(); - mListBinding.getData().setScrolledToNewestConversation(false); - } - - /** - * Call this immediately after attaching the fragment - */ - public void setHost(final ConversationListFragmentHost host) { - Assert.isNull(mHost); - mHost = host; - } - - @Override - public void onConversationListCursorUpdated(final ConversationListData data, - final Cursor cursor) { - mListBinding.ensureBound(data); - final Cursor oldCursor = mAdapter.swapCursor(cursor); - updateEmptyListUi(cursor == null || cursor.getCount() == 0); - if (mListState != null && cursor != null && oldCursor == null) { - mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState); - } - } - - @Override - public void setBlockedParticipantsAvailable(final boolean blockedAvailable) { - mBlockedAvailable = blockedAvailable; - if (mShowBlockedMenuItem != null) { - mShowBlockedMenuItem.setVisible(blockedAvailable); - } - } - - public void updateUi() { - mAdapter.notifyDataSetChanged(); - } - - @Override - public void onPrepareOptionsMenu(final Menu menu) { - super.onPrepareOptionsMenu(menu); - final MenuItem startNewConversationMenuItem = - menu.findItem(R.id.action_start_new_conversation); - if (startNewConversationMenuItem != null) { - // It is recommended for the Floating Action button functionality to be duplicated as a - // menu - AccessibilityManager accessibilityManager = (AccessibilityManager) - getActivity().getSystemService(Context.ACCESSIBILITY_SERVICE); - startNewConversationMenuItem.setVisible(accessibilityManager - .isTouchExplorationEnabled()); - } - - final MenuItem archive = menu.findItem(R.id.action_show_archived); - if (archive != null) { - archive.setVisible(true); - } - } - - @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - if (!isAdded()) { - // Guard against being called before we're added to the activity - return; - } - - mShowBlockedMenuItem = menu.findItem(R.id.action_show_blocked_contacts); - if (mShowBlockedMenuItem != null) { - mShowBlockedMenuItem.setVisible(mBlockedAvailable); - } - } - - /** - * {@inheritDoc} from ConversationListItemView.HostInterface - */ - @Override - public void onConversationClicked(final ConversationListItemData conversationListItemData, - final boolean isLongClick, final ConversationListItemView conversationView) { - final ConversationListData listData = mListBinding.getData(); - mHost.onConversationClick(listData, conversationListItemData, isLongClick, - conversationView); - } - - /** - * {@inheritDoc} from ConversationListItemView.HostInterface - */ - @Override - public boolean isConversationSelected(final String conversationId) { - return mHost.isConversationSelected(conversationId); - } - - @Override - public boolean isSwipeAnimatable() { - return mHost.isSwipeAnimatable(); - } - - // Show and hide empty list UI as needed with appropriate text based on view specifics - private void updateEmptyListUi(final boolean isEmpty) { - if (isEmpty) { - int emptyListText; - if (!mListBinding.getData().getHasFirstSyncCompleted()) { - emptyListText = R.string.conversation_list_first_sync_text; - } else if (mArchiveMode) { - emptyListText = R.string.archived_conversation_list_empty_text; - } else { - emptyListText = R.string.conversation_list_empty_text; - } - mEmptyListMessageView.setTextHint(emptyListText); - mEmptyListMessageView.setVisibility(View.VISIBLE); - mEmptyListMessageView.setIsImageVisible(true); - mEmptyListMessageView.setIsVerticallyCentered(true); - } else { - mEmptyListMessageView.setVisibility(View.GONE); - } - } - - @Override - public List<SnackBarInteraction> getSnackBarInteractions() { - final List<SnackBarInteraction> interactions = new ArrayList<SnackBarInteraction>(1); - final SnackBarInteraction fabInteraction = - new SnackBarInteraction.BasicSnackBarInteraction(mStartNewConversationButton); - interactions.add(fabInteraction); - return interactions; - } - - private ViewPropertyAnimator getNormalizedFabAnimator() { - return mStartNewConversationButton.animate() - .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR) - .setDuration(getActivity().getResources().getInteger( - R.integer.fab_animation_duration_ms)); - } - - public ViewPropertyAnimator dismissFab() { - // To prevent clicking while animating. - mStartNewConversationButton.setEnabled(false); - final MarginLayoutParams lp = - (MarginLayoutParams) mStartNewConversationButton.getLayoutParams(); - final float fabWidthWithLeftRightMargin = mStartNewConversationButton.getWidth() - + lp.leftMargin + lp.rightMargin; - final int direction = AccessibilityUtil.isLayoutRtl(mStartNewConversationButton) ? -1 : 1; - return getNormalizedFabAnimator().translationX(direction * fabWidthWithLeftRightMargin); - } - - public ViewPropertyAnimator showFab() { - return getNormalizedFabAnimator().translationX(0).withEndAction(new Runnable() { - @Override - public void run() { - // Re-enable clicks after the animation. - mStartNewConversationButton.setEnabled(true); - } - }); - } - - public View getHeroElementForTransition() { - return mArchiveMode ? null : mStartNewConversationButton; - } - - @VisibleForAnimation - public RecyclerView getRecyclerView() { - return mRecyclerView; - } - - @Override - public void startFullScreenPhotoViewer( - final Uri initialPhoto, final Rect initialPhotoBounds, final Uri photosUri) { - UIIntents.get().launchFullScreenPhotoViewer( - getActivity(), initialPhoto, initialPhotoBounds, photosUri); - } - - @Override - public void startFullScreenVideoViewer(final Uri videoUri) { - UIIntents.get().launchFullScreenVideoViewer(getActivity(), videoUri); - } - - @Override - public boolean isSelectionMode() { - return mHost != null && mHost.isSelectionMode(); - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ConversationListItemView.java b/src/com/android/messaging/ui/conversationlist/ConversationListItemView.java deleted file mode 100644 index 7525182..0000000 --- a/src/com/android/messaging/ui/conversationlist/ConversationListItemView.java +++ /dev/null @@ -1,643 +0,0 @@ -/* - * 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.conversationlist; - -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.net.Uri; -import android.support.v4.text.BidiFormatter; -import android.support.v4.text.TextDirectionHeuristicsCompat; -import android.text.TextPaint; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnLayoutChangeListener; -import android.view.View.OnLongClickListener; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.annotation.VisibleForAnimation; -import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.media.UriImageRequestDescriptor; -import com.android.messaging.sms.MmsUtils; -import com.android.messaging.ui.AsyncImageView; -import com.android.messaging.ui.AudioAttachmentView; -import com.android.messaging.ui.ContactIconView; -import com.android.messaging.ui.SnackBar; -import com.android.messaging.ui.SnackBarInteraction; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.ImageUtils; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.Typefaces; -import com.android.messaging.util.UiUtils; -import com.android.messaging.util.UriUtil; - -import java.util.List; - -/** - * The view for a single entry in a conversation list. - */ -public class ConversationListItemView extends FrameLayout implements OnClickListener, - OnLongClickListener, OnLayoutChangeListener { - static final int UNREAD_SNIPPET_LINE_COUNT = 3; - static final int NO_UNREAD_SNIPPET_LINE_COUNT = 1; - private int mListItemReadColor; - private int mListItemUnreadColor; - private Typeface mListItemReadTypeface; - private Typeface mListItemUnreadTypeface; - private static String sPlusOneString; - private static String sPlusNString; - - public interface HostInterface { - boolean isConversationSelected(final String conversationId); - void onConversationClicked(final ConversationListItemData conversationListItemData, - boolean isLongClick, final ConversationListItemView conversationView); - boolean isSwipeAnimatable(); - List<SnackBarInteraction> getSnackBarInteractions(); - void startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds, - final Uri photosUri); - void startFullScreenVideoViewer(final Uri videoUri); - boolean isSelectionMode(); - } - - private final OnClickListener fullScreenPreviewClickListener = new OnClickListener() { - @Override - public void onClick(final View v) { - final String previewType = mData.getShowDraft() ? - mData.getDraftPreviewContentType() : mData.getPreviewContentType(); - Assert.isTrue(ContentType.isImageType(previewType) || - ContentType.isVideoType(previewType)); - - final Uri previewUri = mData.getShowDraft() ? - mData.getDraftPreviewUri() : mData.getPreviewUri(); - if (ContentType.isImageType(previewType)) { - final Uri imagesUri = mData.getShowDraft() ? - MessagingContentProvider.buildDraftImagesUri(mData.getConversationId()) : - MessagingContentProvider - .buildConversationImagesUri(mData.getConversationId()); - final Rect previewImageBounds = UiUtils.getMeasuredBoundsOnScreen(v); - mHostInterface.startFullScreenPhotoViewer( - previewUri, previewImageBounds, imagesUri); - } else { - mHostInterface.startFullScreenVideoViewer(previewUri); - } - } - }; - - private final ConversationListItemData mData; - - private int mAnimatingCount; - private ViewGroup mSwipeableContainer; - private ViewGroup mCrossSwipeBackground; - private ViewGroup mSwipeableContent; - private TextView mConversationNameView; - private TextView mSnippetTextView; - private TextView mSubjectTextView; - private TextView mTimestampTextView; - private ContactIconView mContactIconView; - private ImageView mContactCheckmarkView; - private ImageView mNotificationBellView; - private ImageView mFailedStatusIconView; - private ImageView mCrossSwipeArchiveLeftImageView; - private ImageView mCrossSwipeArchiveRightImageView; - private AsyncImageView mImagePreviewView; - private AudioAttachmentView mAudioAttachmentView; - private HostInterface mHostInterface; - - public ConversationListItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mData = new ConversationListItemData(); - final Resources res = context.getResources(); - } - - @Override - protected void onFinishInflate() { - mSwipeableContainer = (ViewGroup) findViewById(R.id.swipeableContainer); - mCrossSwipeBackground = (ViewGroup) findViewById(R.id.crossSwipeBackground); - mSwipeableContent = (ViewGroup) findViewById(R.id.swipeableContent); - mConversationNameView = (TextView) findViewById(R.id.conversation_name); - mSnippetTextView = (TextView) findViewById(R.id.conversation_snippet); - mSubjectTextView = (TextView) findViewById(R.id.conversation_subject); - mTimestampTextView = (TextView) findViewById(R.id.conversation_timestamp); - mContactIconView = (ContactIconView) findViewById(R.id.conversation_icon); - mContactCheckmarkView = (ImageView) findViewById(R.id.conversation_checkmark); - mNotificationBellView = (ImageView) findViewById(R.id.conversation_notification_bell); - mFailedStatusIconView = (ImageView) findViewById(R.id.conversation_failed_status_icon); - mCrossSwipeArchiveLeftImageView = (ImageView) findViewById(R.id.crossSwipeArchiveIconLeft); - mCrossSwipeArchiveRightImageView = - (ImageView) findViewById(R.id.crossSwipeArchiveIconRight); - mImagePreviewView = (AsyncImageView) findViewById(R.id.conversation_image_preview); - mAudioAttachmentView = (AudioAttachmentView) findViewById(R.id.audio_attachment_view); - mConversationNameView.addOnLayoutChangeListener(this); - mSnippetTextView.addOnLayoutChangeListener(this); - - final Resources resources = getContext().getResources(); - mListItemReadColor = resources.getColor(R.color.conversation_list_item_read); - mListItemUnreadColor = resources.getColor(R.color.conversation_list_item_unread); - - mListItemReadTypeface = Typefaces.getRobotoNormal(); - mListItemUnreadTypeface = Typefaces.getRobotoBold(); - - if (OsUtil.isAtLeastL()) { - setTransitionGroup(true); - } - } - - @Override - public void onLayoutChange(final View v, final int left, final int top, final int right, - final int bottom, final int oldLeft, final int oldTop, final int oldRight, - final int oldBottom) { - if (v == mConversationNameView) { - setConversationName(); - } else if (v == mSnippetTextView) { - setSnippet(); - } else if (v == mSubjectTextView) { - setSubject(); - } - } - - private void setConversationName() { - if (mData.getIsRead() || mData.getShowDraft()) { - mConversationNameView.setTextColor(mListItemReadColor); - mConversationNameView.setTypeface(mListItemReadTypeface); - } else { - mConversationNameView.setTextColor(mListItemUnreadColor); - mConversationNameView.setTypeface(mListItemUnreadTypeface); - } - - final String conversationName = mData.getName(); - - // For group conversations, ellipsize the group members that do not fit - final CharSequence ellipsizedName = UiUtils.commaEllipsize( - conversationName, - mConversationNameView.getPaint(), - mConversationNameView.getMeasuredWidth(), - getPlusOneString(), - getPlusNString()); - // RTL : To format conversation name if it happens to be phone number. - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - final String bidiFormattedName = bidiFormatter.unicodeWrap( - ellipsizedName.toString(), - TextDirectionHeuristicsCompat.LTR); - - mConversationNameView.setText(bidiFormattedName); - } - - private static String getPlusOneString() { - if (sPlusOneString == null) { - sPlusOneString = Factory.get().getApplicationContext().getResources() - .getString(R.string.plus_one); - } - return sPlusOneString; - } - - private static String getPlusNString() { - if (sPlusNString == null) { - sPlusNString = Factory.get().getApplicationContext().getResources() - .getString(R.string.plus_n); - } - return sPlusNString; - } - - private void setSubject() { - final String subjectText = mData.getShowDraft() ? - mData.getDraftSubject() : - MmsUtils.cleanseMmsSubject(getContext().getResources(), mData.getSubject()); - if (!TextUtils.isEmpty(subjectText)) { - final String subjectPrepend = getResources().getString(R.string.subject_label); - mSubjectTextView.setText(TextUtils.concat(subjectPrepend, subjectText)); - mSubjectTextView.setVisibility(VISIBLE); - } else { - mSubjectTextView.setVisibility(GONE); - } - } - - private void setSnippet() { - mSnippetTextView.setText(getSnippetText()); - } - - // Resource Ids of content descriptions prefixes for different message status. - private static final int [][][] sPrimaryContentDescriptions = { - // 1:1 conversation - { - // Incoming message - { - R.string.one_on_one_incoming_failed_message_prefix, - R.string.one_on_one_incoming_successful_message_prefix - }, - // Outgoing message - { - R.string.one_on_one_outgoing_failed_message_prefix, - R.string.one_on_one_outgoing_successful_message_prefix, - R.string.one_on_one_outgoing_draft_message_prefix, - R.string.one_on_one_outgoing_sending_message_prefix, - } - }, - - // Group conversation - { - // Incoming message - { - R.string.group_incoming_failed_message_prefix, - R.string.group_incoming_successful_message_prefix, - }, - // Outgoing message - { - R.string.group_outgoing_failed_message_prefix, - R.string.group_outgoing_successful_message_prefix, - R.string.group_outgoing_draft_message_prefix, - R.string.group_outgoing_sending_message_prefix, - } - } - }; - - // Resource Id of the secondary part of the content description for an edge case of a message - // which is in both draft status and failed status. - private static final int sSecondaryContentDescription = - R.string.failed_message_content_description; - - // 1:1 versus group - private static final int CONV_TYPE_ONE_ON_ONE_INDEX = 0; - private static final int CONV_TYPE_ONE_GROUP_INDEX = 1; - // Direction - private static final int DIRECTION_INCOMING_INDEX = 0; - private static final int DIRECTION_OUTGOING_INDEX = 1; - // Message status - private static final int MESSAGE_STATUS_FAILED_INDEX = 0; - private static final int MESSAGE_STATUS_SUCCESSFUL_INDEX = 1; - private static final int MESSAGE_STATUS_DRAFT_INDEX = 2; - private static final int MESSAGE_STATUS_SENDING_INDEX = 3; - - private static final int WIDTH_FOR_ACCESSIBLE_CONVERSATION_NAME = 600; - - public static String buildContentDescription(final Resources resources, - final ConversationListItemData data, final TextPaint conversationNameViewPaint) { - int messageStatusIndex; - boolean outgoingSnippet = data.getIsMessageTypeOutgoing() || data.getShowDraft(); - if (outgoingSnippet) { - if (data.getShowDraft()) { - messageStatusIndex = MESSAGE_STATUS_DRAFT_INDEX; - } else if (data.getIsSendRequested()) { - messageStatusIndex = MESSAGE_STATUS_SENDING_INDEX; - } else { - messageStatusIndex = data.getIsFailedStatus() ? MESSAGE_STATUS_FAILED_INDEX - : MESSAGE_STATUS_SUCCESSFUL_INDEX; - } - } else { - messageStatusIndex = data.getIsFailedStatus() ? MESSAGE_STATUS_FAILED_INDEX - : MESSAGE_STATUS_SUCCESSFUL_INDEX; - } - - int resId = sPrimaryContentDescriptions - [data.getIsGroup() ? CONV_TYPE_ONE_GROUP_INDEX : CONV_TYPE_ONE_ON_ONE_INDEX] - [outgoingSnippet ? DIRECTION_OUTGOING_INDEX : DIRECTION_INCOMING_INDEX] - [messageStatusIndex]; - - final String snippetText = data.getShowDraft() ? - data.getDraftSnippetText() : data.getSnippetText(); - - final String conversationName = data.getName(); - String senderOrConvName = outgoingSnippet ? conversationName : data.getSnippetSenderName(); - - String primaryContentDescription = resources.getString(resId, senderOrConvName, - snippetText == null ? "" : snippetText, - data.getFormattedTimestamp(), - // This is used only for incoming group messages - conversationName); - String contentDescription = primaryContentDescription; - - // An edge case : for an outgoing message, it might be in both draft status and - // failed status. - if (outgoingSnippet && data.getShowDraft() && data.getIsFailedStatus()) { - StringBuilder contentDescriptionBuilder = new StringBuilder(); - contentDescriptionBuilder.append(primaryContentDescription); - - String secondaryContentDescription = - resources.getString(sSecondaryContentDescription); - contentDescriptionBuilder.append(" "); - contentDescriptionBuilder.append(secondaryContentDescription); - contentDescription = contentDescriptionBuilder.toString(); - } - return contentDescription; - } - - /** - * Fills in the data associated with this view. - * - * @param cursor The cursor from a ConversationList that this view is in, pointing to its - * entry. - */ - public void bind(final Cursor cursor, final HostInterface hostInterface) { - // Update our UI model - mHostInterface = hostInterface; - mData.bind(cursor); - - resetAnimatingState(); - - mSwipeableContainer.setOnClickListener(this); - mSwipeableContainer.setOnLongClickListener(this); - - final Resources resources = getContext().getResources(); - - int color; - final int maxLines; - final Typeface typeface; - final int typefaceStyle = mData.getShowDraft() ? Typeface.ITALIC : Typeface.NORMAL; - final String snippetText = getSnippetText(); - - if (mData.getIsRead() || mData.getShowDraft()) { - maxLines = TextUtils.isEmpty(snippetText) ? 0 : NO_UNREAD_SNIPPET_LINE_COUNT; - color = mListItemReadColor; - typeface = mListItemReadTypeface; - } else { - maxLines = TextUtils.isEmpty(snippetText) ? 0 : UNREAD_SNIPPET_LINE_COUNT; - color = mListItemUnreadColor; - typeface = mListItemUnreadTypeface; - } - - mSnippetTextView.setMaxLines(maxLines); - mSnippetTextView.setTextColor(color); - mSnippetTextView.setTypeface(typeface, typefaceStyle); - mSubjectTextView.setTextColor(color); - mSubjectTextView.setTypeface(typeface, typefaceStyle); - - setSnippet(); - setConversationName(); - setSubject(); - setContentDescription(buildContentDescription(resources, mData, - mConversationNameView.getPaint())); - - final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp(); - // don't show the error state unless we're the default sms app - if (mData.getIsFailedStatus() && isDefaultSmsApp) { - mTimestampTextView.setTextColor(resources.getColor(R.color.conversation_list_error)); - mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle); - int failureMessageId = R.string.message_status_download_failed; - if (mData.getIsMessageTypeOutgoing()) { - failureMessageId = MmsUtils.mapRawStatusToErrorResourceId(mData.getMessageStatus(), - mData.getMessageRawTelephonyStatus()); - } - mTimestampTextView.setText(resources.getString(failureMessageId)); - } else if (mData.getShowDraft() - || mData.getMessageStatus() == MessageData.BUGLE_STATUS_OUTGOING_DRAFT - // also check for unknown status which we get because sometimes the conversation - // row is left with a latest_message_id of a no longer existing message and - // therefore the join values come back as null (or in this case zero). - || mData.getMessageStatus() == MessageData.BUGLE_STATUS_UNKNOWN) { - mTimestampTextView.setTextColor(mListItemReadColor); - mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle); - mTimestampTextView.setText(resources.getString( - R.string.conversation_list_item_view_draft_message)); - } else { - mTimestampTextView.setTextColor(mListItemReadColor); - mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle); - final String formattedTimestamp = mData.getFormattedTimestamp(); - if (mData.getIsSendRequested()) { - mTimestampTextView.setText(R.string.message_status_sending); - } else { - mTimestampTextView.setText(formattedTimestamp); - } - } - - final boolean isSelected = mHostInterface.isConversationSelected(mData.getConversationId()); - setSelected(isSelected); - Uri iconUri = null; - int contactIconVisibility = GONE; - int checkmarkVisiblity = GONE; - int failStatusVisiblity = GONE; - if (isSelected) { - checkmarkVisiblity = VISIBLE; - } else { - contactIconVisibility = VISIBLE; - // Only show the fail icon if it is not a group conversation. - // And also require that we be the default sms app. - if (mData.getIsFailedStatus() && !mData.getIsGroup() && isDefaultSmsApp) { - failStatusVisiblity = VISIBLE; - } - } - if (mData.getIcon() != null) { - iconUri = Uri.parse(mData.getIcon()); - } - mContactIconView.setImageResourceUri(iconUri, mData.getParticipantContactId(), - mData.getParticipantLookupKey(), mData.getOtherParticipantNormalizedDestination()); - mContactIconView.setVisibility(contactIconVisibility); - mContactIconView.setOnLongClickListener(this); - mContactIconView.setClickable(!mHostInterface.isSelectionMode()); - mContactIconView.setLongClickable(!mHostInterface.isSelectionMode()); - - mContactCheckmarkView.setVisibility(checkmarkVisiblity); - mFailedStatusIconView.setVisibility(failStatusVisiblity); - - final Uri previewUri = mData.getShowDraft() ? - mData.getDraftPreviewUri() : mData.getPreviewUri(); - final String previewContentType = mData.getShowDraft() ? - mData.getDraftPreviewContentType() : mData.getPreviewContentType(); - OnClickListener previewClickListener = null; - Uri previewImageUri = null; - int previewImageVisibility = GONE; - int audioPreviewVisiblity = GONE; - if (previewUri != null && !TextUtils.isEmpty(previewContentType)) { - if (ContentType.isAudioType(previewContentType)) { - mAudioAttachmentView.bind(previewUri, false); - audioPreviewVisiblity = VISIBLE; - } else if (ContentType.isVideoType(previewContentType)) { - previewImageUri = UriUtil.getUriForResourceId( - getContext(), R.drawable.ic_preview_play); - previewClickListener = fullScreenPreviewClickListener; - previewImageVisibility = VISIBLE; - } else if (ContentType.isImageType(previewContentType)) { - previewImageUri = previewUri; - previewClickListener = fullScreenPreviewClickListener; - previewImageVisibility = VISIBLE; - } - } - - final int imageSize = resources.getDimensionPixelSize( - R.dimen.conversation_list_image_preview_size); - mImagePreviewView.setImageResourceId( - new UriImageRequestDescriptor(previewImageUri, imageSize, imageSize, - true /* allowCompression */, false /* isStatic */, false /*cropToCircle*/, - ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */, - ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */)); - mImagePreviewView.setOnLongClickListener(this); - mImagePreviewView.setVisibility(previewImageVisibility); - mImagePreviewView.setOnClickListener(previewClickListener); - mAudioAttachmentView.setOnLongClickListener(this); - mAudioAttachmentView.setVisibility(audioPreviewVisiblity); - - final int notificationBellVisiblity = mData.getNotificationEnabled() ? GONE : VISIBLE; - mNotificationBellView.setVisibility(notificationBellVisiblity); - } - - public boolean isSwipeAnimatable() { - return mHostInterface.isSwipeAnimatable(); - } - - @VisibleForAnimation - public float getSwipeTranslationX() { - return mSwipeableContainer.getTranslationX(); - } - - @VisibleForAnimation - public void setSwipeTranslationX(final float translationX) { - mSwipeableContainer.setTranslationX(translationX); - if (translationX == 0) { - mCrossSwipeBackground.setVisibility(View.GONE); - mCrossSwipeArchiveLeftImageView.setVisibility(GONE); - mCrossSwipeArchiveRightImageView.setVisibility(GONE); - - mSwipeableContainer.setBackgroundColor(Color.TRANSPARENT); - } else { - mCrossSwipeBackground.setVisibility(View.VISIBLE); - if (translationX > 0) { - mCrossSwipeArchiveLeftImageView.setVisibility(VISIBLE); - mCrossSwipeArchiveRightImageView.setVisibility(GONE); - } else { - mCrossSwipeArchiveLeftImageView.setVisibility(GONE); - mCrossSwipeArchiveRightImageView.setVisibility(VISIBLE); - } - mSwipeableContainer.setBackgroundResource(R.drawable.swipe_shadow_drag); - } - } - - public void onSwipeComplete() { - final String conversationId = mData.getConversationId(); - UpdateConversationArchiveStatusAction.archiveConversation(conversationId); - - final Runnable undoRunnable = new Runnable() { - @Override - public void run() { - UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId); - } - }; - final String message = getResources().getString(R.string.archived_toast_message, 1); - UiUtils.showSnackBar(getContext(), getRootView(), message, undoRunnable, - SnackBar.Action.SNACK_BAR_UNDO, - mHostInterface.getSnackBarInteractions()); - } - - private void setShortAndLongClickable(final boolean clickable) { - setClickable(clickable); - setLongClickable(clickable); - } - - private void resetAnimatingState() { - mAnimatingCount = 0; - setShortAndLongClickable(true); - setSwipeTranslationX(0); - } - - /** - * Notifies this view that it is undergoing animation. This view should disable its click - * targets. - * - * The animating counter is used to reset the swipe controller when the counter becomes 0. A - * positive counter also makes the view not clickable. - */ - public final void setAnimating(final boolean animating) { - final int oldAnimatingCount = mAnimatingCount; - if (animating) { - mAnimatingCount++; - } else { - mAnimatingCount--; - if (mAnimatingCount < 0) { - mAnimatingCount = 0; - } - } - - if (mAnimatingCount == 0) { - // New count is 0. All animations ended. - setShortAndLongClickable(true); - } else if (oldAnimatingCount == 0) { - // New count is > 0. Waiting for some animations to end. - setShortAndLongClickable(false); - } - } - - public boolean isAnimating() { - return mAnimatingCount > 0; - } - - /** - * {@inheritDoc} from OnClickListener - */ - @Override - public void onClick(final View v) { - processClick(v, false); - } - - /** - * {@inheritDoc} from OnLongClickListener - */ - @Override - public boolean onLongClick(final View v) { - return processClick(v, true); - } - - private boolean processClick(final View v, final boolean isLongClick) { - Assert.isTrue(v == mSwipeableContainer || v == mContactIconView || v == mImagePreviewView); - Assert.notNull(mData.getName()); - - if (mHostInterface != null) { - mHostInterface.onConversationClicked(mData, isLongClick, this); - return true; - } - return false; - } - - public View getSwipeableContent() { - return mSwipeableContent; - } - - public View getContactIconView() { - return mContactIconView; - } - - private String getSnippetText() { - String snippetText = mData.getShowDraft() ? - mData.getDraftSnippetText() : mData.getSnippetText(); - final String previewContentType = mData.getShowDraft() ? - mData.getDraftPreviewContentType() : mData.getPreviewContentType(); - if (TextUtils.isEmpty(snippetText)) { - Resources resources = getResources(); - // Use the attachment type as a snippet so the preview doesn't look odd - if (ContentType.isAudioType(previewContentType)) { - snippetText = resources.getString(R.string.conversation_list_snippet_audio_clip); - } else if (ContentType.isImageType(previewContentType)) { - snippetText = resources.getString(R.string.conversation_list_snippet_picture); - } else if (ContentType.isVideoType(previewContentType)) { - snippetText = resources.getString(R.string.conversation_list_snippet_video); - } else if (ContentType.isVCardType(previewContentType)) { - snippetText = resources.getString(R.string.conversation_list_snippet_vcard); - } - } - return snippetText; - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ConversationListSwipeHelper.java b/src/com/android/messaging/ui/conversationlist/ConversationListSwipeHelper.java deleted file mode 100644 index 4988259..0000000 --- a/src/com/android/messaging/ui/conversationlist/ConversationListSwipeHelper.java +++ /dev/null @@ -1,462 +0,0 @@ -/* - * 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.conversationlist; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.TimeInterpolator; -import android.content.Context; -import android.content.res.Resources; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.OnItemTouchListener; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; - -import com.android.messaging.R; -import com.android.messaging.util.Assert; -import com.android.messaging.util.UiUtils; - -/** - * Animation and touch helper class for Conversation List swipe. - */ -public class ConversationListSwipeHelper implements OnItemTouchListener { - private static final int UNIT_SECONDS = 1000; - private static final boolean ANIMATING = true; - - private static final float ERROR_FACTOR_MULTIPLIER = 1.2f; - private static final float PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.4f; - private static final float FLING_PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.05f; - - private static final int SWIPE_DIRECTION_NONE = 0; - private static final int SWIPE_DIRECTION_LEFT = 1; - private static final int SWIPE_DIRECTION_RIGHT = 2; - - private final RecyclerView mRecyclerView; - private final long mDefaultRestoreAnimationDuration; - private final long mDefaultDismissAnimationDuration; - private final long mMaxTranslationAnimationDuration; - private final int mTouchSlop; - private final int mMinimumFlingVelocity; - private final int mMaximumFlingVelocity; - - /* Valid throughout a single gesture. */ - private VelocityTracker mVelocityTracker; - private float mInitialX; - private float mInitialY; - private boolean mIsSwiping; - private ConversationListItemView mListItemView; - - public ConversationListSwipeHelper(final RecyclerView recyclerView) { - mRecyclerView = recyclerView; - - final Context context = mRecyclerView.getContext(); - final Resources res = context.getResources(); - mDefaultRestoreAnimationDuration = res.getInteger(R.integer.swipe_duration_ms); - mDefaultDismissAnimationDuration = res.getInteger(R.integer.swipe_duration_ms); - mMaxTranslationAnimationDuration = res.getInteger(R.integer.swipe_duration_ms); - - final ViewConfiguration viewConfiguration = ViewConfiguration.get(context); - mTouchSlop = viewConfiguration.getScaledPagingTouchSlop(); - mMaximumFlingVelocity = Math.min( - viewConfiguration.getScaledMaximumFlingVelocity(), - res.getInteger(R.integer.swipe_max_fling_velocity_px_per_s)); - mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); - } - - @Override - public boolean onInterceptTouchEvent(final RecyclerView recyclerView, final MotionEvent event) { - if (event.getPointerCount() > 1) { - // Ignore subsequent pointers. - return false; - } - - // We are not yet tracking a swipe gesture. Begin detection by spying on - // touch events bubbling down to our children. - final int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - if (!hasGestureSwipeTarget()) { - onGestureStart(); - - mVelocityTracker.addMovement(event); - mInitialX = event.getX(); - mInitialY = event.getY(); - - final View viewAtPoint = mRecyclerView.findChildViewUnder(mInitialX, mInitialY); - final ConversationListItemView child = (ConversationListItemView) viewAtPoint; - if (viewAtPoint instanceof ConversationListItemView && - child != null && child.isSwipeAnimatable()) { - // Begin detecting swipe on the target for the rest of the gesture. - mListItemView = child; - if (mListItemView.isAnimating()) { - mListItemView = null; - } - } else { - mListItemView = null; - } - } - break; - case MotionEvent.ACTION_MOVE: - if (hasValidGestureSwipeTarget()) { - mVelocityTracker.addMovement(event); - - final int historicalCount = event.getHistorySize(); - // First consume the historical events, then consume the current ones. - for (int i = 0; i < historicalCount + 1; i++) { - float currX; - float currY; - if (i < historicalCount) { - currX = event.getHistoricalX(i); - currY = event.getHistoricalY(i); - } else { - currX = event.getX(); - currY = event.getY(); - } - final float deltaX = currX - mInitialX; - final float deltaY = currY - mInitialY; - final float absDeltaX = Math.abs(deltaX); - final float absDeltaY = Math.abs(deltaY); - - if (!mIsSwiping && absDeltaY > mTouchSlop - && absDeltaY > (ERROR_FACTOR_MULTIPLIER * absDeltaX)) { - // Stop detecting swipe for the remainder of this gesture. - onGestureEnd(); - return false; - } - - if (absDeltaX > mTouchSlop) { - // Swipe detected. Return true so we can handle the gesture in - // onTouchEvent. - mIsSwiping = true; - - // We don't want to suddenly jump the slop distance. - mInitialX = event.getX(); - mInitialY = event.getY(); - - onSwipeGestureStart(mListItemView); - return true; - } - } - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (hasGestureSwipeTarget()) { - onGestureEnd(); - } - break; - } - - // Start intercepting touch events from children if we detect a swipe. - return mIsSwiping; - } - - @Override - public void onTouchEvent(final RecyclerView recyclerView, final MotionEvent event) { - // We should only be here if we intercepted the touch due to swipe. - Assert.isTrue(mIsSwiping); - - // We are now tracking a swipe gesture. - mVelocityTracker.addMovement(event); - - final int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_OUTSIDE: - case MotionEvent.ACTION_MOVE: - if (hasValidGestureSwipeTarget()) { - mListItemView.setSwipeTranslationX(event.getX() - mInitialX); - } - break; - case MotionEvent.ACTION_UP: - if (hasValidGestureSwipeTarget()) { - final float maxVelocity = mMaximumFlingVelocity; - mVelocityTracker.computeCurrentVelocity(UNIT_SECONDS, maxVelocity); - final float velocityX = getLastComputedXVelocity(); - - final float translationX = mListItemView.getSwipeTranslationX(); - - int swipeDirection = SWIPE_DIRECTION_NONE; - if (translationX != 0) { - swipeDirection = - translationX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT; - } else if (velocityX != 0) { - swipeDirection = - velocityX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT; - } - - final boolean fastEnough = isTargetSwipedFastEnough(); - final boolean farEnough = isTargetSwipedFarEnough(); - - final boolean shouldDismiss = (fastEnough || farEnough); - - if (shouldDismiss) { - if (fastEnough) { - animateDismiss(mListItemView, velocityX); - } else { - animateDismiss(mListItemView, swipeDirection); - } - } else { - animateRestore(mListItemView, velocityX); - } - - onSwipeGestureEnd(mListItemView, - shouldDismiss ? swipeDirection : SWIPE_DIRECTION_NONE); - } else { - onGestureEnd(); - } - break; - case MotionEvent.ACTION_CANCEL: - if (hasValidGestureSwipeTarget()) { - animateRestore(mListItemView, 0f); - onSwipeGestureEnd(mListItemView, SWIPE_DIRECTION_NONE); - } else { - onGestureEnd(); - } - break; - } - } - - - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - } - - /** - * We have started to intercept a series of touch events. - */ - private void onGestureStart() { - mIsSwiping = false; - // Work around bug in RecyclerView that sends two identical ACTION_DOWN - // events to #onInterceptTouchEvent. - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.clear(); - } - - /** - * The series of touch events has been detected as a swipe. - * - * Now that the gesture is a swipe, we will begin translating the view of the - * given viewHolder. - */ - private void onSwipeGestureStart(final ConversationListItemView itemView) { - mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); - setHardwareAnimatingLayerType(itemView, ANIMATING); - itemView.setAnimating(true); - } - - /** - * The current swipe gesture is complete. - */ - private void onSwipeGestureEnd(final ConversationListItemView itemView, - final int swipeDirection) { - if (swipeDirection == SWIPE_DIRECTION_RIGHT || swipeDirection == SWIPE_DIRECTION_LEFT) { - itemView.onSwipeComplete(); - } - - // Balances out onSwipeGestureStart. - itemView.setAnimating(false); - - onGestureEnd(); - } - - /** - * The series of touch events has ended in an {@link MotionEvent#ACTION_UP} - * or {@link MotionEvent#ACTION_CANCEL}. - */ - private void onGestureEnd() { - mVelocityTracker.recycle(); - mVelocityTracker = null; - mIsSwiping = false; - mListItemView = null; - } - - /** - * A swipe animation has started. - */ - private void onSwipeAnimationStart(final ConversationListItemView itemView) { - // Disallow interactions. - itemView.setAnimating(true); - ViewCompat.setHasTransientState(itemView, true); - setHardwareAnimatingLayerType(itemView, ANIMATING); - } - - /** - * The swipe animation has ended. - */ - private void onSwipeAnimationEnd(final ConversationListItemView itemView) { - // Restore interactions. - itemView.setAnimating(false); - ViewCompat.setHasTransientState(itemView, false); - setHardwareAnimatingLayerType(itemView, !ANIMATING); - } - - /** - * Animate the dismissal of the given item. The given velocityX is taken into consideration for - * the animation duration. Whether the item is dismissed to the left or right is dependent on - * the given velocityX. - */ - private void animateDismiss(final ConversationListItemView itemView, final float velocityX) { - Assert.isTrue(velocityX != 0); - final int direction = velocityX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT; - animateDismiss(itemView, direction, velocityX); - } - - /** - * Animate the dismissal of the given item. The velocityX is assumed to be 0. - */ - private void animateDismiss(final ConversationListItemView itemView, final int swipeDirection) { - animateDismiss(itemView, swipeDirection, 0f); - } - - /** - * Animate the dismissal of the given item. - */ - private void animateDismiss(final ConversationListItemView itemView, - final int swipeDirection, final float velocityX) { - Assert.isTrue(swipeDirection != SWIPE_DIRECTION_NONE); - - onSwipeAnimationStart(itemView); - - final float animateTo = (swipeDirection == SWIPE_DIRECTION_RIGHT) ? - mRecyclerView.getWidth() : -mRecyclerView.getWidth(); - final long duration; - if (velocityX != 0) { - final float deltaX = animateTo - itemView.getSwipeTranslationX(); - duration = calculateTranslationDuration(deltaX, velocityX); - } else { - duration = mDefaultDismissAnimationDuration; - } - - final ObjectAnimator animator = getSwipeTranslationXAnimator( - itemView, animateTo, duration, UiUtils.DEFAULT_INTERPOLATOR); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - onSwipeAnimationEnd(itemView); - } - }); - animator.start(); - } - - /** - * Animate the bounce back of the given item. - */ - private void animateRestore(final ConversationListItemView itemView, - final float velocityX) { - onSwipeAnimationStart(itemView); - - final float translationX = itemView.getSwipeTranslationX(); - final long duration; - if (velocityX != 0 // Has velocity. - && velocityX > 0 != translationX > 0) { // Right direction. - duration = calculateTranslationDuration(translationX, velocityX); - } else { - duration = mDefaultRestoreAnimationDuration; - } - final ObjectAnimator animator = getSwipeTranslationXAnimator( - itemView, 0f, duration, UiUtils.DEFAULT_INTERPOLATOR); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - onSwipeAnimationEnd(itemView); - } - }); - animator.start(); - } - - /** - * Create and start an animator that animates the given view's translationX - * from its current value to the value given by animateTo. - */ - private ObjectAnimator getSwipeTranslationXAnimator(final ConversationListItemView itemView, - final float animateTo, final long duration, final TimeInterpolator interpolator) { - final ObjectAnimator animator = - ObjectAnimator.ofFloat(itemView, "swipeTranslationX", animateTo); - animator.setDuration(duration); - animator.setInterpolator(interpolator); - return animator; - } - - /** - * Determine if the swipe has enough velocity to be dismissed. - */ - private boolean isTargetSwipedFastEnough() { - final float velocityX = getLastComputedXVelocity(); - final float velocityY = mVelocityTracker.getYVelocity(); - final float minVelocity = mMinimumFlingVelocity; - final float translationX = mListItemView.getSwipeTranslationX(); - final float width = mListItemView.getWidth(); - return (Math.abs(velocityX) > minVelocity) // Fast enough. - && (Math.abs(velocityX) > Math.abs(velocityY)) // Not unintentional. - && (velocityX > 0) == (translationX > 0) // Right direction. - && Math.abs(translationX) > - FLING_PERCENTAGE_OF_WIDTH_TO_DISMISS * width; // Enough movement. - } - - /** - * Only used during a swipe gesture. Determine if the swipe has enough distance to be - * dismissed. - */ - private boolean isTargetSwipedFarEnough() { - final float velocityX = getLastComputedXVelocity(); - - final float translationX = mListItemView.getSwipeTranslationX(); - final float width = mListItemView.getWidth(); - - return (velocityX >= 0) == (translationX > 0) // Right direction. - && Math.abs(translationX) > - PERCENTAGE_OF_WIDTH_TO_DISMISS * width; // Enough movement. - } - - private long calculateTranslationDuration(final float deltaPosition, final float velocity) { - Assert.isTrue(velocity != 0); - final float durationInSeconds = Math.abs(deltaPosition / velocity); - return Math.min((int) (durationInSeconds * UNIT_SECONDS), mMaxTranslationAnimationDuration); - } - - private boolean hasGestureSwipeTarget() { - return mListItemView != null; - } - - private boolean hasValidGestureSwipeTarget() { - return hasGestureSwipeTarget() && mListItemView.getParent() == mRecyclerView; - } - - /** - * Enable a hardware layer for the it view and build that layer. - */ - private void setHardwareAnimatingLayerType(final ConversationListItemView itemView, - final boolean animating) { - if (animating) { - itemView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - if (itemView.getWindowToken() != null) { - itemView.buildLayer(); - } - } else { - itemView.setLayerType(View.LAYER_TYPE_NONE, null); - } - } - - private float getLastComputedXVelocity() { - return mVelocityTracker.getXVelocity(); - } -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/conversationlist/ForwardMessageActivity.java b/src/com/android/messaging/ui/conversationlist/ForwardMessageActivity.java deleted file mode 100644 index 61e3640..0000000 --- a/src/com/android/messaging/ui/conversationlist/ForwardMessageActivity.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.conversationlist; - -import android.app.Fragment; -import android.os.Bundle; - -import com.android.messaging.datamodel.data.ConversationListData; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.ui.BaseBugleActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost; -import com.android.messaging.util.Assert; - -/** - * An activity that lets the user forward a SMS/MMS message by picking from a conversation in the - * conversation list. - */ -public class ForwardMessageActivity extends BaseBugleActivity - implements ConversationListFragmentHost { - private MessageData mDraftMessage; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final ConversationListFragment fragment = - ConversationListFragment.createForwardMessageConversationListFragment(); - getFragmentManager().beginTransaction().add(android.R.id.content, fragment).commit(); - mDraftMessage = getIntent().getParcelableExtra(UIIntents.UI_INTENT_EXTRA_DRAFT_DATA); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - Assert.isTrue(fragment instanceof ConversationListFragment); - final ConversationListFragment clf = (ConversationListFragment) fragment; - clf.setHost(this); - } - - @Override - public void onConversationClick(final ConversationListData listData, - final ConversationListItemData conversationListItemData, - final boolean isLongClick, final ConversationListItemView converastionView) { - UIIntents.get().launchConversationActivity( - this, conversationListItemData.getConversationId(), mDraftMessage); - } - - @Override - public void onCreateConversationClick() { - UIIntents.get().launchCreateNewConversationActivity(this, mDraftMessage); - } - - @Override - public boolean isConversationSelected(final String conversationId) { - return false; - } - - @Override - public boolean isSwipeAnimatable() { - return false; - } - - @Override - public boolean isSelectionMode() { - return false; - } -} diff --git a/src/com/android/messaging/ui/conversationlist/MultiSelectActionModeCallback.java b/src/com/android/messaging/ui/conversationlist/MultiSelectActionModeCallback.java deleted file mode 100644 index bfeec51..0000000 --- a/src/com/android/messaging/ui/conversationlist/MultiSelectActionModeCallback.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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.conversationlist; - -import android.support.v4.util.ArrayMap; -import android.text.TextUtils; -import android.view.ActionMode; -import android.view.ActionMode.Callback; -import android.view.Menu; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ConversationListData; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.util.Assert; - -import java.util.Collection; -import java.util.HashSet; - -public class MultiSelectActionModeCallback implements Callback { - private HashSet<String> mBlockedSet; - - public interface Listener { - void onActionBarDelete(Collection<SelectedConversation> conversations); - void onActionBarArchive(Iterable<SelectedConversation> conversations, - boolean isToArchive); - void onActionBarNotification(Iterable<SelectedConversation> conversations, - boolean isNotificationOn); - void onActionBarAddContact(final SelectedConversation conversation); - void onActionBarBlock(final SelectedConversation conversation); - void onActionBarHome(); - } - - static class SelectedConversation { - public final String conversationId; - public final long timestamp; - public final String icon; - public final String otherParticipantNormalizedDestination; - public final CharSequence participantLookupKey; - public final boolean isGroup; - public final boolean isArchived; - public final boolean notificationEnabled; - public SelectedConversation(ConversationListItemData data) { - conversationId = data.getConversationId(); - timestamp = data.getTimestamp(); - icon = data.getIcon(); - otherParticipantNormalizedDestination = data.getOtherParticipantNormalizedDestination(); - participantLookupKey = data.getParticipantLookupKey(); - isGroup = data.getIsGroup(); - isArchived = data.getIsArchived(); - notificationEnabled = data.getNotificationEnabled(); - } - } - - private final ArrayMap<String, SelectedConversation> mSelectedConversations; - - private Listener mListener; - private MenuItem mArchiveMenuItem; - private MenuItem mUnarchiveMenuItem; - private MenuItem mAddContactMenuItem; - private MenuItem mBlockMenuItem; - private MenuItem mNotificationOnMenuItem; - private MenuItem mNotificationOffMenuItem; - private boolean mHasInflated; - - public MultiSelectActionModeCallback(final Listener listener) { - mListener = listener; - mSelectedConversations = new ArrayMap<>(); - - } - - @Override - public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { - actionMode.getMenuInflater().inflate(R.menu.conversation_list_fragment_select_menu, menu); - mArchiveMenuItem = menu.findItem(R.id.action_archive); - mUnarchiveMenuItem = menu.findItem(R.id.action_unarchive); - mAddContactMenuItem = menu.findItem(R.id.action_add_contact); - mBlockMenuItem = menu.findItem(R.id.action_block); - mNotificationOffMenuItem = menu.findItem(R.id.action_notification_off); - mNotificationOnMenuItem = menu.findItem(R.id.action_notification_on); - mHasInflated = true; - updateActionIconsVisiblity(); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { - return true; - } - - @Override - public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { - switch(menuItem.getItemId()) { - case R.id.action_delete: - mListener.onActionBarDelete(mSelectedConversations.values()); - return true; - case R.id.action_archive: - mListener.onActionBarArchive(mSelectedConversations.values(), true); - return true; - case R.id.action_unarchive: - mListener.onActionBarArchive(mSelectedConversations.values(), false); - return true; - case R.id.action_notification_off: - mListener.onActionBarNotification(mSelectedConversations.values(), false); - return true; - case R.id.action_notification_on: - mListener.onActionBarNotification(mSelectedConversations.values(), true); - return true; - case R.id.action_add_contact: - Assert.isTrue(mSelectedConversations.size() == 1); - mListener.onActionBarAddContact(mSelectedConversations.valueAt(0)); - return true; - case R.id.action_block: - Assert.isTrue(mSelectedConversations.size() == 1); - mListener.onActionBarBlock(mSelectedConversations.valueAt(0)); - return true; - case android.R.id.home: - mListener.onActionBarHome(); - return true; - default: - return false; - } - } - - @Override - public void onDestroyActionMode(ActionMode actionMode) { - mListener = null; - mSelectedConversations.clear(); - mHasInflated = false; - } - - public void toggleSelect(final ConversationListData listData, - final ConversationListItemData conversationListItemData) { - Assert.notNull(conversationListItemData); - mBlockedSet = listData.getBlockedParticipants(); - final String id = conversationListItemData.getConversationId(); - if (mSelectedConversations.containsKey(id)) { - mSelectedConversations.remove(id); - } else { - mSelectedConversations.put(id, new SelectedConversation(conversationListItemData)); - } - - if (mSelectedConversations.isEmpty()) { - mListener.onActionBarHome(); - } else { - updateActionIconsVisiblity(); - } - } - - public boolean isSelected(final String selectedId) { - return mSelectedConversations.containsKey(selectedId); - } - - private void updateActionIconsVisiblity() { - if (!mHasInflated) { - return; - } - - if (mSelectedConversations.size() == 1) { - final SelectedConversation conversation = mSelectedConversations.valueAt(0); - // The look up key is a key given to us by contacts app, so if we have a look up key, - // we know that the participant is already in contacts. - final boolean isInContacts = !TextUtils.isEmpty(conversation.participantLookupKey); - mAddContactMenuItem.setVisible(!conversation.isGroup && !isInContacts); - // ParticipantNormalizedDestination is always null for group conversations. - final String otherParticipant = conversation.otherParticipantNormalizedDestination; - mBlockMenuItem.setVisible(otherParticipant != null - && !mBlockedSet.contains(otherParticipant)); - } else { - mBlockMenuItem.setVisible(false); - mAddContactMenuItem.setVisible(false); - } - - boolean hasCurrentlyArchived = false; - boolean hasCurrentlyUnarchived = false; - boolean hasCurrentlyOnNotification = false; - boolean hasCurrentlyOffNotification = false; - final Iterable<SelectedConversation> conversations = mSelectedConversations.values(); - for (final SelectedConversation conversation : conversations) { - if (conversation.notificationEnabled) { - hasCurrentlyOnNotification = true; - } else { - hasCurrentlyOffNotification = true; - } - - if (conversation.isArchived) { - hasCurrentlyArchived = true; - } else { - hasCurrentlyUnarchived = true; - } - - // If we found at least one of each example we don't need to keep looping. - if (hasCurrentlyOffNotification && hasCurrentlyOnNotification && - hasCurrentlyArchived && hasCurrentlyUnarchived) { - break; - } - } - // If we have notification off conversations we show on button, if we have notification on - // conversation we show off button. We can show both if we have a mixture. - mNotificationOffMenuItem.setVisible(hasCurrentlyOnNotification); - mNotificationOnMenuItem.setVisible(hasCurrentlyOffNotification); - - mArchiveMenuItem.setVisible(hasCurrentlyUnarchived); - mUnarchiveMenuItem.setVisible(hasCurrentlyArchived); - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ShareIntentActivity.java b/src/com/android/messaging/ui/conversationlist/ShareIntentActivity.java deleted file mode 100644 index ef7fcef..0000000 --- a/src/com/android/messaging/ui/conversationlist/ShareIntentActivity.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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.conversationlist; - -import android.app.Fragment; -import android.content.ContentResolver; -import android.content.Intent; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.data.PendingAttachmentData; -import com.android.messaging.ui.BaseBugleActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.MediaMetadataRetrieverWrapper; - -import java.io.IOException; -import java.util.ArrayList; - -public class ShareIntentActivity extends BaseBugleActivity implements - ShareIntentFragment.HostInterface { - - private MessageData mDraftMessage; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Intent intent = getIntent(); - if (Intent.ACTION_SEND.equals(intent.getAction()) && - (!TextUtils.isEmpty(intent.getStringExtra("address")) || - !TextUtils.isEmpty(intent.getStringExtra(Intent.EXTRA_EMAIL)))) { - // This is really more like a SENDTO intent because a destination is supplied. - // It's coming through the SEND intent because that's the intent that is used - // when invoking the chooser with Intent.createChooser(). - final Intent convIntent = UIIntents.get().getLaunchConversationActivityIntent(this); - // Copy the important items from the original intent to the new intent. - convIntent.putExtras(intent); - convIntent.setAction(Intent.ACTION_SENDTO); - convIntent.setDataAndType(intent.getData(), intent.getType()); - // We have to fire off the intent and finish before trying to show the fragment, - // otherwise we get some flashing. - startActivity(convIntent); - finish(); - return; - } - new ShareIntentFragment().show(getFragmentManager(), "ShareIntentFragment"); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - final Intent intent = getIntent(); - final String action = intent.getAction(); - if (Intent.ACTION_SEND.equals(action)) { - final Uri contentUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); - final String contentType = extractContentType(contentUri, intent.getType()); - if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { - LogUtil.d(LogUtil.BUGLE_TAG, String.format( - "onAttachFragment: contentUri=%s, intent.getType()=%s, inferredType=%s", - contentUri, intent.getType(), contentType)); - } - if (ContentType.TEXT_PLAIN.equals(contentType)) { - final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); - if (sharedText != null) { - mDraftMessage = MessageData.createSharedMessage(sharedText); - } else { - mDraftMessage = null; - } - } else if (ContentType.isImageType(contentType) || - ContentType.isVCardType(contentType) || - ContentType.isAudioType(contentType) || - ContentType.isVideoType(contentType)) { - if (contentUri != null) { - mDraftMessage = MessageData.createSharedMessage(null); - addSharedImagePartToDraft(contentType, contentUri); - } else { - mDraftMessage = null; - } - } else { - // Unsupported content type. - Assert.fail("Unsupported shared content type for " + contentUri + ": " + contentType - + " (" + intent.getType() + ")"); - } - } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { - final String contentType = intent.getType(); - if (ContentType.isImageType(contentType)) { - // Handle sharing multiple images. - final ArrayList<Uri> imageUris = intent.getParcelableArrayListExtra( - Intent.EXTRA_STREAM); - if (imageUris != null && imageUris.size() > 0) { - mDraftMessage = MessageData.createSharedMessage(null); - for (final Uri imageUri : imageUris) { - final String actualContentType = extractContentType(imageUri, contentType); - addSharedImagePartToDraft(actualContentType, imageUri); - } - } else { - mDraftMessage = null; - } - } else { - // Unsupported content type. - Assert.fail("Unsupported shared content type: " + contentType); - } - } else { - // Unsupported action. - Assert.fail("Unsupported action type for sharing: " + action); - } - } - - private static String extractContentType(final Uri uri, final String contentType) { - if (uri == null) { - return contentType; - } - // First try looking at file extension. This is less reliable in some ways but it's - // recommended by - // https://developer.android.com/training/secure-file-sharing/retrieve-info.html - // Some implementations of MediaMetadataRetriever get things horribly - // wrong for common formats such as jpeg (reports as video/ffmpeg) - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - final String typeFromExtension = resolver.getType(uri); - if (typeFromExtension != null) { - return typeFromExtension; - } - final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); - try { - retriever.setDataSource(uri); - final String extractedType = retriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_MIMETYPE); - if (extractedType != null) { - return extractedType; - } - } catch (final IOException e) { - LogUtil.i(LogUtil.BUGLE_TAG, "Could not determine type of " + uri, e); - } finally { - retriever.release(); - } - return contentType; - } - - private void addSharedImagePartToDraft(final String contentType, final Uri imageUri) { - mDraftMessage.addPart(PendingAttachmentData.createPendingAttachmentData(contentType, - imageUri)); - } - - @Override - public void onConversationClick(final ConversationListItemData conversationListItemData) { - UIIntents.get().launchConversationActivity( - this, conversationListItemData.getConversationId(), mDraftMessage); - finish(); - } - - @Override - public void onCreateConversationClick() { - UIIntents.get().launchCreateNewConversationActivity(this, mDraftMessage); - finish(); - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ShareIntentAdapter.java b/src/com/android/messaging/ui/conversationlist/ShareIntentAdapter.java deleted file mode 100644 index e894145..0000000 --- a/src/com/android/messaging/ui/conversationlist/ShareIntentAdapter.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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.conversationlist; - -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.datamodel.data.PersonItemData; -import com.android.messaging.ui.CursorRecyclerAdapter; -import com.android.messaging.ui.PersonItemView; -import com.android.messaging.ui.PersonItemView.PersonItemViewListener; -import com.android.messaging.util.PhoneUtils; - -/** - * Turn conversation rows into PeopleItemViews - */ -public class ShareIntentAdapter - extends CursorRecyclerAdapter<ShareIntentAdapter.ShareIntentViewHolder> { - - public interface HostInterface { - void onConversationClicked(final ConversationListItemData conversationListItemData); - } - - private final HostInterface mHostInterface; - - public ShareIntentAdapter(final Context context, final Cursor cursor, - final HostInterface hostInterface) { - super(context, cursor, 0); - mHostInterface = hostInterface; - setHasStableIds(true); - } - - @Override - public void bindViewHolder(final ShareIntentViewHolder holder, final Context context, - final Cursor cursor) { - holder.bind(cursor); - } - - @Override - public ShareIntentViewHolder createViewHolder(final Context context, - final ViewGroup parent, final int viewType) { - final PersonItemView itemView = (PersonItemView) LayoutInflater.from(context).inflate( - R.layout.people_list_item_view, null); - return new ShareIntentViewHolder(itemView); - } - - /** - * Holds a PersonItemView and keeps it synced with a ConversationListItemData. - */ - public class ShareIntentViewHolder extends RecyclerView.ViewHolder implements - PersonItemView.PersonItemViewListener { - private final ConversationListItemData mData = new ConversationListItemData(); - private final PersonItemData mItemData = new PersonItemData() { - @Override - public Uri getAvatarUri() { - return mData.getIcon() == null ? null : Uri.parse(mData.getIcon()); - } - - @Override - public String getDisplayName() { - return mData.getName(); - } - - @Override - public String getDetails() { - final String conversationName = mData.getName(); - final String conversationPhone = PhoneUtils.getDefault().formatForDisplay( - mData.getOtherParticipantNormalizedDestination()); - if (conversationPhone == null || conversationPhone.equals(conversationName)) { - return null; - } - return conversationPhone; - } - - @Override - public Intent getClickIntent() { - return null; - } - - @Override - public long getContactId() { - return ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED; - } - - @Override - public String getLookupKey() { - return null; - } - - @Override - public String getNormalizedDestination() { - return null; - } - }; - - public ShareIntentViewHolder(final PersonItemView itemView) { - super(itemView); - itemView.setListener(this); - } - - public void bind(Cursor cursor) { - mData.bind(cursor); - ((PersonItemView) itemView).bind(mItemData); - } - - @Override - public void onPersonClicked(PersonItemData data) { - mHostInterface.onConversationClicked(mData); - } - - @Override - public boolean onPersonLongClicked(PersonItemData data) { - return false; - } - } -} diff --git a/src/com/android/messaging/ui/conversationlist/ShareIntentFragment.java b/src/com/android/messaging/ui/conversationlist/ShareIntentFragment.java deleted file mode 100644 index bc549ea..0000000 --- a/src/com/android/messaging/ui/conversationlist/ShareIntentFragment.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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.conversationlist; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.database.Cursor; -import android.os.Bundle; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.messaging.R; -import com.android.messaging.datamodel.binding.Binding; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.data.ConversationListData; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener; -import com.android.messaging.ui.ListEmptyView; -import com.android.messaging.datamodel.DataModel; - -/** - * Allow user to pick conversation to which an incoming attachment will be shared. - */ -public class ShareIntentFragment extends DialogFragment implements ConversationListDataListener, - ShareIntentAdapter.HostInterface { - public static final String HIDE_NEW_CONVERSATION_BUTTON_KEY = "hide_conv_button_key"; - - public interface HostInterface { - public void onConversationClick(final ConversationListItemData conversationListItemData); - public void onCreateConversationClick(); - } - - private final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this); - private RecyclerView mRecyclerView; - private ListEmptyView mEmptyListMessageView; - private ShareIntentAdapter mAdapter; - private HostInterface mHost; - private boolean mDismissed; - - /** - * {@inheritDoc} from Fragment - */ - @Override - public Dialog onCreateDialog(final Bundle bundle) { - final Activity activity = getActivity(); - final LayoutInflater inflater = activity.getLayoutInflater(); - View view = inflater.inflate(R.layout.share_intent_conversation_list_view, null); - mEmptyListMessageView = (ListEmptyView) view.findViewById(R.id.no_conversations_view); - mEmptyListMessageView.setImageHint(R.drawable.ic_oobe_conv_list); - // The default behavior for default layout param generation by LinearLayoutManager is to - // provide width and height of WRAP_CONTENT, but this is not desirable for - // ShareIntentFragment; the view in each row should be a width of MATCH_PARENT so that - // the entire row is tappable. - final LinearLayoutManager manager = new LinearLayoutManager(activity) { - @Override - public RecyclerView.LayoutParams generateDefaultLayoutParams() { - return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - } - }; - mListBinding.getData().init(getLoaderManager(), mListBinding); - mAdapter = new ShareIntentAdapter(activity, null, this); - mRecyclerView = (RecyclerView) view.findViewById(android.R.id.list); - mRecyclerView.setLayoutManager(manager); - mRecyclerView.setHasFixedSize(true); - mRecyclerView.setAdapter(mAdapter); - final Builder dialogBuilder = new AlertDialog.Builder(activity) - .setView(view) - .setTitle(R.string.share_intent_activity_label); - - final Bundle arguments = getArguments(); - if (arguments == null || !arguments.getBoolean(HIDE_NEW_CONVERSATION_BUTTON_KEY)) { - dialogBuilder.setPositiveButton(R.string.share_new_message, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mDismissed = true; - mHost.onCreateConversationClick(); - } - }); - } - return dialogBuilder.setNegativeButton(R.string.share_cancel, null) - .create(); - } - - @Override - public void onDismiss(DialogInterface dialog) { - if (!mDismissed) { - final Activity activity = getActivity(); - if (activity != null) { - activity.finish(); - } - } - } - - /** - * {@inheritDoc} from Fragment - */ - @Override - public void onDestroy() { - super.onDestroy(); - mListBinding.unbind(); - } - - @Override - public void onAttach(final Activity activity) { - super.onAttach(activity); - if (activity instanceof HostInterface) { - mHost = (HostInterface) activity; - } - mListBinding.bind(DataModel.get().createConversationListData(activity, this, false)); - } - - @Override - public void onConversationListCursorUpdated(final ConversationListData data, - final Cursor cursor) { - mListBinding.ensureBound(data); - mAdapter.swapCursor(cursor); - updateEmptyListUi(cursor == null || cursor.getCount() == 0); - } - - /** - * {@inheritDoc} from SharIntentItemView.HostInterface - */ - @Override - public void onConversationClicked(final ConversationListItemData conversationListItemData) { - mHost.onConversationClick(conversationListItemData); - } - - // Show and hide empty list UI as needed with appropriate text based on view specifics - private void updateEmptyListUi(final boolean isEmpty) { - if (isEmpty) { - mEmptyListMessageView.setTextHint(R.string.conversation_list_empty_text); - mEmptyListMessageView.setVisibility(View.VISIBLE); - } else { - mEmptyListMessageView.setVisibility(View.GONE); - } - } - - @Override - public void setBlockedParticipantsAvailable(boolean blockedAvailable) { - } -} diff --git a/src/com/android/messaging/ui/conversationsettings/CopyContactDetailDialog.java b/src/com/android/messaging/ui/conversationsettings/CopyContactDetailDialog.java deleted file mode 100644 index d727001..0000000 --- a/src/com/android/messaging/ui/conversationsettings/CopyContactDetailDialog.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.conversationsettings; - -import android.app.AlertDialog; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.DialogInterface; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.util.AccessibilityUtil; - -public class CopyContactDetailDialog implements DialogInterface.OnClickListener { - - private final Context mContext; - private final String mContactDetail; - - public CopyContactDetailDialog(final Context context, final String contactDetail) { - mContext = context; - mContactDetail = contactDetail; - } - - public void show() { - new AlertDialog.Builder(mContext) - .setView(createBodyView()) - .setTitle(R.string.copy_to_clipboard_dialog_title) - .setPositiveButton(R.string.copy_to_clipboard, this) - .show(); - } - - @Override - public void onClick(final DialogInterface dialog, final int which) { - final ClipboardManager clipboard = - (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(ClipData.newPlainText(null /* label */, mContactDetail)); - } - - private View createBodyView() { - LayoutInflater inflater = (LayoutInflater) mContext - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - TextView textView = (TextView) inflater.inflate(R.layout.copy_contact_dialog_view, null, - false); - textView.setText(mContactDetail); - final String vocalizedDisplayName = AccessibilityUtil.getVocalizedPhoneNumber( - mContext.getResources(), mContactDetail); - textView.setContentDescription(vocalizedDisplayName); - return textView; - } -} diff --git a/src/com/android/messaging/ui/conversationsettings/PeopleAndOptionsActivity.java b/src/com/android/messaging/ui/conversationsettings/PeopleAndOptionsActivity.java deleted file mode 100644 index f017328..0000000 --- a/src/com/android/messaging/ui/conversationsettings/PeopleAndOptionsActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.conversationsettings; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.MenuItem; - -import com.android.messaging.R; -import com.android.messaging.ui.BugleActionBarActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.Assert; - -/** - * Shows a list of participants in a conversation. - */ -public class PeopleAndOptionsActivity extends BugleActionBarActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.people_and_options_activity); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - if (fragment instanceof PeopleAndOptionsFragment) { - final String conversationId = - getIntent().getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID); - Assert.notNull(conversationId); - final PeopleAndOptionsFragment peopleAndOptionsFragment = - (PeopleAndOptionsFragment) fragment; - peopleAndOptionsFragment.setConversationId(conversationId); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - // Treat the home press as back press so that when we go back to - // ConversationActivity, it doesn't lose its original intent (conversation id etc.) - onBackPressed(); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/com/android/messaging/ui/conversationsettings/PeopleAndOptionsFragment.java b/src/com/android/messaging/ui/conversationsettings/PeopleAndOptionsFragment.java deleted file mode 100644 index b86d952..0000000 --- a/src/com/android/messaging/ui/conversationsettings/PeopleAndOptionsFragment.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * 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.conversationsettings; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Fragment; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.res.Resources; -import android.database.Cursor; -import android.media.RingtoneManager; -import android.os.Bundle; -import android.os.Parcelable; -import android.provider.Settings; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.BaseAdapter; -import android.widget.ListView; -import android.widget.TextView; - -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.data.ParticipantData; -import com.android.messaging.datamodel.data.ParticipantListItemData; -import com.android.messaging.datamodel.data.PeopleAndOptionsData; -import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener; -import com.android.messaging.datamodel.data.PeopleOptionsItemData; -import com.android.messaging.datamodel.data.PersonItemData; -import com.android.messaging.ui.CompositeAdapter; -import com.android.messaging.ui.PersonItemView; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.conversation.ConversationActivity; -import com.android.messaging.util.Assert; - -import java.util.ArrayList; -import java.util.List; - -/** - * Shows a list of participants of a conversation and displays options. - */ -public class PeopleAndOptionsFragment extends Fragment - implements PeopleAndOptionsDataListener, PeopleOptionsItemView.HostInterface { - private ListView mListView; - private OptionsListAdapter mOptionsListAdapter; - private PeopleListAdapter mPeopleListAdapter; - private final Binding<PeopleAndOptionsData> mBinding = - BindingBase.createBinding(this); - - private static final int REQUEST_CODE_RINGTONE_PICKER = 1000; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mBinding.getData().init(getLoaderManager(), mBinding); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.people_and_options_fragment, container, false); - mListView = (ListView) view.findViewById(android.R.id.list); - mPeopleListAdapter = new PeopleListAdapter(getActivity()); - mOptionsListAdapter = new OptionsListAdapter(); - final CompositeAdapter compositeAdapter = new CompositeAdapter(getActivity()); - compositeAdapter.addPartition(new PeopleAndOptionsPartition(mOptionsListAdapter, - R.string.general_settings_title, false)); - compositeAdapter.addPartition(new PeopleAndOptionsPartition(mPeopleListAdapter, - R.string.participant_list_title, true)); - mListView.setAdapter(compositeAdapter); - return view; - } - - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_RINGTONE_PICKER) { - final Parcelable pick = data.getParcelableExtra( - RingtoneManager.EXTRA_RINGTONE_PICKED_URI); - final String pickedUri = pick == null ? "" : pick.toString(); - mBinding.getData().setConversationNotificationSound(mBinding, pickedUri); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - mBinding.unbind(); - } - - public void setConversationId(final String conversationId) { - Assert.isTrue(getView() == null); - Assert.notNull(conversationId); - mBinding.bind(DataModel.get().createPeopleAndOptionsData(conversationId, getActivity(), - this)); - } - - @Override - public void onOptionsCursorUpdated(final PeopleAndOptionsData data, final Cursor cursor) { - Assert.isTrue(cursor == null || cursor.getCount() == 1); - mBinding.ensureBound(data); - mOptionsListAdapter.swapCursor(cursor); - } - - @Override - public void onParticipantsListLoaded(final PeopleAndOptionsData data, - final List<ParticipantData> participants) { - mBinding.ensureBound(data); - mPeopleListAdapter.updateParticipants(participants); - final ParticipantData otherParticipant = participants.size() == 1 ? - participants.get(0) : null; - mOptionsListAdapter.setOtherParticipant(otherParticipant); - } - - @Override - public void onOptionsItemViewClicked(final PeopleOptionsItemData item, - final boolean isChecked) { - switch (item.getItemId()) { - case PeopleOptionsItemData.SETTING_NOTIFICATION_ENABLED: - mBinding.getData().enableConversationNotifications(mBinding, isChecked); - break; - - case PeopleOptionsItemData.SETTING_NOTIFICATION_SOUND_URI: - final Intent ringtonePickerIntent = UIIntents.get().getRingtonePickerIntent( - getString(R.string.notification_sound_pref_title), - item.getRingtoneUri(), Settings.System.DEFAULT_NOTIFICATION_URI, - RingtoneManager.TYPE_NOTIFICATION); - startActivityForResult(ringtonePickerIntent, REQUEST_CODE_RINGTONE_PICKER); - break; - - case PeopleOptionsItemData.SETTING_NOTIFICATION_VIBRATION: - mBinding.getData().enableConversationNotificationVibration(mBinding, - isChecked); - break; - - case PeopleOptionsItemData.SETTING_BLOCKED: - if (item.getOtherParticipant().isBlocked()) { - mBinding.getData().setDestinationBlocked(mBinding, false); - break; - } - final Resources res = getResources(); - final Activity activity = getActivity(); - new AlertDialog.Builder(activity) - .setTitle(res.getString(R.string.block_confirmation_title, - item.getOtherParticipant().getDisplayDestination())) - .setMessage(res.getString(R.string.block_confirmation_message)) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface arg0, int arg1) { - mBinding.getData().setDestinationBlocked(mBinding, true); - activity.setResult(ConversationActivity.FINISH_RESULT_CODE); - activity.finish(); - } - }) - .create() - .show(); - break; - } - } - - /** - * A simple adapter that takes a conversation metadata cursor and binds - * PeopleAndOptionsItemViews to individual COLUMNS of the first cursor record. (Note - * that this is not a CursorAdapter because it treats individual columns of the cursor as - * separate options to display for the conversation, e.g. notification settings). - */ - private class OptionsListAdapter extends BaseAdapter { - private Cursor mOptionsCursor; - private ParticipantData mOtherParticipantData; - - public Cursor swapCursor(final Cursor newCursor) { - final Cursor oldCursor = mOptionsCursor; - if (newCursor != oldCursor) { - mOptionsCursor = newCursor; - notifyDataSetChanged(); - } - return oldCursor; - } - - public void setOtherParticipant(final ParticipantData participantData) { - if (mOtherParticipantData != participantData) { - mOtherParticipantData = participantData; - notifyDataSetChanged(); - } - } - - @Override - public int getCount() { - int count = PeopleOptionsItemData.SETTINGS_COUNT; - if (mOtherParticipantData == null) { - count--; - } - return mOptionsCursor == null ? 0 : count; - } - - @Override - public Object getItem(final int position) { - return null; - } - - @Override - public long getItemId(final int position) { - return 0; - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) { - final PeopleOptionsItemView itemView; - if (convertView != null && convertView instanceof PeopleOptionsItemView) { - itemView = (PeopleOptionsItemView) convertView; - } else { - final LayoutInflater inflater = (LayoutInflater) getActivity() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - itemView = (PeopleOptionsItemView) - inflater.inflate(R.layout.people_options_item_view, parent, false); - } - mOptionsCursor.moveToFirst(); - itemView.bind(mOptionsCursor, position, mOtherParticipantData, - PeopleAndOptionsFragment.this); - return itemView; - } - } - - /** - * An adapter that takes a list of ParticipantData and displays them as a list of - * ParticipantListItemViews. - */ - private class PeopleListAdapter extends ArrayAdapter<ParticipantData> { - public PeopleListAdapter(final Context context) { - super(context, R.layout.people_list_item_view, new ArrayList<ParticipantData>()); - } - - public void updateParticipants(final List<ParticipantData> newList) { - clear(); - addAll(newList); - notifyDataSetChanged(); - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) { - PersonItemView itemView; - final ParticipantData item = getItem(position); - if (convertView != null && convertView instanceof PersonItemView) { - itemView = (PersonItemView) convertView; - } else { - final LayoutInflater inflater = (LayoutInflater) getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - itemView = (PersonItemView) inflater.inflate(R.layout.people_list_item_view, parent, - false); - } - final ParticipantListItemData itemData = - DataModel.get().createParticipantListItemData(item); - itemView.bind(itemData); - - // Any click on the row should have the same effect as clicking the avatar icon - final PersonItemView itemViewClosure = itemView; - itemView.setListener(new PersonItemView.PersonItemViewListener() { - @Override - public void onPersonClicked(final PersonItemData data) { - itemViewClosure.performClickOnAvatar(); - } - - @Override - public boolean onPersonLongClicked(final PersonItemData data) { - if (mBinding.isBound()) { - final CopyContactDetailDialog dialog = new CopyContactDetailDialog( - getContext(), data.getDetails()); - dialog.show(); - return true; - } - return false; - } - }); - return itemView; - } - } - - /** - * Represents a partition/section in the People & Options list (e.g. "general options" and - * "people in this conversation" sections). - */ - private class PeopleAndOptionsPartition extends CompositeAdapter.Partition { - private final int mHeaderResId; - private final boolean mNeedDivider; - - public PeopleAndOptionsPartition(final BaseAdapter adapter, final int headerResId, - final boolean needDivider) { - super(true /* showIfEmpty */, true /* hasHeader */, adapter); - mHeaderResId = headerResId; - mNeedDivider = needDivider; - } - - @Override - public View getHeaderView(final View convertView, final ViewGroup parentView) { - View view = null; - if (convertView != null && convertView.getId() == R.id.people_and_options_header) { - view = convertView; - } else { - view = LayoutInflater.from(getActivity()).inflate( - R.layout.people_and_options_section_header, parentView, false); - } - final TextView text = (TextView) view.findViewById(R.id.header_text); - final View divider = view.findViewById(R.id.divider); - text.setText(mHeaderResId); - divider.setVisibility(mNeedDivider ? View.VISIBLE : View.GONE); - return view; - } - } -} diff --git a/src/com/android/messaging/ui/conversationsettings/PeopleOptionsItemView.java b/src/com/android/messaging/ui/conversationsettings/PeopleOptionsItemView.java deleted file mode 100644 index 42ecfeb..0000000 --- a/src/com/android/messaging/ui/conversationsettings/PeopleOptionsItemView.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.conversationsettings; - -import android.content.Context; -import android.database.Cursor; -import android.support.v7.widget.SwitchCompat; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.datamodel.data.PeopleOptionsItemData; -import com.android.messaging.util.Assert; - -/** - * The view for a single entry in the options section of people & options activity. - */ -public class PeopleOptionsItemView extends LinearLayout { - /** - * Implemented by the host of this view that handles options click event. - */ - public interface HostInterface { - void onOptionsItemViewClicked(PeopleOptionsItemData item, boolean isChecked); - } - - private TextView mTitle; - private TextView mSubtitle; - private SwitchCompat mSwitch; - private final PeopleOptionsItemData mData; - private HostInterface mHostInterface; - - public PeopleOptionsItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mData = DataModel.get().createPeopleOptionsItemData(context); - } - - @Override - protected void onFinishInflate () { - mTitle = (TextView) findViewById(R.id.title); - mSubtitle = (TextView) findViewById(R.id.subtitle); - mSwitch = (SwitchCompat) findViewById(R.id.switch_button); - setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - mHostInterface.onOptionsItemViewClicked(mData, !mData.getChecked()); - } - }); - } - - public void bind(final Cursor cursor, final int columnIndex, ParticipantData otherParticipant, - final HostInterface hostInterface) { - Assert.isTrue(columnIndex < PeopleOptionsItemData.SETTINGS_COUNT && columnIndex >= 0); - mData.bind(cursor, otherParticipant, columnIndex); - mHostInterface = hostInterface; - - mTitle.setText(mData.getTitle()); - final String subtitle = mData.getSubtitle(); - if (TextUtils.isEmpty(subtitle)) { - mSubtitle.setVisibility(GONE); - } else { - mSubtitle.setVisibility(VISIBLE); - mSubtitle.setText(subtitle); - } - - if (mData.getCheckable()) { - mSwitch.setVisibility(VISIBLE); - mSwitch.setChecked(mData.getChecked()); - } else { - mSwitch.setVisibility(GONE); - } - - final boolean enabled = mData.getEnabled(); - if (enabled != isEnabled()) { - mTitle.setEnabled(enabled); - mSubtitle.setEnabled(enabled); - mSwitch.setEnabled(enabled); - setAlpha(enabled ? 1.0f : 0.5f); - setEnabled(enabled); - } - } -} diff --git a/src/com/android/messaging/ui/debug/DebugMmsConfigActivity.java b/src/com/android/messaging/ui/debug/DebugMmsConfigActivity.java deleted file mode 100644 index 485dcf7..0000000 --- a/src/com/android/messaging/ui/debug/DebugMmsConfigActivity.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.debug; - -import android.os.Bundle; - -import com.android.messaging.R; -import com.android.messaging.ui.BaseBugleActivity; - -/** - * Show list of all MmsConfig key/value pairs and allow editing. - */ -public class DebugMmsConfigActivity extends BaseBugleActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.debug_mmsconfig_activity); - } -} diff --git a/src/com/android/messaging/ui/debug/DebugMmsConfigFragment.java b/src/com/android/messaging/ui/debug/DebugMmsConfigFragment.java deleted file mode 100644 index 7c54db5..0000000 --- a/src/com/android/messaging/ui/debug/DebugMmsConfigFragment.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.debug; - -import android.app.Fragment; -import android.content.Context; -import android.os.Bundle; -import android.telephony.SubscriptionInfo; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; -import android.widget.BaseAdapter; -import android.widget.ListView; -import android.widget.Spinner; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.sms.MmsConfig; -import com.android.messaging.ui.debug.DebugMmsConfigItemView.MmsConfigItemListener; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Show list of all MmsConfig key/value pairs and allow editing. - */ -public class DebugMmsConfigFragment extends Fragment { - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View fragmentView = inflater.inflate(R.layout.mms_config_debug_fragment, container, - false); - final ListView listView = (ListView) fragmentView.findViewById(android.R.id.list); - final Spinner spinner = (Spinner) fragmentView.findViewById(R.id.sim_selector); - final Integer[] subIdArray = getActiveSubIds(); - ArrayAdapter<Integer> spinnerAdapter = new ArrayAdapter<Integer>(getActivity(), - android.R.layout.simple_spinner_item, subIdArray); - spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinner.setAdapter(spinnerAdapter); - spinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - listView.setAdapter(new MmsConfigAdapter(getActivity(), subIdArray[position])); - - final int[] mccmnc = PhoneUtils.get(subIdArray[position]).getMccMnc(); - // Set the title with the mcc/mnc - final TextView title = (TextView) fragmentView.findViewById(R.id.sim_title); - title.setText("(" + mccmnc[0] + "/" + mccmnc[1] + ") " + - getActivity().getString(R.string.debug_sub_id_spinner_text)); - } - - @Override - public void onNothingSelected(AdapterView<?> parent) { - } - }); - - return fragmentView; - } - - public static Integer[] getActiveSubIds() { - if (!OsUtil.isAtLeastL_MR1()) { - return new Integer[] { ParticipantData.DEFAULT_SELF_SUB_ID }; - } - final List<SubscriptionInfo> subRecords = - PhoneUtils.getDefault().toLMr1().getActiveSubscriptionInfoList(); - if (subRecords == null) { - return new Integer[0]; - } - final Integer[] retArray = new Integer[subRecords.size()]; - for (int i = 0; i < subRecords.size(); i++) { - retArray[i] = subRecords.get(i).getSubscriptionId(); - } - return retArray; - } - - private class MmsConfigAdapter extends BaseAdapter implements - DebugMmsConfigItemView.MmsConfigItemListener { - private final LayoutInflater mInflater; - private final List<String> mKeys; - private final MmsConfig mMmsConfig; - - public MmsConfigAdapter(Context context, int subId) { - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mMmsConfig = MmsConfig.get(subId); - mKeys = new ArrayList<>(mMmsConfig.keySet()); - Collections.sort(mKeys); - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) { - final DebugMmsConfigItemView view; - if (convertView != null && convertView instanceof DebugMmsConfigItemView) { - view = (DebugMmsConfigItemView) convertView; - } else { - view = (DebugMmsConfigItemView) mInflater.inflate( - R.layout.debug_mmsconfig_item_view, parent, false); - } - final String key = mKeys.get(position); - view.bind(key, - MmsConfig.getKeyType(key), - String.valueOf(mMmsConfig.getValue(key)), - this); - return view; - } - - @Override - public void onValueChanged(String key, String keyType, String value) { - mMmsConfig.update(key, value, keyType); - notifyDataSetChanged(); - } - - @Override - public int getCount() { - return mKeys.size(); - } - - @Override - public Object getItem(int position) { - return mKeys.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - } -} diff --git a/src/com/android/messaging/ui/debug/DebugMmsConfigItemView.java b/src/com/android/messaging/ui/debug/DebugMmsConfigItemView.java deleted file mode 100644 index 7b899c0..0000000 --- a/src/com/android/messaging/ui/debug/DebugMmsConfigItemView.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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.debug; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnShowListener; -import android.text.InputType; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.inputmethod.InputMethodManager; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.Switch; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.sms.MmsConfig; -import com.android.messaging.util.LogUtil; - -public class DebugMmsConfigItemView extends LinearLayout implements OnClickListener, - OnCheckedChangeListener, DialogInterface.OnClickListener { - - public interface MmsConfigItemListener { - void onValueChanged(String key, String keyType, String value); - } - - private TextView mTitle; - private TextView mTextValue; - private Switch mSwitch; - private String mKey; - private String mKeyType; - private MmsConfigItemListener mListener; - private EditText mEditText; - - public DebugMmsConfigItemView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - } - - @Override - protected void onFinishInflate () { - mTitle = (TextView) findViewById(R.id.title); - mTextValue = (TextView) findViewById(R.id.text_value); - mSwitch = (Switch) findViewById(R.id.switch_button); - setOnClickListener(this); - mSwitch.setOnCheckedChangeListener(this); - } - - public void bind(final String key, final String keyType, final String value, - final MmsConfigItemListener listener) { - mListener = listener; - mKey = key; - mKeyType = keyType; - mTitle.setText(key); - - switch (keyType) { - case MmsConfig.KEY_TYPE_BOOL: - mSwitch.setVisibility(View.VISIBLE); - mTextValue.setVisibility(View.GONE); - mSwitch.setChecked(Boolean.valueOf(value)); - break; - case MmsConfig.KEY_TYPE_STRING: - case MmsConfig.KEY_TYPE_INT: - mTextValue.setVisibility(View.VISIBLE); - mSwitch.setVisibility(View.GONE); - mTextValue.setText(value); - break; - default: - mTextValue.setVisibility(View.GONE); - mSwitch.setVisibility(View.GONE); - LogUtil.e(LogUtil.BUGLE_TAG, "Unexpected keytype: " + keyType); - break; - } - } - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mListener.onValueChanged(mKey, mKeyType, String.valueOf(isChecked)); - } - - @Override - public void onClick(View v) { - if (MmsConfig.KEY_TYPE_BOOL.equals(mKeyType)) { - return; - } - final Context context = getContext(); - mEditText = new EditText(context); - mEditText.setText(mTextValue.getText()); - mEditText.setFocusable(true); - if (MmsConfig.KEY_TYPE_INT.equals(mKeyType)) { - mEditText.setInputType(InputType.TYPE_CLASS_PHONE); - } else { - mEditText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - } - final AlertDialog dialog = new AlertDialog.Builder(context) - .setTitle(mKey) - .setView(mEditText) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null) - .create(); - dialog.setOnShowListener(new OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - mEditText.requestFocus(); - mEditText.selectAll(); - ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)) - .toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); - } - }); - dialog.show(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - mListener.onValueChanged(mKey, mKeyType, mEditText.getText().toString()); - } -} diff --git a/src/com/android/messaging/ui/debug/DebugSmsMmsFromDumpFileDialogFragment.java b/src/com/android/messaging/ui/debug/DebugSmsMmsFromDumpFileDialogFragment.java deleted file mode 100644 index 1aa8be3..0000000 --- a/src/com/android/messaging/ui/debug/DebugSmsMmsFromDumpFileDialogFragment.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.debug; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.telephony.SmsMessage; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.action.ReceiveMmsMessageAction; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.receiver.SmsReceiver; -import com.android.messaging.sms.MmsUtils; -import com.android.messaging.util.DebugUtils; -import com.android.messaging.util.LogUtil; - -/** - * Class that displays UI for choosing SMS/MMS dump files for debugging - */ -public class DebugSmsMmsFromDumpFileDialogFragment extends DialogFragment { - public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; - public static final String KEY_DUMP_FILES = "dump_files"; - public static final String KEY_ACTION = "action"; - - public static final String ACTION_LOAD = "load"; - public static final String ACTION_EMAIL = "email"; - - private String[] mDumpFiles; - private String mAction; - - public static DebugSmsMmsFromDumpFileDialogFragment newInstance(final String[] dumpFiles, - final String action) { - final DebugSmsMmsFromDumpFileDialogFragment frag = - new DebugSmsMmsFromDumpFileDialogFragment(); - final Bundle args = new Bundle(); - args.putSerializable(KEY_DUMP_FILES, dumpFiles); - args.putString(KEY_ACTION, action); - frag.setArguments(args); - return frag; - } - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final Bundle args = getArguments(); - mDumpFiles = (String[]) args.getSerializable(KEY_DUMP_FILES); - mAction = args.getString(KEY_ACTION); - - final LayoutInflater inflater = getActivity().getLayoutInflater(); - final View layout = inflater.inflate( - R.layout.debug_sms_mms_from_dump_file_dialog, null/*root*/); - final ListView list = (ListView) layout.findViewById(R.id.dump_file_list); - list.setAdapter(new DumpFileListAdapter(getActivity(), mDumpFiles)); - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - final Resources resources = getResources(); - if (ACTION_LOAD.equals(mAction)) { - builder.setTitle(resources.getString( - R.string.load_sms_mms_from_dump_file_dialog_title)); - } else if (ACTION_EMAIL.equals(mAction)) { - builder.setTitle(resources.getString( - R.string.email_sms_mms_from_dump_file_dialog_title)); - } - builder.setView(layout); - return builder.create(); - } - - private class DumpFileListAdapter extends ArrayAdapter<String> { - public DumpFileListAdapter(final Context context, final String[] dumpFiles) { - super(context, R.layout.sms_mms_dump_file_list_item, dumpFiles); - } - - @Override - public View getView(final int position, final View view, final ViewGroup parent) { - TextView actionItemView; - if (view == null || !(view instanceof TextView)) { - final LayoutInflater inflater = LayoutInflater.from(getContext()); - actionItemView = (TextView) inflater.inflate( - R.layout.sms_mms_dump_file_list_item, parent, false); - } else { - actionItemView = (TextView) view; - } - - final String file = getItem(position); - actionItemView.setText(file); - actionItemView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View view) { - dismiss(); - if (ACTION_LOAD.equals(mAction)) { - receiveFromDumpFile(file); - } else if (ACTION_EMAIL.equals(mAction)) { - emailDumpFile(file); - } - } - }); - return actionItemView; - } - } - - /** - * Load MMS/SMS from the dump file - */ - private void receiveFromDumpFile(final String dumpFileName) { - if (dumpFileName.startsWith(MmsUtils.SMS_DUMP_PREFIX)) { - final SmsMessage[] messages = DebugUtils.retreiveSmsFromDumpFile(dumpFileName); - if (messages != null) { - SmsReceiver.deliverSmsMessages(getActivity(), ParticipantData.DEFAULT_SELF_SUB_ID, - 0, messages); - } else { - LogUtil.e(LogUtil.BUGLE_TAG, - "receiveFromDumpFile: invalid sms dump file " + dumpFileName); - } - } else if (dumpFileName.startsWith(MmsUtils.MMS_DUMP_PREFIX)) { - final byte[] data = MmsUtils.createDebugNotificationInd(dumpFileName); - if (data != null) { - final ReceiveMmsMessageAction action = new ReceiveMmsMessageAction( - ParticipantData.DEFAULT_SELF_SUB_ID, data); - action.start(); - } else { - LogUtil.e(LogUtil.BUGLE_TAG, - "receiveFromDumpFile: invalid mms dump file " + dumpFileName); - } - } else { - LogUtil.e(LogUtil.BUGLE_TAG, - "receiveFromDumpFile: invalid dump file name " + dumpFileName); - } - } - - /** - * Launch email app to send the dump file - */ - private void emailDumpFile(final String file) { - final Resources resources = getResources(); - final String fileLocation = "file://" - + Environment.getExternalStorageDirectory() + "/" + file; - final Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType(APPLICATION_OCTET_STREAM); - sharingIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(fileLocation)); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, - resources.getString(R.string.email_sms_mms_dump_file_subject)); - getActivity().startActivity(Intent.createChooser(sharingIntent, - resources.getString(R.string.email_sms_mms_dump_file_chooser_title))); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/AudioLevelSource.java b/src/com/android/messaging/ui/mediapicker/AudioLevelSource.java deleted file mode 100644 index a211058..0000000 --- a/src/com/android/messaging/ui/mediapicker/AudioLevelSource.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 com.google.common.base.Preconditions; - -import javax.annotation.concurrent.ThreadSafe; - -/** - * Keeps track of the speech level as last observed by the recognition - * engine as microphone data flows through it. Can be polled by the UI to - * animate its views. - */ -@ThreadSafe -public class AudioLevelSource { - private volatile int mSpeechLevel; - private volatile Listener mListener; - - public static final int LEVEL_UNKNOWN = -1; - - public interface Listener { - void onSpeechLevel(int speechLevel); - } - - public void setSpeechLevel(int speechLevel) { - Preconditions.checkArgument(speechLevel >= 0 && speechLevel <= 100 || - speechLevel == LEVEL_UNKNOWN); - mSpeechLevel = speechLevel; - maybeNotify(); - } - - public int getSpeechLevel() { - return mSpeechLevel; - } - - public void reset() { - setSpeechLevel(LEVEL_UNKNOWN); - } - - public boolean isValid() { - return mSpeechLevel > 0; - } - - private void maybeNotify() { - final Listener l = mListener; - if (l != null) { - l.onSpeechLevel(mSpeechLevel); - } - } - - public synchronized void setListener(Listener listener) { - mListener = listener; - } - - public synchronized void clearListener(Listener listener) { - if (mListener == listener) { - mListener = null; - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/AudioMediaChooser.java b/src/com/android/messaging/ui/mediapicker/AudioMediaChooser.java deleted file mode 100644 index 5d79293..0000000 --- a/src/com/android/messaging/ui/mediapicker/AudioMediaChooser.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.Manifest; -import android.content.pm.PackageManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.util.OsUtil; - -/** - * Chooser which allows the user to record audio - */ -class AudioMediaChooser extends MediaChooser implements - AudioRecordView.HostInterface { - private View mEnabledView; - private View mMissingPermissionView; - - AudioMediaChooser(final MediaPicker mediaPicker) { - super(mediaPicker); - } - - @Override - public int getSupportedMediaTypes() { - return MediaPicker.MEDIA_TYPE_AUDIO; - } - - @Override - public int getIconResource() { - return R.drawable.ic_audio_light; - } - - @Override - public int getIconDescriptionResource() { - return R.string.mediapicker_audioChooserDescription; - } - - @Override - public void onAudioRecorded(final MessagePartData item) { - mMediaPicker.dispatchItemsSelected(item, true); - } - - @Override - public void setThemeColor(final int color) { - if (mView != null) { - ((AudioRecordView) mView).setThemeColor(color); - } - } - - @Override - protected View createView(final ViewGroup container) { - final LayoutInflater inflater = getLayoutInflater(); - final AudioRecordView view = (AudioRecordView) inflater.inflate( - R.layout.mediapicker_audio_chooser, - container /* root */, - false /* attachToRoot */); - view.setHostInterface(this); - view.setThemeColor(mMediaPicker.getConversationThemeColor()); - mEnabledView = view.findViewById(R.id.mediapicker_enabled); - mMissingPermissionView = view.findViewById(R.id.missing_permission_view); - return view; - } - - @Override - int getActionBarTitleResId() { - return R.string.mediapicker_audio_title; - } - - @Override - public boolean isHandlingTouch() { - // Whenever the user is in the process of recording audio, we want to allow the user - // to move the finger within the panel without interpreting that as dragging the media - // picker panel. - return ((AudioRecordView) mView).shouldHandleTouch(); - } - - @Override - public void stopTouchHandling() { - ((AudioRecordView) mView).stopTouchHandling(); - } - - @Override - public void onPause() { - super.onPause(); - if (mView != null) { - ((AudioRecordView) mView).onPause(); - } - } - - @Override - protected void setSelected(final boolean selected) { - super.setSelected(selected); - if (selected && !OsUtil.hasRecordAudioPermission()) { - requestRecordAudioPermission(); - } - } - - private void requestRecordAudioPermission() { - mMediaPicker.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, - MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE); - } - - @Override - protected void onRequestPermissionsResult( - final int requestCode, final String permissions[], final int[] grantResults) { - if (requestCode == MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE) { - final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED; - mEnabledView.setVisibility(permissionGranted ? View.VISIBLE : View.GONE); - mMissingPermissionView.setVisibility(permissionGranted ? View.GONE : View.VISIBLE); - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/AudioRecordView.java b/src/com/android/messaging/ui/mediapicker/AudioRecordView.java deleted file mode 100644 index fba493f..0000000 --- a/src/com/android/messaging/ui/mediapicker/AudioRecordView.java +++ /dev/null @@ -1,351 +0,0 @@ -/* - * 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.content.Context; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.media.MediaRecorder; -import android.net.Uri; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; -import com.android.messaging.datamodel.data.MediaPickerMessagePartData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.sms.MmsConfig; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.MediaUtil; -import com.android.messaging.util.MediaUtil.OnCompletionListener; -import com.android.messaging.util.SafeAsyncTask; -import com.android.messaging.util.ThreadUtil; -import com.android.messaging.util.UiUtils; -import com.google.common.annotations.VisibleForTesting; - -/** - * Hosts an audio recorder with tap and hold to record functionality. - */ -public class AudioRecordView extends FrameLayout implements - MediaRecorder.OnErrorListener, - MediaRecorder.OnInfoListener { - /** - * An interface that communicates with the hosted AudioRecordView. - */ - public interface HostInterface extends DraftMessageSubscriptionDataProvider { - void onAudioRecorded(final MessagePartData item); - } - - /** The initial state, the user may press and hold to start recording */ - private static final int MODE_IDLE = 1; - - /** The user has pressed the record button and we are playing the sound indicating the - * start of recording session. Don't record yet since we don't want the beeping sound - * to get into the recording. */ - private static final int MODE_STARTING = 2; - - /** When the user is actively recording */ - private static final int MODE_RECORDING = 3; - - /** When the user has finished recording, we need to record for some additional time. */ - private static final int MODE_STOPPING = 4; - - // Bug: 16020175: The framework's MediaRecorder would cut off the ending portion of the - // recorded audio by about half a second. To mitigate this issue, we continue the recording - // for some extra time before stopping it. - private static final int AUDIO_RECORD_ENDING_BUFFER_MILLIS = 500; - - /** - * The minimum duration of any recording. Below this threshold, it will be treated as if the - * user clicked the record button and inform the user to tap and hold to record. - */ - private static final int AUDIO_RECORD_MINIMUM_DURATION_MILLIS = 300; - - // For accessibility, the touchable record button is bigger than the record button visual. - private ImageView mRecordButtonVisual; - private View mRecordButton; - private SoundLevels mSoundLevels; - private TextView mHintTextView; - private PausableChronometer mTimerTextView; - private LevelTrackingMediaRecorder mMediaRecorder; - private long mAudioRecordStartTimeMillis; - - private int mCurrentMode = MODE_IDLE; - private HostInterface mHostInterface; - private int mThemeColor; - - public AudioRecordView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mMediaRecorder = new LevelTrackingMediaRecorder(); - } - - public void setHostInterface(final HostInterface hostInterface) { - mHostInterface = hostInterface; - } - - @VisibleForTesting - public void testSetMediaRecorder(final LevelTrackingMediaRecorder recorder) { - mMediaRecorder = recorder; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mSoundLevels = (SoundLevels) findViewById(R.id.sound_levels); - mRecordButtonVisual = (ImageView) findViewById(R.id.record_button_visual); - mRecordButton = findViewById(R.id.record_button); - mHintTextView = (TextView) findViewById(R.id.hint_text); - mTimerTextView = (PausableChronometer) findViewById(R.id.timer_text); - mSoundLevels.setLevelSource(mMediaRecorder.getLevelSource()); - mRecordButton.setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(final View v, final MotionEvent event) { - final int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - onRecordButtonTouchDown(); - - // Don't let the record button handle the down event to let it fall through - // so that we can handle it for the entire panel in onTouchEvent(). This is - // done so that: 1) the user taps on the record button to start recording - // 2) the entire panel owns the touch event so we'd keep recording even - // if the user moves outside the button region. - return false; - } - return false; - } - }); - } - - @Override - public boolean onTouchEvent(final MotionEvent event) { - final int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - return shouldHandleTouch(); - - case MotionEvent.ACTION_MOVE: - return true; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - return onRecordButtonTouchUp(); - } - return super.onTouchEvent(event); - } - - public void onPause() { - // The conversation draft cannot take any updates when it's paused. Therefore, forcibly - // stop recording on pause. - stopRecording(); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - stopRecording(); - } - - private boolean isRecording() { - return mMediaRecorder.isRecording() && mCurrentMode == MODE_RECORDING; - } - - public boolean shouldHandleTouch() { - return mCurrentMode != MODE_IDLE; - } - - public void stopTouchHandling() { - setMode(MODE_IDLE); - stopRecording(); - } - - private void setMode(final int mode) { - if (mCurrentMode != mode) { - mCurrentMode = mode; - updateVisualState(); - } - } - - private void updateVisualState() { - switch (mCurrentMode) { - case MODE_IDLE: - mHintTextView.setVisibility(VISIBLE); - mHintTextView.setTypeface(null, Typeface.NORMAL); - mTimerTextView.setVisibility(GONE); - mSoundLevels.setEnabled(false); - mTimerTextView.stop(); - break; - - case MODE_RECORDING: - case MODE_STOPPING: - mHintTextView.setVisibility(GONE); - mTimerTextView.setVisibility(VISIBLE); - mSoundLevels.setEnabled(true); - mTimerTextView.restart(); - break; - - case MODE_STARTING: - break; // No-Op. - - default: - Assert.fail("invalid mode for AudioRecordView!"); - break; - } - updateRecordButtonAppearance(); - } - - public void setThemeColor(final int color) { - mThemeColor = color; - updateRecordButtonAppearance(); - } - - private void updateRecordButtonAppearance() { - final Drawable foregroundDrawable = getResources().getDrawable(R.drawable.ic_mp_audio_mic); - final GradientDrawable backgroundDrawable = ((GradientDrawable) getResources() - .getDrawable(R.drawable.audio_record_control_button_background)); - if (isRecording()) { - foregroundDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP); - backgroundDrawable.setColor(mThemeColor); - } else { - foregroundDrawable.setColorFilter(mThemeColor, PorterDuff.Mode.SRC_ATOP); - backgroundDrawable.setColor(Color.WHITE); - } - mRecordButtonVisual.setImageDrawable(foregroundDrawable); - mRecordButtonVisual.setBackground(backgroundDrawable); - } - - @VisibleForTesting - boolean onRecordButtonTouchDown() { - if (!mMediaRecorder.isRecording() && mCurrentMode == MODE_IDLE) { - setMode(MODE_STARTING); - playAudioStartSound(new OnCompletionListener() { - @Override - public void onCompletion() { - // Double-check the current mode before recording since the user may have - // lifted finger from the button before the beeping sound is played through. - final int maxSize = MmsConfig.get(mHostInterface.getConversationSelfSubId()) - .getMaxMessageSize(); - if (mCurrentMode == MODE_STARTING && - mMediaRecorder.startRecording(AudioRecordView.this, - AudioRecordView.this, maxSize)) { - setMode(MODE_RECORDING); - } - } - }); - mAudioRecordStartTimeMillis = System.currentTimeMillis(); - return true; - } - return false; - } - - @VisibleForTesting - boolean onRecordButtonTouchUp() { - if (System.currentTimeMillis() - mAudioRecordStartTimeMillis < - AUDIO_RECORD_MINIMUM_DURATION_MILLIS) { - // The recording is too short, bolden the hint text to instruct the user to - // "tap+hold" to record audio. - final Uri outputUri = stopRecording(); - if (outputUri != null) { - SafeAsyncTask.executeOnThreadPool(new Runnable() { - @Override - public void run() { - Factory.get().getApplicationContext().getContentResolver().delete( - outputUri, null, null); - } - }); - } - setMode(MODE_IDLE); - mHintTextView.setTypeface(null, Typeface.BOLD); - } else if (isRecording()) { - // Record for some extra time to ensure the ending part is saved. - setMode(MODE_STOPPING); - ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() { - @Override - public void run() { - onFinishedRecording(); - } - }, AUDIO_RECORD_ENDING_BUFFER_MILLIS); - } else { - setMode(MODE_IDLE); - } - return true; - } - - private Uri stopRecording() { - if (mMediaRecorder.isRecording()) { - return mMediaRecorder.stopRecording(); - } - return null; - } - - @Override // From MediaRecorder.OnInfoListener - public void onInfo(final MediaRecorder mr, final int what, final int extra) { - if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { - // Max size reached. Finish recording immediately. - LogUtil.i(LogUtil.BUGLE_TAG, "Max size reached while recording audio"); - onFinishedRecording(); - } else { - // These are unknown errors. - onErrorWhileRecording(what, extra); - } - } - - @Override // From MediaRecorder.OnErrorListener - public void onError(final MediaRecorder mr, final int what, final int extra) { - onErrorWhileRecording(what, extra); - } - - private void onErrorWhileRecording(final int what, final int extra) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error occurred during audio recording what=" + what + - ", extra=" + extra); - UiUtils.showToastAtBottom(R.string.audio_recording_error); - setMode(MODE_IDLE); - stopRecording(); - } - - private void onFinishedRecording() { - final Uri outputUri = stopRecording(); - if (outputUri != null) { - final Rect startRect = new Rect(); - mRecordButtonVisual.getGlobalVisibleRect(startRect); - final MediaPickerMessagePartData audioItem = - new MediaPickerMessagePartData(startRect, - ContentType.AUDIO_3GPP, outputUri, 0, 0); - mHostInterface.onAudioRecorded(audioItem); - } - playAudioEndSound(); - setMode(MODE_IDLE); - } - - private void playAudioStartSound(final OnCompletionListener completionListener) { - MediaUtil.get().playSound(getContext(), R.raw.audio_initiate, completionListener); - } - - private void playAudioEndSound() { - MediaUtil.get().playSound(getContext(), R.raw.audio_end, null); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/CameraManager.java b/src/com/android/messaging/ui/mediapicker/CameraManager.java deleted file mode 100644 index 166ebd7..0000000 --- a/src/com/android/messaging/ui/mediapicker/CameraManager.java +++ /dev/null @@ -1,1200 +0,0 @@ -/* - * 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.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.hardware.Camera; -import android.hardware.Camera.CameraInfo; -import android.media.MediaRecorder; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Looper; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.view.MotionEvent; -import android.view.OrientationEventListener; -import android.view.Surface; -import android.view.View; -import android.view.WindowManager; - -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; -import com.android.messaging.Factory; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.datamodel.media.ImageRequest; -import com.android.messaging.sms.MmsConfig; -import com.android.messaging.ui.mediapicker.camerafocus.FocusOverlayManager; -import com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay; -import com.android.messaging.util.Assert; -import com.android.messaging.util.BugleGservices; -import com.android.messaging.util.BugleGservicesKeys; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * Class which manages interactions with the camera, but does not do any UI. This class is - * designed to be a singleton to ensure there is one component managing the camera and releasing - * the native resources. - * In order to acquire a camera, a caller must: - * <ul> - * <li>Call selectCamera to select front or back camera - * <li>Call setSurface to control where the preview is shown - * <li>Call openCamera to request the camera start preview - * </ul> - * Callers should call onPause and onResume to ensure that the camera is release while the activity - * is not active. - * This class is not thread safe. It should only be called from one thread (the UI thread or test - * thread) - */ -class CameraManager implements FocusOverlayManager.Listener { - /** - * Wrapper around the framework camera API to allow mocking different hardware scenarios while - * unit testing - */ - interface CameraWrapper { - int getNumberOfCameras(); - void getCameraInfo(int index, CameraInfo cameraInfo); - Camera open(int cameraId); - /** Add a wrapper for release because a final method cannot be mocked */ - void release(Camera camera); - } - - /** - * Callbacks for the camera manager listener - */ - interface CameraManagerListener { - void onCameraError(int errorCode, Exception e); - void onCameraChanged(); - } - - /** - * Callback when taking image or video - */ - interface MediaCallback { - static final int MEDIA_CAMERA_CHANGED = 1; - static final int MEDIA_NO_DATA = 2; - - void onMediaReady(Uri uriToMedia, String contentType, int width, int height); - void onMediaFailed(Exception exception); - void onMediaInfo(int what); - } - - // Error codes - static final int ERROR_OPENING_CAMERA = 1; - static final int ERROR_SHOWING_PREVIEW = 2; - static final int ERROR_INITIALIZING_VIDEO = 3; - static final int ERROR_STORAGE_FAILURE = 4; - static final int ERROR_RECORDING_VIDEO = 5; - static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 6; - static final int ERROR_TAKING_PICTURE = 7; - - private static final String TAG = LogUtil.BUGLE_TAG; - private static final int NO_CAMERA_SELECTED = -1; - - private static CameraManager sInstance; - - /** Default camera wrapper which directs calls to the framework APIs */ - private static CameraWrapper sCameraWrapper = new CameraWrapper() { - @Override - public int getNumberOfCameras() { - return Camera.getNumberOfCameras(); - } - - @Override - public void getCameraInfo(final int index, final CameraInfo cameraInfo) { - Camera.getCameraInfo(index, cameraInfo); - } - - @Override - public Camera open(final int cameraId) { - return Camera.open(cameraId); - } - - @Override - public void release(final Camera camera) { - camera.release(); - } - }; - - /** The CameraInfo for the currently selected camera */ - private final CameraInfo mCameraInfo; - - /** - * The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet - */ - private int mCameraIndex; - - /** True if the device has front and back cameras */ - private final boolean mHasFrontAndBackCamera; - - /** True if the camera should be open (may not yet be actually open) */ - private boolean mOpenRequested; - - /** True if the camera is requested to be in video mode */ - private boolean mVideoModeRequested; - - /** The media recorder for video mode */ - private MmsVideoRecorder mMediaRecorder; - - /** Callback to call with video recording updates */ - private MediaCallback mVideoCallback; - - /** The preview view to show the preview on */ - private CameraPreview mCameraPreview; - - /** The helper classs to handle orientation changes */ - private OrientationHandler mOrientationHandler; - - /** Tracks whether the preview has hardware acceleration */ - private boolean mIsHardwareAccelerationSupported; - - /** - * The task for opening the camera, so it doesn't block the UI thread - * Using AsyncTask rather than SafeAsyncTask because the tasks need to be serialized, but don't - * need to be on the UI thread - * TODO: If we have other AyncTasks (not SafeAsyncTasks) this may contend and we may - * need to create a dedicated thread, or synchronize the threads in the thread pool - */ - private AsyncTask<Integer, Void, Camera> mOpenCameraTask; - - /** - * The camera index that is queued to be opened, but not completed yet, or NO_CAMERA_SELECTED if - * no open task is pending - */ - private int mPendingOpenCameraIndex = NO_CAMERA_SELECTED; - - /** The instance of the currently opened camera */ - private Camera mCamera; - - /** The rotation of the screen relative to the camera's natural orientation */ - private int mRotation; - - /** The callback to notify when errors or other events occur */ - private CameraManagerListener mListener; - - /** True if the camera is currently in the process of taking an image */ - private boolean mTakingPicture; - - /** Provides subscription-related data to access per-subscription configurations. */ - private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider; - - /** Manages auto focus visual and behavior */ - private final FocusOverlayManager mFocusOverlayManager; - - private CameraManager() { - mCameraInfo = new CameraInfo(); - mCameraIndex = NO_CAMERA_SELECTED; - - // Check to see if a front and back camera exist - boolean hasFrontCamera = false; - boolean hasBackCamera = false; - final CameraInfo cameraInfo = new CameraInfo(); - final int cameraCount = sCameraWrapper.getNumberOfCameras(); - try { - for (int i = 0; i < cameraCount; i++) { - sCameraWrapper.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) { - hasFrontCamera = true; - } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) { - hasBackCamera = true; - } - if (hasFrontCamera && hasBackCamera) { - break; - } - } - } catch (final RuntimeException e) { - LogUtil.e(TAG, "Unable to load camera info", e); - } - mHasFrontAndBackCamera = hasFrontCamera && hasBackCamera; - mFocusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper()); - - // Assume the best until we are proven otherwise - mIsHardwareAccelerationSupported = true; - } - - /** Gets the singleton instance */ - static CameraManager get() { - if (sInstance == null) { - sInstance = new CameraManager(); - } - return sInstance; - } - - /** Allows tests to inject a custom camera wrapper */ - @VisibleForTesting - static void setCameraWrapper(final CameraWrapper cameraWrapper) { - sCameraWrapper = cameraWrapper; - sInstance = null; - } - - /** - * Sets the surface to use to display the preview - * This must only be called AFTER the CameraPreview has a texture ready - * @param preview The preview surface view - */ - void setSurface(final CameraPreview preview) { - if (preview == mCameraPreview) { - return; - } - - if (preview != null) { - Assert.isTrue(preview.isValid()); - preview.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(final View view, final MotionEvent motionEvent) { - if ((motionEvent.getActionMasked() & MotionEvent.ACTION_UP) == - MotionEvent.ACTION_UP) { - mFocusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight()); - mFocusOverlayManager.onSingleTapUp( - (int) motionEvent.getX() + view.getLeft(), - (int) motionEvent.getY() + view.getTop()); - } - return true; - } - }); - } - mCameraPreview = preview; - tryShowPreview(); - } - - void setRenderOverlay(final RenderOverlay renderOverlay) { - mFocusOverlayManager.setFocusRenderer(renderOverlay != null ? - renderOverlay.getPieRenderer() : null); - } - - /** Convenience function to swap between front and back facing cameras */ - void swapCamera() { - Assert.isTrue(mCameraIndex >= 0); - selectCamera(mCameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT ? - CameraInfo.CAMERA_FACING_BACK : - CameraInfo.CAMERA_FACING_FRONT); - } - - /** - * Selects the first camera facing the desired direction, or the first camera if there is no - * camera in the desired direction - * @param desiredFacing One of the CameraInfo.CAMERA_FACING_* constants - * @return True if a camera was selected, or false if selecting a camera failed - */ - boolean selectCamera(final int desiredFacing) { - try { - // We already selected a camera facing that direction - if (mCameraIndex >= 0 && mCameraInfo.facing == desiredFacing) { - return true; - } - - final int cameraCount = sCameraWrapper.getNumberOfCameras(); - Assert.isTrue(cameraCount > 0); - - mCameraIndex = NO_CAMERA_SELECTED; - setCamera(null); - final CameraInfo cameraInfo = new CameraInfo(); - for (int i = 0; i < cameraCount; i++) { - sCameraWrapper.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == desiredFacing) { - mCameraIndex = i; - sCameraWrapper.getCameraInfo(i, mCameraInfo); - break; - } - } - - // There's no camera in the desired facing direction, just select the first camera - // regardless of direction - if (mCameraIndex < 0) { - mCameraIndex = 0; - sCameraWrapper.getCameraInfo(0, mCameraInfo); - } - - if (mOpenRequested) { - // The camera is open, so reopen with the newly selected camera - openCamera(); - } - return true; - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.selectCamera", e); - if (mListener != null) { - mListener.onCameraError(ERROR_OPENING_CAMERA, e); - } - return false; - } - } - - int getCameraIndex() { - return mCameraIndex; - } - - void selectCameraByIndex(final int cameraIndex) { - if (mCameraIndex == cameraIndex) { - return; - } - - try { - mCameraIndex = cameraIndex; - sCameraWrapper.getCameraInfo(mCameraIndex, mCameraInfo); - if (mOpenRequested) { - openCamera(); - } - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.selectCameraByIndex", e); - if (mListener != null) { - mListener.onCameraError(ERROR_OPENING_CAMERA, e); - } - } - } - - @VisibleForTesting - CameraInfo getCameraInfo() { - if (mCameraIndex == NO_CAMERA_SELECTED) { - return null; - } - return mCameraInfo; - } - - /** @return True if this device has camera capabilities */ - boolean hasAnyCamera() { - return sCameraWrapper.getNumberOfCameras() > 0; - } - - /** @return True if the device has both a front and back camera */ - boolean hasFrontAndBackCamera() { - return mHasFrontAndBackCamera; - } - - /** - * Opens the camera on a separate thread and initiates the preview if one is available - */ - void openCamera() { - if (mCameraIndex == NO_CAMERA_SELECTED) { - // Ensure a selected camera if none is currently selected. This may happen if the - // camera chooser is not the default media chooser. - selectCamera(CameraInfo.CAMERA_FACING_BACK); - } - mOpenRequested = true; - // We're already opening the camera or already have the camera handle, nothing more to do - if (mPendingOpenCameraIndex == mCameraIndex || mCamera != null) { - return; - } - - // True if the task to open the camera has to be delayed until the current one completes - boolean delayTask = false; - - // Cancel any previous open camera tasks - if (mOpenCameraTask != null) { - mPendingOpenCameraIndex = NO_CAMERA_SELECTED; - delayTask = true; - } - - mPendingOpenCameraIndex = mCameraIndex; - mOpenCameraTask = new AsyncTask<Integer, Void, Camera>() { - private Exception mException; - - @Override - protected Camera doInBackground(final Integer... params) { - try { - final int cameraIndex = params[0]; - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "Opening camera " + mCameraIndex); - } - return sCameraWrapper.open(cameraIndex); - } catch (final Exception e) { - LogUtil.e(TAG, "Exception while opening camera", e); - mException = e; - return null; - } - } - - @Override - protected void onPostExecute(final Camera camera) { - // If we completed, but no longer want this camera, then release the camera - if (mOpenCameraTask != this || !mOpenRequested) { - releaseCamera(camera); - cleanup(); - return; - } - - cleanup(); - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "Opened camera " + mCameraIndex + " " + (camera != null)); - } - - setCamera(camera); - if (camera == null) { - if (mListener != null) { - mListener.onCameraError(ERROR_OPENING_CAMERA, mException); - } - LogUtil.e(TAG, "Error opening camera"); - } - } - - @Override - protected void onCancelled() { - super.onCancelled(); - cleanup(); - } - - private void cleanup() { - mPendingOpenCameraIndex = NO_CAMERA_SELECTED; - if (mOpenCameraTask != null && mOpenCameraTask.getStatus() == Status.PENDING) { - // If there's another task waiting on this one to complete, start it now - mOpenCameraTask.execute(mCameraIndex); - } else { - mOpenCameraTask = null; - } - - } - }; - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "Start opening camera " + mCameraIndex); - } - - if (!delayTask) { - mOpenCameraTask.execute(mCameraIndex); - } - } - - boolean isVideoMode() { - return mVideoModeRequested; - } - - boolean isRecording() { - return mVideoModeRequested && mVideoCallback != null; - } - - void setVideoMode(final boolean videoMode) { - if (mVideoModeRequested == videoMode) { - return; - } - mVideoModeRequested = videoMode; - tryInitOrCleanupVideoMode(); - } - - /** Closes the camera releasing the resources it uses */ - void closeCamera() { - mOpenRequested = false; - setCamera(null); - } - - /** Temporarily closes the camera if it is open */ - void onPause() { - setCamera(null); - } - - /** Reopens the camera if it was opened when onPause was called */ - void onResume() { - if (mOpenRequested) { - openCamera(); - } - } - - /** - * Sets the listener which will be notified of errors or other events in the camera - * @param listener The listener to notify - */ - void setListener(final CameraManagerListener listener) { - Assert.isMainThread(); - mListener = listener; - if (!mIsHardwareAccelerationSupported && mListener != null) { - mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); - } - } - - void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) { - mSubscriptionDataProvider = provider; - } - - void takePicture(final float heightPercent, @NonNull final MediaCallback callback) { - Assert.isTrue(!mVideoModeRequested); - Assert.isTrue(!mTakingPicture); - Assert.notNull(callback); - if (mCamera == null) { - // The caller should have checked isCameraAvailable first, but just in case, protect - // against a null camera by notifying the callback that taking the picture didn't work - callback.onMediaFailed(null); - return; - } - final Camera.PictureCallback jpegCallback = new Camera.PictureCallback() { - @Override - public void onPictureTaken(final byte[] bytes, final Camera camera) { - mTakingPicture = false; - if (mCamera != camera) { - // This may happen if the camera was changed between front/back while the - // picture is being taken. - callback.onMediaInfo(MediaCallback.MEDIA_CAMERA_CHANGED); - return; - } - - if (bytes == null) { - callback.onMediaInfo(MediaCallback.MEDIA_NO_DATA); - return; - } - - final Camera.Size size = camera.getParameters().getPictureSize(); - int width; - int height; - if (mRotation == 90 || mRotation == 270) { - width = size.height; - height = size.width; - } else { - width = size.width; - height = size.height; - } - new ImagePersistTask( - width, height, heightPercent, bytes, mCameraPreview.getContext(), callback) - .executeOnThreadPool(); - } - }; - - mTakingPicture = true; - try { - mCamera.takePicture( - null /* shutter */, - null /* raw */, - null /* postView */, - jpegCallback); - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.takePicture", e); - mTakingPicture = false; - if (mListener != null) { - mListener.onCameraError(ERROR_TAKING_PICTURE, e); - } - } - } - - void startVideo(final MediaCallback callback) { - Assert.notNull(callback); - Assert.isTrue(!isRecording()); - mVideoCallback = callback; - tryStartVideoCapture(); - } - - /** - * Asynchronously releases a camera - * @param camera The camera to release - */ - private void releaseCamera(final Camera camera) { - if (camera == null) { - return; - } - - mFocusOverlayManager.onCameraReleased(); - - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(final Void... params) { - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "Releasing camera " + mCameraIndex); - } - sCameraWrapper.release(camera); - return null; - } - }.execute(); - } - - private void releaseMediaRecorder(final boolean cleanupFile) { - if (mMediaRecorder == null) { - return; - } - mVideoModeRequested = false; - - if (cleanupFile) { - mMediaRecorder.cleanupTempFile(); - if (mVideoCallback != null) { - final MediaCallback callback = mVideoCallback; - mVideoCallback = null; - // Notify the callback that we've stopped recording - callback.onMediaReady(null /*uri*/, null /*contentType*/, 0 /*width*/, - 0 /*height*/); - } - } - - mMediaRecorder.release(); - mMediaRecorder = null; - - if (mCamera != null) { - try { - mCamera.reconnect(); - } catch (final IOException e) { - LogUtil.e(TAG, "IOException in CameraManager.releaseMediaRecorder", e); - if (mListener != null) { - mListener.onCameraError(ERROR_OPENING_CAMERA, e); - } - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.releaseMediaRecorder", e); - if (mListener != null) { - mListener.onCameraError(ERROR_OPENING_CAMERA, e); - } - } - } - restoreRequestedOrientation(); - } - - /** Updates the orientation of the camera to match the orientation of the device */ - private void updateCameraOrientation() { - if (mCamera == null || mCameraPreview == null || mTakingPicture) { - return; - } - - final WindowManager windowManager = - (WindowManager) mCameraPreview.getContext().getSystemService( - Context.WINDOW_SERVICE); - - int degrees = 0; - switch (windowManager.getDefaultDisplay().getRotation()) { - case Surface.ROTATION_0: degrees = 0; break; - case Surface.ROTATION_90: degrees = 90; break; - case Surface.ROTATION_180: degrees = 180; break; - case Surface.ROTATION_270: degrees = 270; break; - } - - // The display orientation of the camera (this controls the preview image). - int orientation; - - // The clockwise rotation angle relative to the orientation of the camera. This affects - // pictures returned by the camera in Camera.PictureCallback. - int rotation; - if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - orientation = (mCameraInfo.orientation + degrees) % 360; - rotation = orientation; - // compensate the mirror but only for orientation - orientation = (360 - orientation) % 360; - } else { // back-facing - orientation = (mCameraInfo.orientation - degrees + 360) % 360; - rotation = orientation; - } - mRotation = rotation; - if (mMediaRecorder == null) { - try { - mCamera.setDisplayOrientation(orientation); - final Camera.Parameters params = mCamera.getParameters(); - params.setRotation(rotation); - mCamera.setParameters(params); - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.updateCameraOrientation", e); - if (mListener != null) { - mListener.onCameraError(ERROR_OPENING_CAMERA, e); - } - } - } - } - - /** Sets the current camera, releasing any previously opened camera */ - private void setCamera(final Camera camera) { - if (mCamera == camera) { - return; - } - - releaseMediaRecorder(true /* cleanupFile */); - releaseCamera(mCamera); - mCamera = camera; - tryShowPreview(); - if (mListener != null) { - mListener.onCameraChanged(); - } - } - - /** Shows the preview if the camera is open and the preview is loaded */ - private void tryShowPreview() { - if (mCameraPreview == null || mCamera == null) { - if (mOrientationHandler != null) { - mOrientationHandler.disable(); - mOrientationHandler = null; - } - releaseMediaRecorder(true /* cleanupFile */); - mFocusOverlayManager.onPreviewStopped(); - return; - } - try { - mCamera.stopPreview(); - updateCameraOrientation(); - - final Camera.Parameters params = mCamera.getParameters(); - final Camera.Size pictureSize = chooseBestPictureSize(); - final Camera.Size previewSize = chooseBestPreviewSize(pictureSize); - params.setPreviewSize(previewSize.width, previewSize.height); - params.setPictureSize(pictureSize.width, pictureSize.height); - logCameraSize("Setting preview size: ", previewSize); - logCameraSize("Setting picture size: ", pictureSize); - mCameraPreview.setSize(previewSize, mCameraInfo.orientation); - for (final String focusMode : params.getSupportedFocusModes()) { - if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { - // Use continuous focus if available - params.setFocusMode(focusMode); - break; - } - } - - mCamera.setParameters(params); - mCameraPreview.startPreview(mCamera); - mCamera.startPreview(); - mCamera.setAutoFocusMoveCallback(new Camera.AutoFocusMoveCallback() { - @Override - public void onAutoFocusMoving(final boolean start, final Camera camera) { - mFocusOverlayManager.onAutoFocusMoving(start); - } - }); - mFocusOverlayManager.setParameters(mCamera.getParameters()); - mFocusOverlayManager.setMirror(mCameraInfo.facing == CameraInfo.CAMERA_FACING_BACK); - mFocusOverlayManager.onPreviewStarted(); - tryInitOrCleanupVideoMode(); - if (mOrientationHandler == null) { - mOrientationHandler = new OrientationHandler(mCameraPreview.getContext()); - mOrientationHandler.enable(); - } - } catch (final IOException e) { - LogUtil.e(TAG, "IOException in CameraManager.tryShowPreview", e); - if (mListener != null) { - mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); - } - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.tryShowPreview", e); - if (mListener != null) { - mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); - } - } - } - - private void tryInitOrCleanupVideoMode() { - if (!mVideoModeRequested || mCamera == null || mCameraPreview == null) { - releaseMediaRecorder(true /* cleanupFile */); - return; - } - - if (mMediaRecorder != null) { - return; - } - - try { - mCamera.unlock(); - final int maxMessageSize = getMmsConfig().getMaxMessageSize(); - mMediaRecorder = new MmsVideoRecorder(mCamera, mCameraIndex, mRotation, maxMessageSize); - mMediaRecorder.prepare(); - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "FileNotFoundException in CameraManager.tryInitOrCleanupVideoMode", e); - if (mListener != null) { - mListener.onCameraError(ERROR_STORAGE_FAILURE, e); - } - setVideoMode(false); - return; - } catch (final IOException e) { - LogUtil.e(TAG, "IOException in CameraManager.tryInitOrCleanupVideoMode", e); - if (mListener != null) { - mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); - } - setVideoMode(false); - return; - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.tryInitOrCleanupVideoMode", e); - if (mListener != null) { - mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); - } - setVideoMode(false); - return; - } - - tryStartVideoCapture(); - } - - private void tryStartVideoCapture() { - if (mMediaRecorder == null || mVideoCallback == null) { - return; - } - - mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() { - @Override - public void onError(final MediaRecorder mediaRecorder, final int what, - final int extra) { - if (mListener != null) { - mListener.onCameraError(ERROR_RECORDING_VIDEO, null); - } - restoreRequestedOrientation(); - } - }); - - mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { - @Override - public void onInfo(final MediaRecorder mediaRecorder, final int what, final int extra) { - if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED || - what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { - stopVideo(); - } - } - }); - - try { - mMediaRecorder.start(); - final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - lockOrientation(); - } catch (final IllegalStateException e) { - LogUtil.e(TAG, "IllegalStateException in CameraManager.tryStartVideoCapture", e); - if (mListener != null) { - mListener.onCameraError(ERROR_RECORDING_VIDEO, e); - } - setVideoMode(false); - restoreRequestedOrientation(); - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.tryStartVideoCapture", e); - if (mListener != null) { - mListener.onCameraError(ERROR_RECORDING_VIDEO, e); - } - setVideoMode(false); - restoreRequestedOrientation(); - } - } - - void stopVideo() { - int width = ImageRequest.UNSPECIFIED_SIZE; - int height = ImageRequest.UNSPECIFIED_SIZE; - Uri uri = null; - String contentType = null; - try { - final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - mMediaRecorder.stop(); - width = mMediaRecorder.getVideoWidth(); - height = mMediaRecorder.getVideoHeight(); - uri = mMediaRecorder.getVideoUri(); - contentType = mMediaRecorder.getContentType(); - } catch (final RuntimeException e) { - // MediaRecorder.stop will throw a RuntimeException if the video was too short, let the - // finally clause call the callback with null uri and handle cleanup - LogUtil.e(TAG, "RuntimeException in CameraManager.stopVideo", e); - } finally { - final MediaCallback videoCallback = mVideoCallback; - mVideoCallback = null; - releaseMediaRecorder(false /* cleanupFile */); - if (uri == null) { - tryInitOrCleanupVideoMode(); - } - videoCallback.onMediaReady(uri, contentType, width, height); - } - } - - boolean isCameraAvailable() { - return mCamera != null && !mTakingPicture && mIsHardwareAccelerationSupported; - } - - /** - * External components call into this to report if hardware acceleration is supported. When - * hardware acceleration isn't supported, we need to report an error through the listener - * interface - * @param isHardwareAccelerationSupported True if the preview is rendering in a hardware - * accelerated view. - */ - void reportHardwareAccelerationSupported(final boolean isHardwareAccelerationSupported) { - Assert.isMainThread(); - if (mIsHardwareAccelerationSupported == isHardwareAccelerationSupported) { - // If the value hasn't changed nothing more to do - return; - } - - mIsHardwareAccelerationSupported = isHardwareAccelerationSupported; - if (!isHardwareAccelerationSupported) { - LogUtil.e(TAG, "Software rendering - cannot open camera"); - if (mListener != null) { - mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); - } - } - } - - /** Returns the scale factor to scale the width/height to max allowed in MmsConfig */ - private float getScaleFactorForMaxAllowedSize(final int width, final int height, - final int maxWidth, final int maxHeight) { - if (maxWidth <= 0 || maxHeight <= 0) { - // MmsConfig initialization runs asynchronously on application startup, so there's a - // chance (albeit a very slight one) that we don't have it yet. - LogUtil.w(LogUtil.BUGLE_TAG, "Max image size not loaded in MmsConfig"); - return 1.0f; - } - - if (width <= maxWidth && height <= maxHeight) { - // Already meeting requirements. - return 1.0f; - } - - return Math.min(maxWidth * 1.0f / width, maxHeight * 1.0f / height); - } - - private MmsConfig getMmsConfig() { - final int subId = mSubscriptionDataProvider != null ? - mSubscriptionDataProvider.getConversationSelfSubId() : - ParticipantData.DEFAULT_SELF_SUB_ID; - return MmsConfig.get(subId); - } - - /** - * Choose the best picture size by trying to find a size close to the MmsConfig's max size, - * which is closest to the screen aspect ratio - */ - private Camera.Size chooseBestPictureSize() { - final Context context = mCameraPreview.getContext(); - final Resources resources = context.getResources(); - final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); - final int displayOrientation = resources.getConfiguration().orientation; - int cameraOrientation = mCameraInfo.orientation; - - int screenWidth; - int screenHeight; - if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE) { - // Rotate the camera orientation 90 degrees to compensate for the rotated display - // metrics. Direction doesn't matter because we're just using it for width/height - cameraOrientation += 90; - } - - // Check the camera orientation relative to the display. - // For 0, 180, 360, the screen width/height are the display width/height - // For 90, 270, the screen width/height are inverted from the display - if (cameraOrientation % 180 == 0) { - screenWidth = displayMetrics.widthPixels; - screenHeight = displayMetrics.heightPixels; - } else { - screenWidth = displayMetrics.heightPixels; - screenHeight = displayMetrics.widthPixels; - } - - final MmsConfig mmsConfig = getMmsConfig(); - final int maxWidth = mmsConfig.getMaxImageWidth(); - final int maxHeight = mmsConfig.getMaxImageHeight(); - - // Constrain the size within the max width/height defined by MmsConfig. - final float scaleFactor = getScaleFactorForMaxAllowedSize(screenWidth, screenHeight, - maxWidth, maxHeight); - screenWidth *= scaleFactor; - screenHeight *= scaleFactor; - - final float aspectRatio = BugleGservices.get().getFloat( - BugleGservicesKeys.CAMERA_ASPECT_RATIO, - screenWidth / (float) screenHeight); - final List<Camera.Size> sizes = new ArrayList<Camera.Size>( - mCamera.getParameters().getSupportedPictureSizes()); - final int maxPixels = maxWidth * maxHeight; - - // Sort the sizes so the best size is first - Collections.sort(sizes, new SizeComparator(maxWidth, maxHeight, aspectRatio, maxPixels)); - - return sizes.get(0); - } - - /** - * Chose the best preview size based on the picture size. Try to find a size with the same - * aspect ratio and size as the picture if possible - */ - private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) { - final List<Camera.Size> sizes = new ArrayList<Camera.Size>( - mCamera.getParameters().getSupportedPreviewSizes()); - final float aspectRatio = pictureSize.width / (float) pictureSize.height; - final int capturePixels = pictureSize.width * pictureSize.height; - - // Sort the sizes so the best size is first - Collections.sort(sizes, new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, - aspectRatio, capturePixels)); - - return sizes.get(0); - } - - private class OrientationHandler extends OrientationEventListener { - OrientationHandler(final Context context) { - super(context); - } - - @Override - public void onOrientationChanged(final int orientation) { - updateCameraOrientation(); - } - } - - private static class SizeComparator implements Comparator<Camera.Size> { - private static final int PREFER_LEFT = -1; - private static final int PREFER_RIGHT = 1; - - // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit - private final int mMaxWidth; - private final int mMaxHeight; - - // The desired aspect ratio - private final float mTargetAspectRatio; - - // The desired size (width x height) to try to match - private final int mTargetPixels; - - public SizeComparator(final int maxWidth, final int maxHeight, - final float targetAspectRatio, final int targetPixels) { - mMaxWidth = maxWidth; - mMaxHeight = maxHeight; - mTargetAspectRatio = targetAspectRatio; - mTargetPixels = targetPixels; - } - - /** - * Returns a negative value if left is a better choice than right, or a positive value if - * right is a better choice is better than left. 0 if they are equal - */ - @Override - public int compare(final Camera.Size left, final Camera.Size right) { - // If one size is less than the max size prefer it over the other - if ((left.width <= mMaxWidth && left.height <= mMaxHeight) != - (right.width <= mMaxWidth && right.height <= mMaxHeight)) { - return left.width <= mMaxWidth ? PREFER_LEFT : PREFER_RIGHT; - } - - // If one is closer to the target aspect ratio, prefer it. - final float leftAspectRatio = left.width / (float) left.height; - final float rightAspectRatio = right.width / (float) right.height; - final float leftAspectRatioDiff = Math.abs(leftAspectRatio - mTargetAspectRatio); - final float rightAspectRatioDiff = Math.abs(rightAspectRatio - mTargetAspectRatio); - if (leftAspectRatioDiff != rightAspectRatioDiff) { - return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? - PREFER_LEFT : PREFER_RIGHT; - } - - // At this point they have the same aspect ratio diff and are either both bigger - // than the max size or both smaller than the max size, so prefer the one closest - // to target size - final int leftDiff = Math.abs((left.width * left.height) - mTargetPixels); - final int rightDiff = Math.abs((right.width * right.height) - mTargetPixels); - return leftDiff - rightDiff; - } - } - - @Override // From FocusOverlayManager.Listener - public void autoFocus() { - if (mCamera == null) { - return; - } - - try { - mCamera.autoFocus(new Camera.AutoFocusCallback() { - @Override - public void onAutoFocus(final boolean success, final Camera camera) { - mFocusOverlayManager.onAutoFocus(success, false /* shutterDown */); - } - }); - } catch (final RuntimeException e) { - LogUtil.e(TAG, "RuntimeException in CameraManager.autoFocus", e); - // If autofocus fails, the camera should have called the callback with success=false, - // but some throw an exception here - mFocusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/); - } - } - - @Override // From FocusOverlayManager.Listener - public void cancelAutoFocus() { - if (mCamera == null) { - return; - } - try { - mCamera.cancelAutoFocus(); - } catch (final RuntimeException e) { - // Ignore - LogUtil.e(TAG, "RuntimeException in CameraManager.cancelAutoFocus", e); - } - } - - @Override // From FocusOverlayManager.Listener - public boolean capture() { - return false; - } - - @Override // From FocusOverlayManager.Listener - public void setFocusParameters() { - if (mCamera == null) { - return; - } - try { - final Camera.Parameters parameters = mCamera.getParameters(); - parameters.setFocusMode(mFocusOverlayManager.getFocusMode()); - if (parameters.getMaxNumFocusAreas() > 0) { - // Don't set focus areas (even to null) if focus areas aren't supported, camera may - // crash - parameters.setFocusAreas(mFocusOverlayManager.getFocusAreas()); - } - parameters.setMeteringAreas(mFocusOverlayManager.getMeteringAreas()); - mCamera.setParameters(parameters); - } catch (final RuntimeException e) { - // This occurs when the device is out of space or when the camera is locked - LogUtil.e(TAG, "RuntimeException in CameraManager setFocusParameters"); - } - } - - private void logCameraSize(final String prefix, final Camera.Size size) { - // Log the camera size and aspect ratio for help when examining bug reports for camera - // failures - LogUtil.i(TAG, prefix + size.width + "x" + size.height + - " (" + (size.width / (float) size.height) + ")"); - } - - - private Integer mSavedOrientation = null; - - private void lockOrientation() { - // when we start recording, lock our orientation - final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); - final WindowManager windowManager = - (WindowManager) a.getSystemService(Context.WINDOW_SERVICE); - final int rotation = windowManager.getDefaultDisplay().getRotation(); - - mSavedOrientation = a.getRequestedOrientation(); - switch (rotation) { - case Surface.ROTATION_0: - a.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - break; - case Surface.ROTATION_90: - a.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - break; - case Surface.ROTATION_180: - a.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); - break; - case Surface.ROTATION_270: - a.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); - break; - } - - } - - private void restoreRequestedOrientation() { - if (mSavedOrientation != null) { - final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); - if (a != null) { - a.setRequestedOrientation(mSavedOrientation); - } - mSavedOrientation = null; - } - } - - static boolean hasCameraPermission() { - return OsUtil.hasPermission(Manifest.permission.CAMERA); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/CameraMediaChooser.java b/src/com/android/messaging/ui/mediapicker/CameraMediaChooser.java deleted file mode 100644 index 2c7a7f2..0000000 --- a/src/com/android/messaging/ui/mediapicker/CameraMediaChooser.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - * 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.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.Rect; -import android.hardware.Camera; -import android.net.Uri; -import android.os.SystemClock; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.widget.Chronometer; -import android.widget.ImageButton; - -import com.android.messaging.R; -import com.android.messaging.datamodel.data.MediaPickerMessagePartData; -import com.android.messaging.ui.mediapicker.CameraManager.MediaCallback; -import com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay; -import com.android.messaging.util.Assert; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; - -/** - * Chooser which allows the user to take pictures or video without leaving the current app/activity - */ -class CameraMediaChooser extends MediaChooser implements - CameraManager.CameraManagerListener { - private CameraPreview.CameraPreviewHost mCameraPreviewHost; - private ImageButton mFullScreenButton; - private ImageButton mSwapCameraButton; - private ImageButton mSwapModeButton; - private ImageButton mCaptureButton; - private ImageButton mCancelVideoButton; - private Chronometer mVideoCounter; - private boolean mVideoCancelled; - private int mErrorToast; - private View mEnabledView; - private View mMissingPermissionView; - - CameraMediaChooser(final MediaPicker mediaPicker) { - super(mediaPicker); - } - - @Override - public int getSupportedMediaTypes() { - if (CameraManager.get().hasAnyCamera()) { - return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO; - } else { - return MediaPicker.MEDIA_TYPE_NONE; - } - } - - @Override - public View destroyView() { - CameraManager.get().closeCamera(); - CameraManager.get().setListener(null); - CameraManager.get().setSubscriptionDataProvider(null); - return super.destroyView(); - } - - @Override - protected View createView(final ViewGroup container) { - CameraManager.get().setListener(this); - CameraManager.get().setSubscriptionDataProvider(this); - CameraManager.get().setVideoMode(false); - final LayoutInflater inflater = getLayoutInflater(); - final CameraMediaChooserView view = (CameraMediaChooserView) inflater.inflate( - R.layout.mediapicker_camera_chooser, - container /* root */, - false /* attachToRoot */); - mCameraPreviewHost = (CameraPreview.CameraPreviewHost) view.findViewById( - R.id.camera_preview); - mCameraPreviewHost.getView().setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(final View view, final MotionEvent motionEvent) { - if (CameraManager.get().isVideoMode()) { - // Prevent the swipe down in video mode because video is always captured in - // full screen - return true; - } - - return false; - } - }); - - final View shutterVisual = view.findViewById(R.id.camera_shutter_visual); - - mFullScreenButton = (ImageButton) view.findViewById(R.id.camera_fullScreen_button); - mFullScreenButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - mMediaPicker.setFullScreen(true); - } - }); - - mSwapCameraButton = (ImageButton) view.findViewById(R.id.camera_swapCamera_button); - mSwapCameraButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - CameraManager.get().swapCamera(); - } - }); - - mCaptureButton = (ImageButton) view.findViewById(R.id.camera_capture_button); - mCaptureButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View v) { - final float heightPercent = Math.min(mMediaPicker.getViewPager().getHeight() / - (float) mCameraPreviewHost.getView().getHeight(), 1); - - if (CameraManager.get().isRecording()) { - CameraManager.get().stopVideo(); - } else { - final CameraManager.MediaCallback callback = new CameraManager.MediaCallback() { - @Override - public void onMediaReady( - final Uri uriToVideo, final String contentType, - final int width, final int height) { - mVideoCounter.stop(); - if (mVideoCancelled || uriToVideo == null) { - mVideoCancelled = false; - } else { - final Rect startRect = new Rect(); - // It's possible to throw out the chooser while taking the - // picture/video. In that case, still use the attachment, just - // skip the startRect - if (mView != null) { - mView.getGlobalVisibleRect(startRect); - } - mMediaPicker.dispatchItemsSelected( - new MediaPickerMessagePartData(startRect, contentType, - uriToVideo, width, height), - true /* dismissMediaPicker */); - } - updateViewState(); - } - - @Override - public void onMediaFailed(final Exception exception) { - UiUtils.showToastAtBottom(R.string.camera_media_failure); - updateViewState(); - } - - @Override - public void onMediaInfo(final int what) { - if (what == MediaCallback.MEDIA_NO_DATA) { - UiUtils.showToastAtBottom(R.string.camera_media_failure); - } - updateViewState(); - } - }; - if (CameraManager.get().isVideoMode()) { - CameraManager.get().startVideo(callback); - mVideoCounter.setBase(SystemClock.elapsedRealtime()); - mVideoCounter.start(); - updateViewState(); - } else { - showShutterEffect(shutterVisual); - CameraManager.get().takePicture(heightPercent, callback); - updateViewState(); - } - } - } - }); - - mSwapModeButton = (ImageButton) view.findViewById(R.id.camera_swap_mode_button); - mSwapModeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - final boolean isSwitchingToVideo = !CameraManager.get().isVideoMode(); - if (isSwitchingToVideo && !OsUtil.hasRecordAudioPermission()) { - requestRecordAudioPermission(); - } else { - onSwapMode(); - } - } - }); - - mCancelVideoButton = (ImageButton) view.findViewById(R.id.camera_cancel_button); - mCancelVideoButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - mVideoCancelled = true; - CameraManager.get().stopVideo(); - mMediaPicker.dismiss(true); - } - }); - - mVideoCounter = (Chronometer) view.findViewById(R.id.camera_video_counter); - - CameraManager.get().setRenderOverlay((RenderOverlay) view.findViewById(R.id.focus_visual)); - - mEnabledView = view.findViewById(R.id.mediapicker_enabled); - mMissingPermissionView = view.findViewById(R.id.missing_permission_view); - - // Must set mView before calling updateViewState because it operates on mView - mView = view; - updateViewState(); - updateForPermissionState(CameraManager.hasCameraPermission()); - return view; - } - - @Override - public int getIconResource() { - return R.drawable.ic_camera_light; - } - - @Override - public int getIconDescriptionResource() { - return R.string.mediapicker_cameraChooserDescription; - } - - /** - * Updates the view when entering or leaving full-screen camera mode - * @param fullScreen - */ - @Override - void onFullScreenChanged(final boolean fullScreen) { - super.onFullScreenChanged(fullScreen); - if (!fullScreen && CameraManager.get().isVideoMode()) { - CameraManager.get().setVideoMode(false); - } - updateViewState(); - } - - /** - * Initializes the control to a default state when it is opened / closed - * @param open True if the control is opened - */ - @Override - void onOpenedChanged(final boolean open) { - super.onOpenedChanged(open); - updateViewState(); - } - - @Override - protected void setSelected(final boolean selected) { - super.setSelected(selected); - if (selected) { - if (CameraManager.hasCameraPermission()) { - // If an error occurred before the chooser was selected, show it now - showErrorToastIfNeeded(); - } else { - requestCameraPermission(); - } - } - } - - private void requestCameraPermission() { - mMediaPicker.requestPermissions(new String[] { Manifest.permission.CAMERA }, - MediaPicker.CAMERA_PERMISSION_REQUEST_CODE); - } - - private void requestRecordAudioPermission() { - mMediaPicker.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, - MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE); - } - - @Override - protected void onRequestPermissionsResult( - final int requestCode, final String permissions[], final int[] grantResults) { - if (requestCode == MediaPicker.CAMERA_PERMISSION_REQUEST_CODE) { - final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED; - updateForPermissionState(permissionGranted); - if (permissionGranted) { - mCameraPreviewHost.onCameraPermissionGranted(); - } - } else if (requestCode == MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE) { - Assert.isFalse(CameraManager.get().isVideoMode()); - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Switch to video mode - onSwapMode(); - } else { - // Stay in still-photo mode - } - } - } - - private void updateForPermissionState(final boolean granted) { - // onRequestPermissionsResult can sometimes get called before createView(). - if (mEnabledView == null) { - return; - } - - mEnabledView.setVisibility(granted ? View.VISIBLE : View.GONE); - mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE); - } - - @Override - public boolean canSwipeDown() { - if (CameraManager.get().isVideoMode()) { - return true; - } - return super.canSwipeDown(); - } - - /** - * Handles an error from the camera manager by showing the appropriate error message to the user - * @param errorCode One of the CameraManager.ERROR_* constants - * @param e The exception which caused the error, if any - */ - @Override - public void onCameraError(final int errorCode, final Exception e) { - switch (errorCode) { - case CameraManager.ERROR_OPENING_CAMERA: - case CameraManager.ERROR_SHOWING_PREVIEW: - mErrorToast = R.string.camera_error_opening; - break; - case CameraManager.ERROR_INITIALIZING_VIDEO: - mErrorToast = R.string.camera_error_video_init_fail; - updateViewState(); - break; - case CameraManager.ERROR_STORAGE_FAILURE: - mErrorToast = R.string.camera_error_storage_fail; - updateViewState(); - break; - case CameraManager.ERROR_TAKING_PICTURE: - mErrorToast = R.string.camera_error_failure_taking_picture; - break; - default: - mErrorToast = R.string.camera_error_unknown; - LogUtil.w(LogUtil.BUGLE_TAG, "Unknown camera error:" + errorCode); - break; - } - showErrorToastIfNeeded(); - } - - private void showErrorToastIfNeeded() { - if (mErrorToast != 0 && mSelected) { - UiUtils.showToastAtBottom(mErrorToast); - mErrorToast = 0; - } - } - - @Override - public void onCameraChanged() { - updateViewState(); - } - - private void onSwapMode() { - CameraManager.get().setVideoMode(!CameraManager.get().isVideoMode()); - if (CameraManager.get().isVideoMode()) { - mMediaPicker.setFullScreen(true); - - // For now we start recording immediately - mCaptureButton.performClick(); - } - updateViewState(); - } - - private void showShutterEffect(final View shutterVisual) { - final float maxAlpha = getContext().getResources().getFraction( - R.fraction.camera_shutter_max_alpha, 1 /* base */, 1 /* pBase */); - - // Divide by 2 so each half of the animation adds up to the full duration - final int animationDuration = getContext().getResources().getInteger( - R.integer.camera_shutter_duration) / 2; - - final AnimationSet animation = new AnimationSet(false /* shareInterpolator */); - final Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha); - alphaInAnimation.setDuration(animationDuration); - animation.addAnimation(alphaInAnimation); - - final Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f); - alphaOutAnimation.setStartOffset(animationDuration); - alphaOutAnimation.setDuration(animationDuration); - animation.addAnimation(alphaOutAnimation); - - animation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(final Animation animation) { - shutterVisual.setVisibility(View.VISIBLE); - } - - @Override - public void onAnimationEnd(final Animation animation) { - shutterVisual.setVisibility(View.GONE); - } - - @Override - public void onAnimationRepeat(final Animation animation) { - } - }); - shutterVisual.startAnimation(animation); - } - - /** Updates the state of the buttons and overlays based on the current state of the view */ - private void updateViewState() { - if (mView == null) { - return; - } - - final Context context = getContext(); - if (context == null) { - // Context is null if the fragment was already removed from the activity - return; - } - final boolean fullScreen = mMediaPicker.isFullScreen(); - final boolean videoMode = CameraManager.get().isVideoMode(); - final boolean isRecording = CameraManager.get().isRecording(); - final boolean isCameraAvailable = isCameraAvailable(); - final Camera.CameraInfo cameraInfo = CameraManager.get().getCameraInfo(); - final boolean frontCamera = cameraInfo != null && cameraInfo.facing == - Camera.CameraInfo.CAMERA_FACING_FRONT; - - mView.setSystemUiVisibility( - fullScreen ? View.SYSTEM_UI_FLAG_LOW_PROFILE : - View.SYSTEM_UI_FLAG_VISIBLE); - - mFullScreenButton.setVisibility(!fullScreen ? View.VISIBLE : View.GONE); - mFullScreenButton.setEnabled(isCameraAvailable); - mSwapCameraButton.setVisibility( - fullScreen && !isRecording && CameraManager.get().hasFrontAndBackCamera() ? - View.VISIBLE : View.GONE); - mSwapCameraButton.setImageResource(frontCamera ? - R.drawable.ic_camera_front_light : - R.drawable.ic_camera_rear_light); - mSwapCameraButton.setEnabled(isCameraAvailable); - - mCancelVideoButton.setVisibility(isRecording ? View.VISIBLE : View.GONE); - mVideoCounter.setVisibility(isRecording ? View.VISIBLE : View.GONE); - - mSwapModeButton.setImageResource(videoMode ? - R.drawable.ic_mp_camera_small_light : - R.drawable.ic_mp_video_small_light); - mSwapModeButton.setContentDescription(context.getString(videoMode ? - R.string.camera_switch_to_still_mode : R.string.camera_switch_to_video_mode)); - mSwapModeButton.setVisibility(isRecording ? View.GONE : View.VISIBLE); - mSwapModeButton.setEnabled(isCameraAvailable); - - if (isRecording) { - mCaptureButton.setImageResource(R.drawable.ic_mp_capture_stop_large_light); - mCaptureButton.setContentDescription(context.getString( - R.string.camera_stop_recording)); - } else if (videoMode) { - mCaptureButton.setImageResource(R.drawable.ic_mp_video_large_light); - mCaptureButton.setContentDescription(context.getString( - R.string.camera_start_recording)); - } else { - mCaptureButton.setImageResource(R.drawable.ic_checkmark_large_light); - mCaptureButton.setContentDescription(context.getString( - R.string.camera_take_picture)); - } - mCaptureButton.setEnabled(isCameraAvailable); - } - - @Override - int getActionBarTitleResId() { - return 0; - } - - /** - * Returns if the camera is currently ready camera is loaded and not taking a picture. - * otherwise we should avoid taking another picture, swapping camera or recording video. - */ - private boolean isCameraAvailable() { - return CameraManager.get().isCameraAvailable(); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/CameraMediaChooserView.java b/src/com/android/messaging/ui/mediapicker/CameraMediaChooserView.java deleted file mode 100644 index 64c07b2..0000000 --- a/src/com/android/messaging/ui/mediapicker/CameraMediaChooserView.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.content.Context; -import android.graphics.Canvas; -import android.hardware.Camera; -import android.os.Bundle; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import com.android.messaging.R; -import com.android.messaging.ui.PersistentInstanceState; -import com.android.messaging.util.ThreadUtil; - -public class CameraMediaChooserView extends FrameLayout implements PersistentInstanceState { - private static final String KEY_CAMERA_INDEX = "camera_index"; - - // True if we have at least queued an update to the view tree to support software rendering - // fallback - private boolean mIsSoftwareFallbackActive; - - public CameraMediaChooserView(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected Parcelable onSaveInstanceState() { - final Bundle bundle = new Bundle(); - bundle.putInt(KEY_CAMERA_INDEX, CameraManager.get().getCameraIndex()); - return bundle; - } - - @Override - protected void onRestoreInstanceState(final Parcelable state) { - if (!(state instanceof Bundle)) { - return; - } - - final Bundle bundle = (Bundle) state; - CameraManager.get().selectCameraByIndex(bundle.getInt(KEY_CAMERA_INDEX)); - } - - @Override - public Parcelable saveState() { - return onSaveInstanceState(); - } - - @Override - public void restoreState(final Parcelable restoredState) { - onRestoreInstanceState(restoredState); - } - - @Override - public void resetState() { - CameraManager.get().selectCamera(Camera.CameraInfo.CAMERA_FACING_BACK); - } - - @Override - protected void onDraw(final Canvas canvas) { - super.onDraw(canvas); - // If the canvas isn't hardware accelerated, we have to replace the HardwareCameraPreview - // with a SoftwareCameraPreview which supports software rendering - if (!canvas.isHardwareAccelerated() && !mIsSoftwareFallbackActive) { - mIsSoftwareFallbackActive = true; - // Post modifying the tree since we can't modify the view tree during a draw pass - ThreadUtil.getMainThreadHandler().post(new Runnable() { - @Override - public void run() { - final HardwareCameraPreview cameraPreview = - (HardwareCameraPreview) findViewById(R.id.camera_preview); - if (cameraPreview == null) { - return; - } - final ViewGroup parent = ((ViewGroup) cameraPreview.getParent()); - final int index = parent.indexOfChild(cameraPreview); - final SoftwareCameraPreview softwareCameraPreview = - new SoftwareCameraPreview(getContext()); - // Be sure to remove the hardware view before adding the software view to - // prevent having 2 camera previews active at the same time - parent.removeView(cameraPreview); - parent.addView(softwareCameraPreview, index); - } - }); - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/CameraPreview.java b/src/com/android/messaging/ui/mediapicker/CameraPreview.java deleted file mode 100644 index ecac978..0000000 --- a/src/com/android/messaging/ui/mediapicker/CameraPreview.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.content.Context; -import android.content.res.Configuration; -import android.hardware.Camera; -import android.view.View; -import android.view.View.MeasureSpec; -import com.android.messaging.util.Assert; - -import java.io.IOException; - -/** - * Contains shared code for SoftwareCameraPreview and HardwareCameraPreview, cannot use inheritance - * because those classes must inherit from separate Views, so those classes delegate calls to this - * helper class. Specifics for each implementation are in CameraPreviewHost - */ -public class CameraPreview { - public interface CameraPreviewHost { - View getView(); - boolean isValid(); - void startPreview(final Camera camera) throws IOException; - void onCameraPermissionGranted(); - - } - - private int mCameraWidth = -1; - private int mCameraHeight = -1; - - private final CameraPreviewHost mHost; - - public CameraPreview(final CameraPreviewHost host) { - Assert.notNull(host); - Assert.notNull(host.getView()); - mHost = host; - } - - public void setSize(final Camera.Size size, final int orientation) { - switch (orientation) { - case 0: - case 180: - mCameraWidth = size.width; - mCameraHeight = size.height; - break; - case 90: - case 270: - default: - mCameraWidth = size.height; - mCameraHeight = size.width; - } - mHost.getView().requestLayout(); - } - - public int getWidthMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) { - if (mCameraHeight >= 0) { - final int width = View.MeasureSpec.getSize(widthMeasureSpec); - return MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); - } else { - return widthMeasureSpec; - } - } - - public int getHeightMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) { - if (mCameraHeight >= 0) { - final int orientation = getContext().getResources().getConfiguration().orientation; - final int width = View.MeasureSpec.getSize(widthMeasureSpec); - final float aspectRatio = (float) mCameraWidth / (float) mCameraHeight; - int height; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - height = (int) (width * aspectRatio); - } else { - height = (int) (width / aspectRatio); - } - return View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); - } else { - return heightMeasureSpec; - } - } - - public void onVisibilityChanged(final int visibility) { - if (CameraManager.hasCameraPermission()) { - if (visibility == View.VISIBLE) { - CameraManager.get().openCamera(); - } else { - CameraManager.get().closeCamera(); - } - } - } - - public Context getContext() { - return mHost.getView().getContext(); - } - - public void setOnTouchListener(final View.OnTouchListener listener) { - mHost.getView().setOnTouchListener(listener); - } - - public int getHeight() { - return mHost.getView().getHeight(); - } - - public void onAttachedToWindow() { - if (CameraManager.hasCameraPermission()) { - CameraManager.get().openCamera(); - } - } - - public void onDetachedFromWindow() { - CameraManager.get().closeCamera(); - } - - public void onRestoreInstanceState() { - if (CameraManager.hasCameraPermission()) { - CameraManager.get().openCamera(); - } - } - - public void onCameraPermissionGranted() { - CameraManager.get().openCamera(); - } - - /** - * @return True if the view is valid and prepared for the camera to start showing the preview - */ - public boolean isValid() { - return mHost.isValid(); - } - - /** - * Starts the camera preview on the current surface. Abstracts out the differences in API - * from the CameraManager - * @throws IOException Which is caught by the CameraManager to display an error - */ - public void startPreview(final Camera camera) throws IOException { - mHost.startPreview(camera); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/DocumentImagePicker.java b/src/com/android/messaging/ui/mediapicker/DocumentImagePicker.java deleted file mode 100644 index 2c36752..0000000 --- a/src/com/android/messaging/ui/mediapicker/DocumentImagePicker.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.Intent; -import android.net.Uri; -import android.os.Bundle; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.data.PendingAttachmentData; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.ImageUtils; -import com.android.messaging.util.SafeAsyncTask; - -/** - * Wraps around the functionalities to allow the user to pick images from the document - * picker. Instances of this class must be tied to a Fragment which is able to delegate activity - * result callbacks. - */ -public class DocumentImagePicker { - - /** - * An interface for a listener that listens for when a document has been picked. - */ - public interface SelectionListener { - /** - * Called when an document is selected from picker. At this point, the file hasn't been - * actually loaded and staged in the temp directory, so we are passing in a pending - * MessagePartData, which the consumer should use to display a placeholder image. - * @param pendingItem a temporary attachment data for showing the placeholder state. - */ - void onDocumentSelected(PendingAttachmentData pendingItem); - } - - // The owning fragment. - private final Fragment mFragment; - - // The listener on the picker events. - private final SelectionListener mListener; - - private static final String EXTRA_PHOTO_URL = "photo_url"; - - /** - * Creates a new instance of DocumentImagePicker. - * @param activity The activity that owns the picker, or the activity that hosts the owning - * fragment. - */ - public DocumentImagePicker(final Fragment fragment, - final SelectionListener listener) { - mFragment = fragment; - mListener = listener; - } - - /** - * Intent out to open an image/video from document picker. - */ - public void launchPicker() { - UIIntents.get().launchDocumentImagePicker(mFragment); - } - - /** - * Must be called from the fragment/activity's onActivityResult(). - */ - public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - if (requestCode == UIIntents.REQUEST_PICK_IMAGE_FROM_DOCUMENT_PICKER && - resultCode == Activity.RESULT_OK) { - // Sometimes called after media item has been picked from the document picker. - String url = data.getStringExtra(EXTRA_PHOTO_URL); - if (url == null) { - // we're using the builtin photo picker which supplies the return - // url as it's "data" - url = data.getDataString(); - if (url == null) { - final Bundle extras = data.getExtras(); - if (extras != null) { - final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM); - if (uri != null) { - url = uri.toString(); - } - } - } - } - - // Guard against null uri cases for when the activity returns a null/invalid intent. - if (url != null) { - final Uri uri = Uri.parse(url); - prepareDocumentForAttachment(uri); - } - } - } - - private void prepareDocumentForAttachment(final Uri documentUri) { - // Notify our listener with a PendingAttachmentData containing the metadata. - // Asynchronously get the content type for the picked image since - // ImageUtils.getContentType() potentially involves I/O and can be expensive. - new SafeAsyncTask<Void, Void, String>() { - @Override - protected String doInBackgroundTimed(final Void... params) { - return ImageUtils.getContentType( - Factory.get().getApplicationContext().getContentResolver(), documentUri); - } - - @Override - protected void onPostExecute(final String contentType) { - // Ask the listener to create a temporary placeholder item to show the progress. - final PendingAttachmentData pendingItem = - PendingAttachmentData.createPendingAttachmentData(contentType, - documentUri); - mListener.onDocumentSelected(pendingItem); - } - }.executeOnThreadPool(); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/GalleryGridAdapter.java b/src/com/android/messaging/ui/mediapicker/GalleryGridAdapter.java deleted file mode 100644 index fda3b19..0000000 --- a/src/com/android/messaging/ui/mediapicker/GalleryGridAdapter.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.content.Context; -import android.database.Cursor; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; - -import com.android.messaging.R; -import com.android.messaging.ui.mediapicker.GalleryGridItemView.HostInterface; -import com.android.messaging.util.Assert; - -/** - * Bridges between the image cursor loaded by GalleryBoundCursorLoader and the GalleryGridView. - */ -public class GalleryGridAdapter extends CursorAdapter { - private GalleryGridItemView.HostInterface mGgivHostInterface; - - public GalleryGridAdapter(final Context context, final Cursor cursor) { - super(context, cursor, 0); - } - - public void setHostInterface(final HostInterface ggivHostInterface) { - mGgivHostInterface = ggivHostInterface; - } - - /** - * {@inheritDoc} - */ - @Override - public void bindView(final View view, final Context context, final Cursor cursor) { - Assert.isTrue(view instanceof GalleryGridItemView); - final GalleryGridItemView galleryImageView = (GalleryGridItemView) view; - galleryImageView.bind(cursor, mGgivHostInterface); - } - - /** - * {@inheritDoc} - */ - @Override - public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { - final LayoutInflater layoutInflater = LayoutInflater.from(context); - return layoutInflater.inflate(R.layout.gallery_grid_item_view, parent, false); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/GalleryGridItemView.java b/src/com/android/messaging/ui/mediapicker/GalleryGridItemView.java deleted file mode 100644 index 3d71fe6..0000000 --- a/src/com/android/messaging/ui/mediapicker/GalleryGridItemView.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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.content.Context; -import android.database.Cursor; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.TouchDelegate; -import android.view.View; -import android.widget.CheckBox; -import android.widget.FrameLayout; -import android.widget.ImageView.ScaleType; - -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.datamodel.data.GalleryGridItemData; -import com.android.messaging.ui.AsyncImageView; -import com.android.messaging.ui.ConversationDrawables; -import com.google.common.annotations.VisibleForTesting; - -import java.util.concurrent.TimeUnit; - -/** - * Shows an item in the gallery picker grid view. Hosts an FileImageView with a checkbox. - */ -public class GalleryGridItemView extends FrameLayout { - /** - * Implemented by the owner of this GalleryGridItemView instance to communicate on media - * picking and selection events. - */ - public interface HostInterface { - void onItemClicked(View view, GalleryGridItemData data, boolean longClick); - boolean isItemSelected(GalleryGridItemData data); - boolean isMultiSelectEnabled(); - } - - @VisibleForTesting - GalleryGridItemData mData; - private AsyncImageView mImageView; - private CheckBox mCheckBox; - private HostInterface mHostInterface; - private final OnClickListener mOnClickListener = new OnClickListener() { - @Override - public void onClick(final View v) { - mHostInterface.onItemClicked(GalleryGridItemView.this, mData, false /*longClick*/); - } - }; - - public GalleryGridItemView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mData = DataModel.get().createGalleryGridItemData(); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mImageView = (AsyncImageView) findViewById(R.id.image); - mCheckBox = (CheckBox) findViewById(R.id.checkbox); - mCheckBox.setOnClickListener(mOnClickListener); - setOnClickListener(mOnClickListener); - final OnLongClickListener longClickListener = new OnLongClickListener() { - @Override - public boolean onLongClick(final View v) { - mHostInterface.onItemClicked(v, mData, true /* longClick */); - return true; - } - }; - setOnLongClickListener(longClickListener); - mCheckBox.setOnLongClickListener(longClickListener); - addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - // Enlarge the clickable region for the checkbox to fill the entire view. - final Rect region = new Rect(0, 0, getWidth(), getHeight()); - setTouchDelegate(new TouchDelegate(region, mCheckBox) { - @Override - public boolean onTouchEvent(MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - setPressed(true); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - setPressed(false); - break; - } - return super.onTouchEvent(event); - } - }); - } - }); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - // The grid view auto-fit the columns, so we want to let the height match the width - // to make the image square. - super.onMeasure(widthMeasureSpec, widthMeasureSpec); - } - - public void bind(final Cursor cursor, final HostInterface hostInterface) { - final int desiredSize = getResources() - .getDimensionPixelSize(R.dimen.gallery_image_cell_size); - mData.bind(cursor, desiredSize, desiredSize); - mHostInterface = hostInterface; - updateViewState(); - } - - private void updateViewState() { - updateImageView(); - if (mHostInterface.isMultiSelectEnabled() && !mData.isDocumentPickerItem()) { - mCheckBox.setVisibility(VISIBLE); - mCheckBox.setClickable(true); - mCheckBox.setChecked(mHostInterface.isItemSelected(mData)); - } else { - mCheckBox.setVisibility(GONE); - mCheckBox.setClickable(false); - } - } - - private void updateImageView() { - if (mData.isDocumentPickerItem()) { - mImageView.setScaleType(ScaleType.CENTER); - setBackgroundColor(ConversationDrawables.get().getConversationThemeColor()); - mImageView.setImageResourceId(null); - mImageView.setImageResource(R.drawable.ic_photo_library_light); - mImageView.setContentDescription(getResources().getString( - R.string.pick_image_from_document_library_content_description)); - } else { - mImageView.setScaleType(ScaleType.CENTER_CROP); - setBackgroundColor(getResources().getColor(R.color.gallery_image_default_background)); - mImageView.setImageResourceId(mData.getImageRequestDescriptor()); - final long dateSeconds = mData.getDateSeconds(); - final boolean isValidDate = (dateSeconds > 0); - final int templateId = isValidDate ? - R.string.mediapicker_gallery_image_item_description : - R.string.mediapicker_gallery_image_item_description_no_date; - String contentDescription = String.format(getResources().getString(templateId), - dateSeconds * TimeUnit.SECONDS.toMillis(1)); - mImageView.setContentDescription(contentDescription); - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/GalleryGridView.java b/src/com/android/messaging/ui/mediapicker/GalleryGridView.java deleted file mode 100644 index a5a7dad..0000000 --- a/src/com/android/messaging/ui/mediapicker/GalleryGridView.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * 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.content.Context; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.v4.util.ArrayMap; -import android.util.AttributeSet; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; - -import com.android.messaging.R; -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.GalleryGridItemData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener; -import com.android.messaging.ui.PersistentInstanceState; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; - -import java.util.Iterator; -import java.util.Map; - -/** - * Shows a list of galley images from external storage in a GridView with multi-select - * capabilities, and with the option to intent out to a standalone image picker. - */ -public class GalleryGridView extends MediaPickerGridView implements - GalleryGridItemView.HostInterface, - PersistentInstanceState, - DraftMessageDataListener { - /** - * Implemented by the owner of this GalleryGridView instance to communicate on image - * picking and multi-image selection events. - */ - public interface GalleryGridViewListener { - void onDocumentPickerItemClicked(); - void onItemSelected(MessagePartData item); - void onItemUnselected(MessagePartData item); - void onConfirmSelection(); - void onUpdate(); - } - - private GalleryGridViewListener mListener; - - // TODO: Consider putting this into the data model object if we add more states. - private final ArrayMap<Uri, MessagePartData> mSelectedImages; - private boolean mIsMultiSelectMode = false; - private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel; - - public GalleryGridView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mSelectedImages = new ArrayMap<Uri, MessagePartData>(); - } - - public void setHostInterface(final GalleryGridViewListener hostInterface) { - mListener = hostInterface; - } - - public void setDraftMessageDataModel(final BindingBase<DraftMessageData> dataModel) { - mDraftMessageDataModel = BindingBase.createBindingReference(dataModel); - mDraftMessageDataModel.getData().addListener(this); - } - - @Override - public void onItemClicked(final View view, final GalleryGridItemData data, - final boolean longClick) { - if (data.isDocumentPickerItem()) { - mListener.onDocumentPickerItemClicked(); - } else if (ContentType.isMediaType(data.getContentType())) { - if (longClick) { - // Turn on multi-select mode when an item is long-pressed. - setMultiSelectEnabled(true); - } - - final Rect startRect = new Rect(); - view.getGlobalVisibleRect(startRect); - if (isMultiSelectEnabled()) { - toggleItemSelection(startRect, data); - } else { - mListener.onItemSelected(data.constructMessagePartData(startRect)); - } - } else { - LogUtil.w(LogUtil.BUGLE_TAG, - "Selected item has invalid contentType " + data.getContentType()); - } - } - - @Override - public boolean isItemSelected(final GalleryGridItemData data) { - return mSelectedImages.containsKey(data.getImageUri()); - } - - int getSelectionCount() { - return mSelectedImages.size(); - } - - @Override - public boolean isMultiSelectEnabled() { - return mIsMultiSelectMode; - } - - private void toggleItemSelection(final Rect startRect, final GalleryGridItemData data) { - Assert.isTrue(isMultiSelectEnabled()); - if (isItemSelected(data)) { - final MessagePartData item = mSelectedImages.remove(data.getImageUri()); - mListener.onItemUnselected(item); - if (mSelectedImages.size() == 0) { - // No image is selected any more, turn off multi-select mode. - setMultiSelectEnabled(false); - } - } else { - final MessagePartData item = data.constructMessagePartData(startRect); - mSelectedImages.put(data.getImageUri(), item); - mListener.onItemSelected(item); - } - invalidateViews(); - } - - private void toggleMultiSelect() { - mIsMultiSelectMode = !mIsMultiSelectMode; - invalidateViews(); - } - - private void setMultiSelectEnabled(final boolean enabled) { - if (mIsMultiSelectMode != enabled) { - toggleMultiSelect(); - } - } - - private boolean canToggleMultiSelect() { - // We allow the user to toggle multi-select mode only when nothing has selected. If - // something has been selected, we show a confirm button instead. - return mSelectedImages.size() == 0; - } - - public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) { - inflater.inflate(R.menu.gallery_picker_menu, menu); - final MenuItem toggleMultiSelect = menu.findItem(R.id.action_multiselect); - final MenuItem confirmMultiSelect = menu.findItem(R.id.action_confirm_multiselect); - final boolean canToggleMultiSelect = canToggleMultiSelect(); - toggleMultiSelect.setVisible(canToggleMultiSelect); - confirmMultiSelect.setVisible(!canToggleMultiSelect); - } - - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_multiselect: - Assert.isTrue(canToggleMultiSelect()); - toggleMultiSelect(); - return true; - - case R.id.action_confirm_multiselect: - Assert.isTrue(!canToggleMultiSelect()); - mListener.onConfirmSelection(); - return true; - } - return false; - } - - - @Override - public void onDraftChanged(final DraftMessageData data, final int changeFlags) { - mDraftMessageDataModel.ensureBound(data); - // Whenever attachment changed, refresh selection state to remove those that are not - // selected. - if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) == - DraftMessageData.ATTACHMENTS_CHANGED) { - refreshImageSelectionStateOnAttachmentChange(); - } - } - - @Override - public void onDraftAttachmentLimitReached(final DraftMessageData data) { - mDraftMessageDataModel.ensureBound(data); - // Whenever draft attachment limit is reach, refresh selection state to remove those - // not actually added to draft. - refreshImageSelectionStateOnAttachmentChange(); - } - - @Override - public void onDraftAttachmentLoadFailed() { - // Nothing to do since the failed attachment gets removed automatically. - } - - private void refreshImageSelectionStateOnAttachmentChange() { - boolean changed = false; - final Iterator<Map.Entry<Uri, MessagePartData>> iterator = - mSelectedImages.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry<Uri, MessagePartData> entry = iterator.next(); - if (!mDraftMessageDataModel.getData().containsAttachment(entry.getKey())) { - iterator.remove(); - changed = true; - } - } - - if (changed) { - mListener.onUpdate(); - invalidateViews(); - } - } - - @Override // PersistentInstanceState - public Parcelable saveState() { - return onSaveInstanceState(); - } - - @Override // PersistentInstanceState - public void restoreState(final Parcelable restoredState) { - onRestoreInstanceState(restoredState); - invalidateViews(); - } - - @Override - public Parcelable onSaveInstanceState() { - final Parcelable superState = super.onSaveInstanceState(); - final SavedState savedState = new SavedState(superState); - savedState.isMultiSelectMode = mIsMultiSelectMode; - savedState.selectedImages = mSelectedImages.values() - .toArray(new MessagePartData[mSelectedImages.size()]); - return savedState; - } - - @Override - public void onRestoreInstanceState(final Parcelable state) { - if (!(state instanceof SavedState)) { - super.onRestoreInstanceState(state); - return; - } - - final SavedState savedState = (SavedState) state; - super.onRestoreInstanceState(savedState.getSuperState()); - mIsMultiSelectMode = savedState.isMultiSelectMode; - mSelectedImages.clear(); - for (int i = 0; i < savedState.selectedImages.length; i++) { - final MessagePartData selectedImage = savedState.selectedImages[i]; - mSelectedImages.put(selectedImage.getContentUri(), selectedImage); - } - } - - @Override // PersistentInstanceState - public void resetState() { - mSelectedImages.clear(); - mIsMultiSelectMode = false; - invalidateViews(); - } - - public static class SavedState extends BaseSavedState { - boolean isMultiSelectMode; - MessagePartData[] selectedImages; - - SavedState(final Parcelable superState) { - super(superState); - } - - private SavedState(final Parcel in) { - super(in); - isMultiSelectMode = in.readInt() == 1 ? true : false; - - // Read parts - final int partCount = in.readInt(); - selectedImages = new MessagePartData[partCount]; - for (int i = 0; i < partCount; i++) { - selectedImages[i] = ((MessagePartData) in.readParcelable( - MessagePartData.class.getClassLoader())); - } - } - - @Override - public void writeToParcel(final Parcel out, final int flags) { - super.writeToParcel(out, flags); - out.writeInt(isMultiSelectMode ? 1 : 0); - - // Write parts - out.writeInt(selectedImages.length); - for (final MessagePartData image : selectedImages) { - out.writeParcelable(image, flags); - } - } - - public static final Parcelable.Creator<SavedState> CREATOR = - new Parcelable.Creator<SavedState>() { - @Override - public SavedState createFromParcel(final Parcel in) { - return new SavedState(in); - } - @Override - public SavedState[] newArray(final int size) { - return new SavedState[size]; - } - }; - } -} diff --git a/src/com/android/messaging/ui/mediapicker/GalleryMediaChooser.java b/src/com/android/messaging/ui/mediapicker/GalleryMediaChooser.java deleted file mode 100644 index 9422386..0000000 --- a/src/com/android/messaging/ui/mediapicker/GalleryMediaChooser.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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.Manifest; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.support.v7.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 com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.data.GalleryGridItemData; -import com.android.messaging.datamodel.data.MediaPickerData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.MediaPickerData.MediaPickerDataListener; -import com.android.messaging.util.Assert; -import com.android.messaging.util.OsUtil; - -/** - * Chooser which allows the user to select one or more existing images or videos - */ -class GalleryMediaChooser extends MediaChooser implements - GalleryGridView.GalleryGridViewListener, MediaPickerDataListener { - private final GalleryGridAdapter mAdapter; - private GalleryGridView mGalleryGridView; - private View mMissingPermissionView; - - GalleryMediaChooser(final MediaPicker mediaPicker) { - super(mediaPicker); - mAdapter = new GalleryGridAdapter(Factory.get().getApplicationContext(), null); - } - - @Override - public int getSupportedMediaTypes() { - return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO; - } - - @Override - public View destroyView() { - mGalleryGridView.setAdapter(null); - mAdapter.setHostInterface(null); - // The loader is started only if startMediaPickerDataLoader() is called - if (OsUtil.hasStoragePermission()) { - mBindingRef.getData().destroyLoader(MediaPickerData.GALLERY_IMAGE_LOADER); - } - return super.destroyView(); - } - - @Override - public int getIconResource() { - return R.drawable.ic_image_light; - } - - @Override - public int getIconDescriptionResource() { - return R.string.mediapicker_galleryChooserDescription; - } - - @Override - public boolean canSwipeDown() { - return mGalleryGridView.canSwipeDown(); - } - - @Override - public void onItemSelected(final MessagePartData item) { - mMediaPicker.dispatchItemsSelected(item, !mGalleryGridView.isMultiSelectEnabled()); - } - - @Override - public void onItemUnselected(final MessagePartData item) { - mMediaPicker.dispatchItemUnselected(item); - } - - @Override - public void onConfirmSelection() { - // The user may only confirm if multiselect is enabled. - Assert.isTrue(mGalleryGridView.isMultiSelectEnabled()); - mMediaPicker.dispatchConfirmItemSelection(); - } - - @Override - public void onUpdate() { - mMediaPicker.invalidateOptionsMenu(); - } - - @Override - public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) { - if (mView != null) { - mGalleryGridView.onCreateOptionsMenu(inflater, menu); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - return (mView != null) ? mGalleryGridView.onOptionsItemSelected(item) : false; - } - - @Override - protected View createView(final ViewGroup container) { - final LayoutInflater inflater = getLayoutInflater(); - final View view = inflater.inflate( - R.layout.mediapicker_image_chooser, - container /* root */, - false /* attachToRoot */); - - mGalleryGridView = (GalleryGridView) view.findViewById(R.id.gallery_grid_view); - mAdapter.setHostInterface(mGalleryGridView); - mGalleryGridView.setAdapter(mAdapter); - mGalleryGridView.setHostInterface(this); - mGalleryGridView.setDraftMessageDataModel(mMediaPicker.getDraftMessageDataModel()); - if (OsUtil.hasStoragePermission()) { - startMediaPickerDataLoader(); - } - - mMissingPermissionView = view.findViewById(R.id.missing_permission_view); - updateForPermissionState(OsUtil.hasStoragePermission()); - return view; - } - - @Override - int getActionBarTitleResId() { - return R.string.mediapicker_gallery_title; - } - - @Override - public void onDocumentPickerItemClicked() { - mMediaPicker.launchDocumentPicker(); - } - - @Override - void updateActionBar(final ActionBar actionBar) { - super.updateActionBar(actionBar); - if (mGalleryGridView == null) { - return; - } - final int selectionCount = mGalleryGridView.getSelectionCount(); - if (selectionCount > 0 && mGalleryGridView.isMultiSelectEnabled()) { - actionBar.setTitle(getContext().getResources().getString( - R.string.mediapicker_gallery_title_selection, - selectionCount)); - } - } - - @Override - public void onMediaPickerDataUpdated(final MediaPickerData mediaPickerData, final Object data, - final int loaderId) { - mBindingRef.ensureBound(mediaPickerData); - Assert.equals(MediaPickerData.GALLERY_IMAGE_LOADER, loaderId); - Cursor rawCursor = null; - if (data instanceof Cursor) { - rawCursor = (Cursor) data; - } - // Before delivering the cursor, wrap around the local gallery cursor - // with an extra item for document picker integration in the front. - final MatrixCursor specialItemsCursor = - new MatrixCursor(GalleryGridItemData.SPECIAL_ITEM_COLUMNS); - specialItemsCursor.addRow(new Object[] { GalleryGridItemData.ID_DOCUMENT_PICKER_ITEM }); - final MergeCursor cursor = - new MergeCursor(new Cursor[] { specialItemsCursor, rawCursor }); - mAdapter.swapCursor(cursor); - } - - @Override - public void onResume() { - if (OsUtil.hasStoragePermission()) { - // Work around a bug in MediaStore where cursors querying the Files provider don't get - // updated for changes to Images.Media or Video.Media. - startMediaPickerDataLoader(); - } - } - - @Override - protected void setSelected(final boolean selected) { - super.setSelected(selected); - if (selected && !OsUtil.hasStoragePermission()) { - mMediaPicker.requestPermissions( - new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, - MediaPicker.GALLERY_PERMISSION_REQUEST_CODE); - } - } - - private void startMediaPickerDataLoader() { - mBindingRef.getData().startLoader(MediaPickerData.GALLERY_IMAGE_LOADER, mBindingRef, null, - this); - } - - @Override - protected void onRequestPermissionsResult( - final int requestCode, final String permissions[], final int[] grantResults) { - if (requestCode == MediaPicker.GALLERY_PERMISSION_REQUEST_CODE) { - final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED; - if (permissionGranted) { - startMediaPickerDataLoader(); - } - updateForPermissionState(permissionGranted); - } - } - - private void updateForPermissionState(final boolean granted) { - // onRequestPermissionsResult can sometimes get called before createView(). - if (mGalleryGridView == null) { - return; - } - - mGalleryGridView.setVisibility(granted ? View.VISIBLE : View.GONE); - mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/HardwareCameraPreview.java b/src/com/android/messaging/ui/mediapicker/HardwareCameraPreview.java deleted file mode 100644 index 45d9579..0000000 --- a/src/com/android/messaging/ui/mediapicker/HardwareCameraPreview.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.content.Context; -import android.graphics.SurfaceTexture; -import android.hardware.Camera; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.view.TextureView; -import android.view.View; - -import java.io.IOException; - -/** - * A hardware accelerated preview texture for the camera. This is the preferred CameraPreview - * because it animates smoother. When hardware acceleration isn't available, SoftwareCameraPreview - * is used. - * - * There is a significant amount of duplication between HardwareCameraPreview and - * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The - * implementations of the shared methods are delegated to CameraPreview - */ -public class HardwareCameraPreview extends TextureView implements CameraPreview.CameraPreviewHost { - private CameraPreview mPreview; - - public HardwareCameraPreview(final Context context, final AttributeSet attrs) { - super(context, attrs); - mPreview = new CameraPreview(this); - setSurfaceTextureListener(new SurfaceTextureListener() { - @Override - public void onSurfaceTextureAvailable(final SurfaceTexture surfaceTexture, final int i, final int i2) { - CameraManager.get().setSurface(mPreview); - } - - @Override - public void onSurfaceTextureSizeChanged(final SurfaceTexture surfaceTexture, final int i, final int i2) { - CameraManager.get().setSurface(mPreview); - } - - @Override - public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) { - CameraManager.get().setSurface(null); - return true; - } - - @Override - public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) { - CameraManager.get().setSurface(mPreview); - } - }); - } - - @Override - protected void onVisibilityChanged(final View changedView, final int visibility) { - super.onVisibilityChanged(changedView, visibility); - mPreview.onVisibilityChanged(visibility); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mPreview.onDetachedFromWindow(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mPreview.onAttachedToWindow(); - } - - @Override - protected void onRestoreInstanceState(final Parcelable state) { - super.onRestoreInstanceState(state); - mPreview.onRestoreInstanceState(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec); - heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - public View getView() { - return this; - } - - @Override - public boolean isValid() { - return getSurfaceTexture() != null; - } - - @Override - public void startPreview(final Camera camera) throws IOException { - camera.setPreviewTexture(getSurfaceTexture()); - } - - @Override - public void onCameraPermissionGranted() { - mPreview.onCameraPermissionGranted(); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/ImagePersistTask.java b/src/com/android/messaging/ui/mediapicker/ImagePersistTask.java deleted file mode 100644 index 637eb84..0000000 --- a/src/com/android/messaging/ui/mediapicker/ImagePersistTask.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.net.Uri; - -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.SafeAsyncTask; -import com.android.messaging.util.exif.ExifInterface; -import com.android.messaging.util.exif.ExifTag; - -import java.io.IOException; -import java.io.OutputStream; - -public class ImagePersistTask extends SafeAsyncTask<Void, Void, Void> { - private static final String JPEG_EXTENSION = "jpg"; - private static final String TAG = LogUtil.BUGLE_TAG; - - private int mWidth; - private int mHeight; - private final float mHeightPercent; - private final byte[] mBytes; - private final Context mContext; - private final CameraManager.MediaCallback mCallback; - private Uri mOutputUri; - private Exception mException; - - public ImagePersistTask( - final int width, - final int height, - final float heightPercent, - final byte[] bytes, - final Context context, - final CameraManager.MediaCallback callback) { - Assert.isTrue(heightPercent >= 0 && heightPercent <= 1); - Assert.notNull(bytes); - Assert.notNull(context); - Assert.notNull(callback); - mWidth = width; - mHeight = height; - mHeightPercent = heightPercent; - mBytes = bytes; - mContext = context; - mCallback = callback; - // TODO: We probably want to store directly in MMS storage to prevent this - // intermediate step - mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(JPEG_EXTENSION); - } - - @Override - protected Void doInBackgroundTimed(final Void... params) { - OutputStream outputStream = null; - Bitmap bitmap = null; - Bitmap clippedBitmap = null; - try { - outputStream = - mContext.getContentResolver().openOutputStream(mOutputUri); - if (mHeightPercent != 1.0f) { - int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED; - final ExifInterface exifInterface = new ExifInterface(); - try { - exifInterface.readExif(mBytes); - final Integer orientationValue = - exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION); - if (orientationValue != null) { - orientation = orientationValue.intValue(); - } - // The thumbnail is of the full image, but we're cropping it, so just clear - // the thumbnail - exifInterface.setCompressedThumbnail((byte[]) null); - } catch (IOException e) { - // Couldn't get exif tags, not the end of the world - } - bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length); - final int clippedWidth; - final int clippedHeight; - if (ExifInterface.getOrientationParams(orientation).invertDimensions) { - Assert.equals(mWidth, bitmap.getHeight()); - Assert.equals(mHeight, bitmap.getWidth()); - clippedWidth = (int) (mHeight * mHeightPercent); - clippedHeight = mWidth; - } else { - Assert.equals(mWidth, bitmap.getWidth()); - Assert.equals(mHeight, bitmap.getHeight()); - clippedWidth = mWidth; - clippedHeight = (int) (mHeight * mHeightPercent); - } - final int offsetTop = (bitmap.getHeight() - clippedHeight) / 2; - final int offsetLeft = (bitmap.getWidth() - clippedWidth) / 2; - mWidth = clippedWidth; - mHeight = clippedHeight; - clippedBitmap = Bitmap.createBitmap(clippedWidth, clippedHeight, - Bitmap.Config.ARGB_8888); - clippedBitmap.setDensity(bitmap.getDensity()); - final Canvas clippedBitmapCanvas = new Canvas(clippedBitmap); - final Matrix matrix = new Matrix(); - matrix.postTranslate(-offsetLeft, -offsetTop); - clippedBitmapCanvas.drawBitmap(bitmap, matrix, null /* paint */); - clippedBitmapCanvas.save(); - // EXIF data can take a big chunk of the file size and is often cleared by the - // carrier, only store orientation since that's critical - ExifTag orientationTag = exifInterface.getTag(ExifInterface.TAG_ORIENTATION); - exifInterface.clearExif(); - exifInterface.setTag(orientationTag); - exifInterface.writeExif(clippedBitmap, outputStream); - } else { - outputStream.write(mBytes); - } - } catch (final IOException e) { - mOutputUri = null; - mException = e; - LogUtil.e(TAG, "Unable to persist image to temp storage " + e); - } finally { - if (bitmap != null) { - bitmap.recycle(); - } - - if (clippedBitmap != null) { - clippedBitmap.recycle(); - } - - if (outputStream != null) { - try { - outputStream.flush(); - } catch (final IOException e) { - mOutputUri = null; - mException = e; - LogUtil.e(TAG, "error trying to flush and close the outputStream" + e); - } finally { - try { - outputStream.close(); - } catch (final IOException e) { - // Do nothing. - } - } - } - } - return null; - } - - @Override - protected void onPostExecute(final Void aVoid) { - if (mOutputUri != null) { - mCallback.onMediaReady(mOutputUri, ContentType.IMAGE_JPEG, mWidth, mHeight); - } else { - Assert.notNull(mException); - mCallback.onMediaFailed(mException); - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java b/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java deleted file mode 100644 index 06730a3..0000000 --- a/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * 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.media.MediaRecorder; -import android.net.Uri; -import android.os.ParcelFileDescriptor; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.SafeAsyncTask; -import com.android.messaging.util.UiUtils; - -import java.io.IOException; - -/** - * Wraps around the functionalities of MediaRecorder, performs routine setup for audio recording - * and updates the audio level to be displayed in UI. - * - * During the start and end of a recording session, we kick off a thread that polls for audio - * levels, and updates the thread-safe AudioLevelSource instance. Consumers may bind to the - * sound level by either polling from the level source, or register for a level change callback - * on the level source object. In Bugle, the UI element (SoundLevels) polls for the sound level - * on the UI thread by using animation ticks and invalidating itself. - * - * Aside from tracking sound levels, this also encapsulates the functionality to save the file - * to the scratch space. The saved file is returned by calling stopRecording(). - */ -public class LevelTrackingMediaRecorder { - // We refresh sound level every 100ms during a recording session. - private static final int REFRESH_INTERVAL_MILLIS = 100; - - // The native amplitude returned from MediaRecorder ranges from 0~32768 (unfortunately, this - // is not a constant that's defined anywhere, but the framework's Recorder app is using the - // same hard-coded number). Therefore, a constant is needed in order to make it 0~100. - private static final int MAX_AMPLITUDE_FACTOR = 32768 / 100; - - // We want to limit the max audio file size by the max message size allowed by MmsConfig, - // plus multiplied by this fudge ratio to guarantee that we don't go over limit. - private static final float MAX_SIZE_RATIO = 0.8f; - - // Default recorder settings for Bugle. - // TODO: Do we want these to be tweakable? - private static final int MEDIA_RECORDER_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC; - private static final int MEDIA_RECORDER_OUTPUT_FORMAT = MediaRecorder.OutputFormat.THREE_GPP; - private static final int MEDIA_RECORDER_AUDIO_ENCODER = MediaRecorder.AudioEncoder.AMR_NB; - - private final AudioLevelSource mLevelSource; - private Thread mRefreshLevelThread; - private MediaRecorder mRecorder; - private Uri mOutputUri; - private ParcelFileDescriptor mOutputFD; - - public LevelTrackingMediaRecorder() { - mLevelSource = new AudioLevelSource(); - } - - public AudioLevelSource getLevelSource() { - return mLevelSource; - } - - /** - * @return if we are currently in a recording session. - */ - public boolean isRecording() { - return mRecorder != null; - } - - /** - * Start a new recording session. - * @return true if a session is successfully started; false if something went wrong or if - * we are already recording. - */ - public boolean startRecording(final MediaRecorder.OnErrorListener errorListener, - final MediaRecorder.OnInfoListener infoListener, int maxSize) { - synchronized (LevelTrackingMediaRecorder.class) { - if (mRecorder == null) { - mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri( - ContentType.THREE_GPP_EXTENSION); - mRecorder = new MediaRecorder(); - try { - // The scratch space file is a Uri, however MediaRecorder - // API only accepts absolute FD's. Therefore, get the - // FileDescriptor from the content resolver to ensure the - // directory is created and get the file path to output the - // audio to. - maxSize *= MAX_SIZE_RATIO; - mOutputFD = Factory.get().getApplicationContext() - .getContentResolver().openFileDescriptor(mOutputUri, "w"); - mRecorder.setAudioSource(MEDIA_RECORDER_AUDIO_SOURCE); - mRecorder.setOutputFormat(MEDIA_RECORDER_OUTPUT_FORMAT); - mRecorder.setAudioEncoder(MEDIA_RECORDER_AUDIO_ENCODER); - mRecorder.setOutputFile(mOutputFD.getFileDescriptor()); - mRecorder.setMaxFileSize(maxSize); - mRecorder.setOnErrorListener(errorListener); - mRecorder.setOnInfoListener(infoListener); - mRecorder.prepare(); - mRecorder.start(); - startTrackingSoundLevel(); - return true; - } catch (final Exception e) { - // There may be a device failure or I/O failure, record the error but - // don't fail. - LogUtil.e(LogUtil.BUGLE_TAG, "Something went wrong when starting " + - "media recorder. " + e); - UiUtils.showToastAtBottom(R.string.audio_recording_start_failed); - stopRecording(); - } - } else { - Assert.fail("Trying to start a new recording session while already recording!"); - } - return false; - } - } - - /** - * Stop the current recording session. - * @return the Uri of the output file, or null if not currently recording. - */ - public Uri stopRecording() { - synchronized (LevelTrackingMediaRecorder.class) { - if (mRecorder != null) { - try { - mRecorder.stop(); - } catch (final RuntimeException ex) { - // This may happen when the recording is too short, so just drop the recording - // in this case. - LogUtil.w(LogUtil.BUGLE_TAG, "Something went wrong when stopping " + - "media recorder. " + ex); - if (mOutputUri != null) { - final Uri outputUri = mOutputUri; - SafeAsyncTask.executeOnThreadPool(new Runnable() { - @Override - public void run() { - Factory.get().getApplicationContext().getContentResolver().delete( - outputUri, null, null); - } - }); - mOutputUri = null; - } - } finally { - mRecorder.release(); - mRecorder = null; - } - } else { - Assert.fail("Not currently recording!"); - return null; - } - } - - if (mOutputFD != null) { - try { - mOutputFD.close(); - } catch (final IOException e) { - // Nothing to do - } - mOutputFD = null; - } - - stopTrackingSoundLevel(); - return mOutputUri; - } - - private int getAmplitude() { - synchronized (LevelTrackingMediaRecorder.class) { - if (mRecorder != null) { - final int maxAmplitude = mRecorder.getMaxAmplitude() / MAX_AMPLITUDE_FACTOR; - return Math.min(maxAmplitude, 100); - } else { - return 0; - } - } - } - - private void startTrackingSoundLevel() { - stopTrackingSoundLevel(); - mRefreshLevelThread = new Thread() { - @Override - public void run() { - try { - while (true) { - synchronized (LevelTrackingMediaRecorder.class) { - if (mRecorder != null) { - mLevelSource.setSpeechLevel(getAmplitude()); - } else { - // The recording session is over, finish the thread. - return; - } - } - Thread.sleep(REFRESH_INTERVAL_MILLIS); - } - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - }; - mRefreshLevelThread.start(); - } - - private void stopTrackingSoundLevel() { - if (mRefreshLevelThread != null && mRefreshLevelThread.isAlive()) { - mRefreshLevelThread.interrupt(); - mRefreshLevelThread = null; - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/MediaChooser.java b/src/com/android/messaging/ui/mediapicker/MediaChooser.java deleted file mode 100644 index 9ac0d1b..0000000 --- a/src/com/android/messaging/ui/mediapicker/MediaChooser.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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.FragmentManager; -import android.content.Context; -import android.support.v7.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 com.android.messaging.R; -import com.android.messaging.datamodel.binding.ImmutableBindingRef; -import com.android.messaging.datamodel.data.MediaPickerData; -import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; -import com.android.messaging.ui.BasePagerViewHolder; -import com.android.messaging.util.Assert; -import com.android.messaging.util.OsUtil; - -abstract class MediaChooser extends BasePagerViewHolder - implements DraftMessageSubscriptionDataProvider { - /** The media picker that the chooser is hosted in */ - protected final MediaPicker mMediaPicker; - - /** Referencing the main media picker binding to perform data loading */ - protected final ImmutableBindingRef<MediaPickerData> mBindingRef; - - /** True if this is the selected chooser */ - protected boolean mSelected; - - /** True if this chooser is open */ - protected boolean mOpen; - - /** The button to show in the tab strip */ - private ImageButton mTabButton; - - /** Used by subclasses to indicate that no loader is required from the data model in order for - * this chooser to function. - */ - public static final int NO_LOADER_REQUIRED = -1; - - /** - * Initializes a new instance of the Chooser class - * @param mediaPicker The media picker that the chooser is hosted in - */ - MediaChooser(final MediaPicker mediaPicker) { - Assert.notNull(mediaPicker); - mMediaPicker = mediaPicker; - mBindingRef = mediaPicker.getMediaPickerDataBinding(); - mSelected = false; - } - - protected void setSelected(final boolean selected) { - mSelected = selected; - if (selected) { - // If we're selected, it must be open - mOpen = true; - } - if (mTabButton != null) { - mTabButton.setSelected(selected); - mTabButton.setAlpha(selected ? 1 : 0.5f); - } - } - - ImageButton getTabButton() { - return mTabButton; - } - - void onCreateTabButton(final LayoutInflater inflater, final ViewGroup parent) { - mTabButton = (ImageButton) inflater.inflate( - R.layout.mediapicker_tab_button, - parent, - false /* addToParent */); - mTabButton.setImageResource(getIconResource()); - mTabButton.setContentDescription( - inflater.getContext().getResources().getString(getIconDescriptionResource())); - setSelected(mSelected); - mTabButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - mMediaPicker.selectChooser(MediaChooser.this); - } - }); - } - - protected Context getContext() { - return mMediaPicker.getActivity(); - } - - protected FragmentManager getFragmentManager() { - return OsUtil.isAtLeastJB_MR1() ? mMediaPicker.getChildFragmentManager() : - mMediaPicker.getFragmentManager(); - } - protected LayoutInflater getLayoutInflater() { - return LayoutInflater.from(getContext()); - } - - /** Allows the chooser to handle full screen change */ - void onFullScreenChanged(final boolean fullScreen) {} - - /** Allows the chooser to handle the chooser being opened or closed */ - void onOpenedChanged(final boolean open) { - mOpen = open; - } - - /** @return The bit field of media types that this chooser can pick */ - public abstract int getSupportedMediaTypes(); - - /** @return The resource id of the icon for the chooser */ - abstract int getIconResource(); - - /** @return The resource id of the string to use for the accessibility text of the icon */ - abstract int getIconDescriptionResource(); - - /** - * Sets up the action bar to show the current state of the full-screen chooser - * @param actionBar The action bar to populate - */ - void updateActionBar(final ActionBar actionBar) { - final int actionBarTitleResId = getActionBarTitleResId(); - if (actionBarTitleResId == 0) { - actionBar.hide(); - } else { - actionBar.setCustomView(null); - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.show(); - // Use X instead of <- in the action bar - actionBar.setHomeAsUpIndicator(R.drawable.ic_remove_small_light); - actionBar.setTitle(actionBarTitleResId); - } - } - - /** - * Returns the resource Id used for the action bar title. - */ - abstract int getActionBarTitleResId(); - - /** - * Throws an exception if the media chooser object doesn't require data support. - */ - public void onDataUpdated(final Object data, final int loaderId) { - throw new IllegalStateException(); - } - - /** - * Called by the MediaPicker to determine whether this panel can be swiped down further. If - * not, then a swipe down gestured will be captured by the MediaPickerPanel to shrink the - * entire panel. - */ - public boolean canSwipeDown() { - return false; - } - - /** - * Typically the media picker is closed when the IME is opened, but this allows the chooser to - * specify that showing the IME is okay while the chooser is up - */ - public boolean canShowIme() { - return false; - } - - public boolean onBackPressed() { - return false; - } - - public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) { - } - - public boolean onOptionsItemSelected(final MenuItem item) { - return false; - } - - public void setThemeColor(final int color) { - } - - /** - * Returns true if the chooser is owning any incoming touch events, so that the media picker - * panel won't process it and slide the panel. - */ - public boolean isHandlingTouch() { - return false; - } - - public void stopTouchHandling() { - } - - @Override - public int getConversationSelfSubId() { - return mMediaPicker.getConversationSelfSubId(); - } - - /** Optional activity life-cycle methods to be overridden by subclasses */ - public void onPause() { } - public void onResume() { } - protected void onRequestPermissionsResult( - final int requestCode, final String permissions[], final int[] grantResults) { } -} diff --git a/src/com/android/messaging/ui/mediapicker/MediaPicker.java b/src/com/android/messaging/ui/mediapicker/MediaPicker.java deleted file mode 100644 index f441d09..0000000 --- a/src/com/android/messaging/ui/mediapicker/MediaPicker.java +++ /dev/null @@ -1,736 +0,0 @@ -/* - * 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 android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.support.v7.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); - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/MediaPickerGridView.java b/src/com/android/messaging/ui/mediapicker/MediaPickerGridView.java deleted file mode 100644 index cc3a4a1..0000000 --- a/src/com/android/messaging/ui/mediapicker/MediaPickerGridView.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.content.Context; -import android.util.AttributeSet; -import android.widget.GridView; - -public class MediaPickerGridView extends GridView { - - public MediaPickerGridView(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - /** - * Returns if the grid view can be swiped down further. It cannot be swiped down - * if there's no item or if we are already at the top. - */ - public boolean canSwipeDown() { - if (getAdapter() == null || getAdapter().getCount() == 0 || getChildCount() == 0) { - return false; - } - - final int firstVisiblePosition = getFirstVisiblePosition(); - if (firstVisiblePosition == 0 && getChildAt(0).getTop() >= 0) { - return false; - } - return true; - } -} diff --git a/src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java b/src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java deleted file mode 100644 index 56b0a03..0000000 --- a/src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * 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.content.Context; -import android.content.res.Resources; -import android.os.Handler; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.Transformation; -import android.widget.LinearLayout; - -import com.android.messaging.R; -import com.android.messaging.ui.PagingAwareViewPager; -import com.android.messaging.util.Assert; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.UiUtils; - -/** - * Custom layout panel which makes the MediaPicker animations seamless and synchronized - * Designed to be very specific to the MediaPicker's usage - */ -public class MediaPickerPanel extends ViewGroup { - /** - * The window of time in which we might to decide to reinterpret the intent of a gesture. - */ - private static final long TOUCH_RECAPTURE_WINDOW_MS = 500L; - - // The two view components to layout - private LinearLayout mTabStrip; - private boolean mFullScreenOnly; - private PagingAwareViewPager mViewPager; - - /** - * True if the MediaPicker is full screen or animating into it - */ - private boolean mFullScreen; - - /** - * True if the MediaPicker is open at all - */ - private boolean mExpanded; - - /** - * The current desired height of the MediaPicker. This property may be animated and the - * measure pass uses it to determine what size the components are. - */ - private int mCurrentDesiredHeight; - - private final Handler mHandler = new Handler(); - - /** - * The media picker for dispatching events to the MediaPicker's listener - */ - private MediaPicker mMediaPicker; - - /** - * The computed default "half-screen" height of the view pager in px - */ - private final int mDefaultViewPagerHeight; - - /** - * The action bar height used to compute the padding on the view pager when it's full screen. - */ - private final int mActionBarHeight; - - private TouchHandler mTouchHandler; - - static final int PAGE_NOT_SET = -1; - - public MediaPickerPanel(final Context context, final AttributeSet attrs) { - super(context, attrs); - // Cache the computed dimension - mDefaultViewPagerHeight = getResources().getDimensionPixelSize( - R.dimen.mediapicker_default_chooser_height); - mActionBarHeight = getResources().getDimensionPixelSize(R.dimen.action_bar_height); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTabStrip = (LinearLayout) findViewById(R.id.mediapicker_tabstrip); - mViewPager = (PagingAwareViewPager) findViewById(R.id.mediapicker_view_pager); - mTouchHandler = new TouchHandler(); - setOnTouchListener(mTouchHandler); - mViewPager.setOnTouchListener(mTouchHandler); - - // Make sure full screen mode is updated in landscape mode change when the panel is open. - addOnLayoutChangeListener(new OnLayoutChangeListener() { - private boolean mLandscapeMode = UiUtils.isLandscapeMode(); - - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - final boolean newLandscapeMode = UiUtils.isLandscapeMode(); - if (mLandscapeMode != newLandscapeMode) { - mLandscapeMode = newLandscapeMode; - if (mExpanded) { - setExpanded(mExpanded, false /* animate */, mViewPager.getCurrentItem(), - true /* force */); - } - } - } - }); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - int requestedHeight = MeasureSpec.getSize(heightMeasureSpec); - if (mMediaPicker.getChooserShowsActionBarInFullScreen()) { - requestedHeight -= mActionBarHeight; - } - int desiredHeight = Math.min(mCurrentDesiredHeight, requestedHeight); - if (mExpanded && desiredHeight == 0) { - // If we want to be shown, we have to have a non-0 height. Returning a height of 0 will - // cause the framework to abort the animation from 0, so we must always have some - // height once we start expanding - desiredHeight = 1; - } else if (!mExpanded && desiredHeight == 0) { - mViewPager.setVisibility(View.GONE); - mViewPager.setAdapter(null); - } - - measureChild(mTabStrip, widthMeasureSpec, heightMeasureSpec); - - int tabStripHeight; - if (requiresFullScreen()) { - // Ensure that the tab strip is always visible, even in full screen. - tabStripHeight = mTabStrip.getMeasuredHeight(); - } else { - // Slide out the tab strip at the end of the animation to full screen. - tabStripHeight = Math.min(mTabStrip.getMeasuredHeight(), - requestedHeight - desiredHeight); - } - - // If we are animating and have an interim desired height, use the default height. We can't - // take the max here as on some devices the mDefaultViewPagerHeight may be too big in - // landscape mode after animation. - final int tabAdjustedDesiredHeight = desiredHeight - tabStripHeight; - final int viewPagerHeight = - tabAdjustedDesiredHeight <= 1 ? mDefaultViewPagerHeight : tabAdjustedDesiredHeight; - - int viewPagerHeightMeasureSpec = MeasureSpec.makeMeasureSpec( - viewPagerHeight, MeasureSpec.EXACTLY); - measureChild(mViewPager, widthMeasureSpec, viewPagerHeightMeasureSpec); - setMeasuredDimension(mViewPager.getMeasuredWidth(), desiredHeight); - } - - @Override - protected void onLayout(final boolean changed, final int left, final int top, final int right, - final int bottom) { - int y = top; - final int width = right - left; - - final int viewPagerHeight = mViewPager.getMeasuredHeight(); - mViewPager.layout(0, y, width, y + viewPagerHeight); - y += viewPagerHeight; - - mTabStrip.layout(0, y, width, y + mTabStrip.getMeasuredHeight()); - } - - void onChooserChanged() { - if (mFullScreen) { - setDesiredHeight(getDesiredHeight(), true); - } - } - - void setFullScreenOnly(boolean fullScreenOnly) { - mFullScreenOnly = fullScreenOnly; - } - - boolean isFullScreen() { - return mFullScreen; - } - - void setMediaPicker(final MediaPicker mediaPicker) { - mMediaPicker = mediaPicker; - } - - /** - * Get the desired height of the media picker panel for when the panel is not in motion (i.e. - * not being dragged by the user). - */ - private int getDesiredHeight() { - if (mFullScreen) { - int fullHeight = getContext().getResources().getDisplayMetrics().heightPixels; - if (OsUtil.isAtLeastKLP() && isAttachedToWindow()) { - // When we're attached to the window, we can get an accurate height, not necessary - // on older API level devices because they don't include the action bar height - View composeContainer = - getRootView().findViewById(R.id.conversation_and_compose_container); - if (composeContainer != null) { - // protect against composeContainer having been unloaded already - fullHeight -= UiUtils.getMeasuredBoundsOnScreen(composeContainer).top; - } - } - if (mMediaPicker.getChooserShowsActionBarInFullScreen()) { - return fullHeight - mActionBarHeight; - } else { - return fullHeight; - } - } else if (mExpanded) { - return LayoutParams.WRAP_CONTENT; - } else { - return 0; - } - } - - private void setupViewPager(final int startingPage) { - mViewPager.setVisibility(View.VISIBLE); - if (startingPage >= 0 && startingPage < mMediaPicker.getPagerAdapter().getCount()) { - mViewPager.setAdapter(mMediaPicker.getPagerAdapter()); - mViewPager.setCurrentItem(startingPage); - } - updateViewPager(); - } - - /** - * Expand the media picker panel. Since we always set the pager adapter to null when the panel - * is collapsed, we need to restore the adapter and the starting page. - * @param expanded expanded or collapsed - * @param animate need animation - * @param startingPage the desired selected page to start - */ - void setExpanded(final boolean expanded, final boolean animate, final int startingPage) { - setExpanded(expanded, animate, startingPage, false /* force */); - } - - private void setExpanded(final boolean expanded, final boolean animate, final int startingPage, - final boolean force) { - if (expanded == mExpanded && !force) { - return; - } - mFullScreen = false; - mExpanded = expanded; - mHandler.post(new Runnable() { - @Override - public void run() { - setDesiredHeight(getDesiredHeight(), animate); - } - }); - if (expanded) { - setupViewPager(startingPage); - mMediaPicker.dispatchOpened(); - } else { - mMediaPicker.dispatchDismissed(); - } - - // Call setFullScreenView() when we are in landscape mode so it can go full screen as - // soon as it is expanded. - if (expanded && requiresFullScreen()) { - setFullScreenView(true, animate); - } - } - - private boolean requiresFullScreen() { - return mFullScreenOnly || UiUtils.isLandscapeMode(); - } - - private void setDesiredHeight(int height, final boolean animate) { - final int startHeight = mCurrentDesiredHeight; - if (height == LayoutParams.WRAP_CONTENT) { - height = measureHeight(); - } - clearAnimation(); - if (animate) { - final int deltaHeight = height - startHeight; - final Animation animation = new Animation() { - @Override - protected void applyTransformation(final float interpolatedTime, - final Transformation t) { - mCurrentDesiredHeight = (int) (startHeight + deltaHeight * interpolatedTime); - requestLayout(); - } - - @Override - public boolean willChangeBounds() { - return true; - } - }; - animation.setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION); - animation.setInterpolator(UiUtils.EASE_OUT_INTERPOLATOR); - startAnimation(animation); - } else { - mCurrentDesiredHeight = height; - } - requestLayout(); - } - - /** - * @return The minimum total height of the view - */ - private int measureHeight() { - final int measureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST); - measureChild(mTabStrip, measureSpec, measureSpec); - return mDefaultViewPagerHeight + mTabStrip.getMeasuredHeight(); - } - - /** - * Enters or leaves full screen view - * - * @param fullScreen True to enter full screen view, false to leave - * @param animate True to animate the transition - */ - void setFullScreenView(final boolean fullScreen, final boolean animate) { - if (fullScreen == mFullScreen) { - return; - } - - if (requiresFullScreen() && !fullScreen) { - setExpanded(false /* expanded */, true /* animate */, PAGE_NOT_SET); - return; - } - mFullScreen = fullScreen; - setDesiredHeight(getDesiredHeight(), animate); - mMediaPicker.dispatchFullScreen(mFullScreen); - updateViewPager(); - } - - /** - * ViewPager should have its paging disabled when in full screen mode. - */ - private void updateViewPager() { - mViewPager.setPagingEnabled(!mFullScreen); - } - - @Override - public boolean onInterceptTouchEvent(final MotionEvent ev) { - return mTouchHandler.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); - } - - /** - * Helper class to handle touch events and swipe gestures - */ - private class TouchHandler implements OnTouchListener { - /** - * The height of the view when the touch press started - */ - private int mDownHeight = -1; - - /** - * True if the panel moved at all (changed height) during the drag - */ - private boolean mMoved = false; - - // The threshold constants converted from DP to px - private final float mFlingThresholdPx; - private final float mBigFlingThresholdPx; - - // The system defined pixel size to determine when a movement is considered a drag. - private final int mTouchSlop; - - /** - * A copy of the MotionEvent that started the drag/swipe gesture - */ - private MotionEvent mDownEvent; - - /** - * Whether we are currently moving down. We may not be able to move down in full screen - * mode when the child view can swipe down (such as a list view). - */ - private boolean mMovedDown = false; - - /** - * Indicates whether the child view contained in the panel can swipe down at the beginning - * of the drag event (i.e. the initial down). The MediaPanel can contain - * scrollable children such as a list view / grid view. If the child view can swipe down, - * We want to let the child view handle the scroll first instead of handling it ourselves. - */ - private boolean mCanChildViewSwipeDown = false; - - /** - * Necessary direction ratio for a fling to be considered in one direction this prevents - * horizontal swipes with small vertical components from triggering vertical swipe actions - */ - private static final float DIRECTION_RATIO = 1.1f; - - TouchHandler() { - final Resources resources = getContext().getResources(); - final ViewConfiguration configuration = ViewConfiguration.get(getContext()); - mFlingThresholdPx = resources.getDimensionPixelSize( - R.dimen.mediapicker_fling_threshold); - mBigFlingThresholdPx = resources.getDimensionPixelSize( - R.dimen.mediapicker_big_fling_threshold); - mTouchSlop = configuration.getScaledTouchSlop(); - } - - /** - * The media picker panel may contain scrollable children such as a GridView, which eats - * all touch events before we get to it. Therefore, we'd like to intercept these events - * before the children to determine if we should handle swiping down in full screen mode. - * In non-full screen mode, we should handle all vertical scrolling events and leave - * horizontal scrolling to the view pager. - */ - public boolean onInterceptTouchEvent(final MotionEvent ev) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - // Never capture the initial down, so that the children may handle it - // as well. Let the touch handler know about the down event as well. - mTouchHandler.onTouch(MediaPickerPanel.this, ev); - - // Ask the MediaPicker whether the contained view can be swiped down. - // We record the value at the start of the drag to decide the swiping mode - // for the entire motion. - mCanChildViewSwipeDown = mMediaPicker.canSwipeDownChooser(); - return false; - - case MotionEvent.ACTION_MOVE: { - if (mMediaPicker.isChooserHandlingTouch()) { - if (shouldAllowRecaptureTouch(ev)) { - mMediaPicker.stopChooserTouchHandling(); - mViewPager.setPagingEnabled(true); - return false; - } - // If the chooser is claiming ownership on all touch events, then we - // shouldn't try to handle them (neither should the view pager). - mViewPager.setPagingEnabled(false); - return false; - } else if (mCanChildViewSwipeDown) { - // Never capture event if the child view can swipe down. - return false; - } else if (!mFullScreen && mMoved) { - // When we are not fullscreen, we own any vertical drag motion. - return true; - } else if (mMovedDown) { - // We are currently handling the down swipe ourselves, so always - // capture this event. - return true; - } else { - // The current interaction mode is undetermined, so always let the - // touch handler know about this event. However, don't capture this - // event so the child may handle it as well. - mTouchHandler.onTouch(MediaPickerPanel.this, ev); - - // Capture the touch event from now on if we are handling the drag. - return mFullScreen ? mMovedDown : mMoved; - } - } - } - return false; - } - - /** - * Determine whether we think the user is actually trying to expand or slide despite the - * fact that they touched first on a chooser that captured the input. - */ - private boolean shouldAllowRecaptureTouch(MotionEvent ev) { - final long elapsedMs = ev.getEventTime() - ev.getDownTime(); - if (mDownEvent == null || elapsedMs == 0 || elapsedMs > TOUCH_RECAPTURE_WINDOW_MS) { - // Either we don't have info to decide or it's been long enough that we no longer - // want to reinterpret user intent. - return false; - } - final float dx = ev.getRawX() - mDownEvent.getRawX(); - final float dy = ev.getRawY() - mDownEvent.getRawY(); - final float dt = elapsedMs / 1000.0f; - final float maxAbsDelta = Math.max(Math.abs(dx), Math.abs(dy)); - final float velocity = maxAbsDelta / dt; - return velocity > mFlingThresholdPx; - } - - @Override - public boolean onTouch(final View view, final MotionEvent motionEvent) { - switch (motionEvent.getAction()) { - case MotionEvent.ACTION_UP: { - if (!mMoved || mDownEvent == null) { - return false; - } - final float dx = motionEvent.getRawX() - mDownEvent.getRawX(); - final float dy = motionEvent.getRawY() - mDownEvent.getRawY(); - - final float dt = - (motionEvent.getEventTime() - mDownEvent.getEventTime()) / 1000.0f; - final float yVelocity = dy / dt; - - boolean handled = false; - - // Vertical swipe occurred if the direction is as least mostly in the y - // component and has the required velocity (px/sec) - if ((dx == 0 || (Math.abs(dy) / Math.abs(dx)) > DIRECTION_RATIO) && - Math.abs(yVelocity) > mFlingThresholdPx) { - if (yVelocity < 0 && mExpanded) { - setFullScreenView(true, true); - handled = true; - } else if (yVelocity > 0) { - if (mFullScreen && yVelocity < mBigFlingThresholdPx) { - setFullScreenView(false, true); - } else { - setExpanded(false, true, PAGE_NOT_SET); - } - handled = true; - } - } - - if (!handled) { - // If they didn't swipe enough, animate back to resting state - setDesiredHeight(getDesiredHeight(), true); - } - resetState(); - break; - } - case MotionEvent.ACTION_DOWN: { - mDownHeight = getHeight(); - mDownEvent = MotionEvent.obtain(motionEvent); - // If we are here and care about the return value (i.e. this is not called - // from onInterceptTouchEvent), then presumably no children view in the panel - // handles the down event. We'd like to handle future ACTION_MOVE events, so - // always claim ownership on this event so it doesn't fall through and gets - // cancelled by the framework. - return true; - } - case MotionEvent.ACTION_MOVE: { - if (mDownEvent == null) { - return mMoved; - } - - final float dx = mDownEvent.getRawX() - motionEvent.getRawX(); - final float dy = mDownEvent.getRawY() - motionEvent.getRawY(); - // Don't act if the move is mostly horizontal - if (Math.abs(dy) > mTouchSlop && - (Math.abs(dy) / Math.abs(dx)) > DIRECTION_RATIO) { - setDesiredHeight((int) (mDownHeight + dy), false); - mMoved = true; - if (dy < -mTouchSlop) { - mMovedDown = true; - } - } - return mMoved; - } - - } - return mMoved; - } - - private void resetState() { - mDownEvent = null; - mDownHeight = -1; - mMoved = false; - mMovedDown = false; - mCanChildViewSwipeDown = false; - updateViewPager(); - } - } -} - diff --git a/src/com/android/messaging/ui/mediapicker/MmsVideoRecorder.java b/src/com/android/messaging/ui/mediapicker/MmsVideoRecorder.java deleted file mode 100644 index 7ac7871..0000000 --- a/src/com/android/messaging/ui/mediapicker/MmsVideoRecorder.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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.hardware.Camera; -import android.media.CamcorderProfile; -import android.media.MediaRecorder; -import android.net.Uri; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.SafeAsyncTask; - -import java.io.FileNotFoundException; - -class MmsVideoRecorder extends MediaRecorder { - private static final float VIDEO_OVERSHOOT_SLOP = .85F; - - private static final int BITS_PER_BYTE = 8; - - // We think user will expect to be able to record videos at least this long - private static final long MIN_DURATION_LIMIT_SECONDS = 25; - - /** The uri where video is being recorded to */ - private Uri mTempVideoUri; - - /** The settings used for video recording */ - private final CamcorderProfile mCamcorderProfile; - - public MmsVideoRecorder(final Camera camera, final int cameraIndex, final int orientation, - final int maxMessageSize) - throws FileNotFoundException { - mCamcorderProfile = - CamcorderProfile.get(cameraIndex, CamcorderProfile.QUALITY_LOW); - mTempVideoUri = MediaScratchFileProvider.buildMediaScratchSpaceUri( - ContentType.getExtension(getContentType())); - - // The video recorder can sometimes return a file that's larger than the max we - // say we can handle. Try to handle that overshoot by specifying an 85% limit. - final long sizeLimit = (long) (maxMessageSize * VIDEO_OVERSHOOT_SLOP); - - // The QUALITY_LOW profile might not be low enough to allow for video of a reasonable - // minimum duration. Adjust a/v bitrates to allow at least MIN_DURATION_LIMIT video - // to be recorded. - int audioBitRate = mCamcorderProfile.audioBitRate; - int videoBitRate = mCamcorderProfile.videoBitRate; - final double initialDurationLimit = sizeLimit * BITS_PER_BYTE - / (double) (audioBitRate + videoBitRate); - if (initialDurationLimit < MIN_DURATION_LIMIT_SECONDS) { - // Reduce the suggested bitrates. These bitrates are only requests, if implementation - // can't actually hit these goals it will still record video at higher rate and stop when - // it hits the size limit. - final double bitRateAdjustmentFactor = initialDurationLimit / MIN_DURATION_LIMIT_SECONDS; - audioBitRate *= bitRateAdjustmentFactor; - videoBitRate *= bitRateAdjustmentFactor; - } - - setCamera(camera); - setOrientationHint(orientation); - setAudioSource(MediaRecorder.AudioSource.CAMCORDER); - setVideoSource(MediaRecorder.VideoSource.CAMERA); - setOutputFormat(mCamcorderProfile.fileFormat); - setOutputFile( - Factory.get().getApplicationContext().getContentResolver().openFileDescriptor( - mTempVideoUri, "w").getFileDescriptor()); - - // Copy settings from CamcorderProfile to MediaRecorder - setAudioEncodingBitRate(audioBitRate); - setAudioChannels(mCamcorderProfile.audioChannels); - setAudioEncoder(mCamcorderProfile.audioCodec); - setAudioSamplingRate(mCamcorderProfile.audioSampleRate); - setVideoEncodingBitRate(videoBitRate); - setVideoEncoder(mCamcorderProfile.videoCodec); - setVideoFrameRate(mCamcorderProfile.videoFrameRate); - setVideoSize( - mCamcorderProfile.videoFrameWidth, mCamcorderProfile.videoFrameHeight); - setMaxFileSize(sizeLimit); - } - - Uri getVideoUri() { - return mTempVideoUri; - } - - int getVideoWidth() { - return mCamcorderProfile.videoFrameWidth; - } - - int getVideoHeight() { - return mCamcorderProfile.videoFrameHeight; - } - - void cleanupTempFile() { - final Uri tempUri = mTempVideoUri; - SafeAsyncTask.executeOnThreadPool(new Runnable() { - @Override - public void run() { - Factory.get().getApplicationContext().getContentResolver().delete( - tempUri, null, null); - } - }); - mTempVideoUri = null; - } - - String getContentType() { - if (mCamcorderProfile.fileFormat == OutputFormat.MPEG_4) { - return ContentType.VIDEO_MP4; - } else { - // 3GPP is the only other video format with a constant in OutputFormat - return ContentType.VIDEO_3GPP; - } - } -} diff --git a/src/com/android/messaging/ui/mediapicker/PausableChronometer.java b/src/com/android/messaging/ui/mediapicker/PausableChronometer.java deleted file mode 100644 index dc8f90b..0000000 --- a/src/com/android/messaging/ui/mediapicker/PausableChronometer.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.content.Context; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.widget.Chronometer; - -import com.android.messaging.ui.PlaybackStateView; - -/** - * A pausable Chronometer implementation. The default Chronometer in Android only stops the UI - * from updating when you call stop(), but doesn't actually pause it. This implementation adds an - * additional timestamp that tracks the timespan for the pause and compensate for that. - */ -public class PausableChronometer extends Chronometer implements PlaybackStateView { - // Keeps track of how far long the Chronometer has been tracking when it's paused. We'd like - // to start from this time the next time it's resumed. - private long mTimeWhenPaused = 0; - - public PausableChronometer(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - /** - * Reset the timer and start counting from zero. - */ - @Override - public void restart() { - reset(); - start(); - } - - /** - * Reset the timer to zero, but don't start it. - */ - @Override - public void reset() { - stop(); - setBase(SystemClock.elapsedRealtime()); - mTimeWhenPaused = 0; - } - - /** - * Resume the timer after a previous pause. - */ - @Override - public void resume() { - setBase(SystemClock.elapsedRealtime() - mTimeWhenPaused); - start(); - } - - /** - * Pause the timer. - */ - @Override - public void pause() { - stop(); - mTimeWhenPaused = SystemClock.elapsedRealtime() - getBase(); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/SoftwareCameraPreview.java b/src/com/android/messaging/ui/mediapicker/SoftwareCameraPreview.java deleted file mode 100644 index 5dc3185..0000000 --- a/src/com/android/messaging/ui/mediapicker/SoftwareCameraPreview.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.content.Context; -import android.hardware.Camera; -import android.os.Parcelable; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; - -import java.io.IOException; - -/** - * A software rendered preview surface for the camera. This renders slower and causes more jank, so - * HardwareCameraPreview is preferred if possible. - * - * There is a significant amount of duplication between HardwareCameraPreview and - * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The - * implementations of the shared methods are delegated to CameraPreview - */ -public class SoftwareCameraPreview extends SurfaceView implements CameraPreview.CameraPreviewHost { - private final CameraPreview mPreview; - - public SoftwareCameraPreview(final Context context) { - super(context); - mPreview = new CameraPreview(this); - getHolder().addCallback(new SurfaceHolder.Callback() { - @Override - public void surfaceCreated(final SurfaceHolder surfaceHolder) { - CameraManager.get().setSurface(mPreview); - } - - @Override - public void surfaceChanged(final SurfaceHolder surfaceHolder, final int format, final int width, - final int height) { - CameraManager.get().setSurface(mPreview); - } - - @Override - public void surfaceDestroyed(final SurfaceHolder surfaceHolder) { - CameraManager.get().setSurface(null); - } - }); - } - - - @Override - protected void onVisibilityChanged(final View changedView, final int visibility) { - super.onVisibilityChanged(changedView, visibility); - mPreview.onVisibilityChanged(visibility); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mPreview.onDetachedFromWindow(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mPreview.onAttachedToWindow(); - } - - @Override - protected void onRestoreInstanceState(final Parcelable state) { - super.onRestoreInstanceState(state); - mPreview.onRestoreInstanceState(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec); - heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - public View getView() { - return this; - } - - @Override - public boolean isValid() { - return getHolder() != null; - } - - @Override - public void startPreview(final Camera camera) throws IOException { - camera.setPreviewDisplay(getHolder()); - } - - @Override - public void onCameraPermissionGranted() { - mPreview.onCameraPermissionGranted(); - } -} - - diff --git a/src/com/android/messaging/ui/mediapicker/SoundLevels.java b/src/com/android/messaging/ui/mediapicker/SoundLevels.java deleted file mode 100644 index 6f4dca6..0000000 --- a/src/com/android/messaging/ui/mediapicker/SoundLevels.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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.animation.ObjectAnimator; -import android.animation.TimeAnimator; -import android.animation.TimeAnimator.TimeListener; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.accessibility.AccessibilityNodeInfo; - -import com.android.messaging.R; -import com.android.messaging.util.LogUtil; - -/** - * This view draws circular sound levels. By default the sound levels are black, unless - * otherwise defined via {@link #mPrimaryLevelPaint}. - */ -public class SoundLevels extends View { - private static final String TAG = LogUtil.BUGLE_TAG; - private static final boolean DEBUG = false; - - private boolean mCenterDefined; - private int mCenterX; - private int mCenterY; - - // Paint for the main level meter, most closely follows the mic. - private final Paint mPrimaryLevelPaint; - - // The minimum size of the levels as a percentage of the max, that is the size when volume is 0. - private final float mMinimumLevel; - - // The minimum size of the levels, that is the size when volume is 0. - private final float mMinimumLevelSize; - - // The maximum size of the levels, that is the size when volume is 100. - private final float mMaximumLevelSize; - - // Generates clock ticks for the animation using the global animation loop. - private final TimeAnimator mSpeechLevelsAnimator; - - private float mCurrentVolume; - - // Indicates whether we should be animating the sound level. - private boolean mIsEnabled; - - // Input level is pulled from here. - private AudioLevelSource mLevelSource; - - public SoundLevels(final Context context) { - this(context, null); - } - - public SoundLevels(final Context context, final AttributeSet attrs) { - this(context, attrs, 0); - } - - public SoundLevels(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - - // Safe source, replaced with system one when attached. - mLevelSource = new AudioLevelSource(); - mLevelSource.setSpeechLevel(0); - - final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SoundLevels, - defStyle, 0); - - mMaximumLevelSize = a.getDimensionPixelOffset( - R.styleable.SoundLevels_maxLevelRadius, 0); - mMinimumLevelSize = a.getDimensionPixelOffset( - R.styleable.SoundLevels_minLevelRadius, 0); - mMinimumLevel = mMinimumLevelSize / mMaximumLevelSize; - - mPrimaryLevelPaint = new Paint(); - mPrimaryLevelPaint.setColor( - a.getColor(R.styleable.SoundLevels_primaryColor, Color.BLACK)); - mPrimaryLevelPaint.setFlags(Paint.ANTI_ALIAS_FLAG); - - a.recycle(); - - // This animator generates ticks that invalidate the - // view so that the animation is synced with the global animation loop. - // TODO: We could probably remove this in favor of using postInvalidateOnAnimation - // which might improve things further. - mSpeechLevelsAnimator = new TimeAnimator(); - mSpeechLevelsAnimator.setRepeatCount(ObjectAnimator.INFINITE); - mSpeechLevelsAnimator.setTimeListener(new TimeListener() { - @Override - public void onTimeUpdate(final TimeAnimator animation, final long totalTime, - final long deltaTime) { - invalidate(); - } - }); - } - - @Override - protected void onDraw(final Canvas canvas) { - if (!mIsEnabled) { - return; - } - - if (!mCenterDefined) { - // One time computation here, because we can't rely on getWidth() to be computed at - // constructor time or in onFinishInflate :(. - mCenterX = getWidth() / 2; - mCenterY = getWidth() / 2; - mCenterDefined = true; - } - - final int level = mLevelSource.getSpeechLevel(); - // Either ease towards the target level, or decay away from it depending on whether - // its higher or lower than the current. - if (level > mCurrentVolume) { - mCurrentVolume = mCurrentVolume + ((level - mCurrentVolume) / 4); - } else { - mCurrentVolume = mCurrentVolume * 0.95f; - } - - final float radius = mMinimumLevel + (1f - mMinimumLevel) * mCurrentVolume / 100; - mPrimaryLevelPaint.setStyle(Style.FILL); - canvas.drawCircle(mCenterX, mCenterY, radius * mMaximumLevelSize, mPrimaryLevelPaint); - } - - public void setLevelSource(final AudioLevelSource source) { - if (DEBUG) { - Log.d(TAG, "Speech source set."); - } - mLevelSource = source; - } - - private void startSpeechLevelsAnimator() { - if (DEBUG) { - Log.d(TAG, "startAnimator()"); - } - if (!mSpeechLevelsAnimator.isStarted()) { - mSpeechLevelsAnimator.start(); - } - } - - private void stopSpeechLevelsAnimator() { - if (DEBUG) { - Log.d(TAG, "stopAnimator()"); - } - if (mSpeechLevelsAnimator.isStarted()) { - mSpeechLevelsAnimator.end(); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - stopSpeechLevelsAnimator(); - } - - @Override - public void setEnabled(final boolean enabled) { - if (enabled == mIsEnabled) { - return; - } - if (DEBUG) { - Log.d("TAG", "setEnabled: " + enabled); - } - super.setEnabled(enabled); - mIsEnabled = enabled; - setKeepScreenOn(enabled); - updateSpeechLevelsAnimatorState(); - } - - private void updateSpeechLevelsAnimatorState() { - if (mIsEnabled) { - startSpeechLevelsAnimator(); - } else { - stopSpeechLevelsAnimator(); - } - } - - /** - * This is required to make the View findable by uiautomator - */ - @Override - public void onInitializeAccessibilityNodeInfo(final AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(SoundLevels.class.getCanonicalName()); - } - - /** - * Set the alpha level of the sound circles. - */ - public void setPrimaryColorAlpha(final int alpha) { - mPrimaryLevelPaint.setAlpha(alpha); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/camerafocus/FocusIndicator.java b/src/com/android/messaging/ui/mediapicker/camerafocus/FocusIndicator.java deleted file mode 100644 index 92ed3c1..0000000 --- a/src/com/android/messaging/ui/mediapicker/camerafocus/FocusIndicator.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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.camerafocus; - -public interface FocusIndicator { - public void showStart(); - public void showSuccess(boolean timeout); - public void showFail(boolean timeout); - public void clear(); -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/mediapicker/camerafocus/FocusOverlayManager.java b/src/com/android/messaging/ui/mediapicker/camerafocus/FocusOverlayManager.java deleted file mode 100644 index e620fc2..0000000 --- a/src/com/android/messaging/ui/mediapicker/camerafocus/FocusOverlayManager.java +++ /dev/null @@ -1,589 +0,0 @@ -/* - * 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.camerafocus; - -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.RectF; -import android.hardware.Camera.Area; -import android.hardware.Camera.Parameters; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; - -import com.android.messaging.util.Assert; -import com.android.messaging.util.LogUtil; - -import java.util.ArrayList; -import java.util.List; - -/* A class that handles everything about focus in still picture mode. - * This also handles the metering area because it is the same as focus area. - * - * The test cases: - * (1) The camera has continuous autofocus. Move the camera. Take a picture when - * CAF is not in progress. - * (2) The camera has continuous autofocus. Move the camera. Take a picture when - * CAF is in progress. - * (3) The camera has face detection. Point the camera at some faces. Hold the - * shutter. Release to take a picture. - * (4) The camera has face detection. Point the camera at some faces. Single tap - * the shutter to take a picture. - * (5) The camera has autofocus. Single tap the shutter to take a picture. - * (6) The camera has autofocus. Hold the shutter. Release to take a picture. - * (7) The camera has no autofocus. Single tap the shutter and take a picture. - * (8) The camera has autofocus and supports focus area. Touch the screen to - * trigger autofocus. Take a picture. - * (9) The camera has autofocus and supports focus area. Touch the screen to - * trigger autofocus. Wait until it times out. - * (10) The camera has no autofocus and supports metering area. Touch the screen - * to change metering area. - */ -public class FocusOverlayManager { - private static final String TAG = LogUtil.BUGLE_TAG; - private static final String TRUE = "true"; - private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported"; - private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED = - "auto-whitebalance-lock-supported"; - - private static final int RESET_TOUCH_FOCUS = 0; - private static final int RESET_TOUCH_FOCUS_DELAY = 3000; - - private int mState = STATE_IDLE; - private static final int STATE_IDLE = 0; // Focus is not active. - private static final int STATE_FOCUSING = 1; // Focus is in progress. - // Focus is in progress and the camera should take a picture after focus finishes. - private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2; - private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds. - private static final int STATE_FAIL = 4; // Focus finishes and fails. - - private boolean mInitialized; - private boolean mFocusAreaSupported; - private boolean mMeteringAreaSupported; - private boolean mLockAeAwbNeeded; - private boolean mAeAwbLock; - private Matrix mMatrix; - - private PieRenderer mPieRenderer; - - private int mPreviewWidth; // The width of the preview frame layout. - private int mPreviewHeight; // The height of the preview frame layout. - private boolean mMirror; // true if the camera is front-facing. - private int mDisplayOrientation; - private List<Object> mFocusArea; // focus area in driver format - private List<Object> mMeteringArea; // metering area in driver format - private String mFocusMode; - private String mOverrideFocusMode; - private Parameters mParameters; - private Handler mHandler; - Listener mListener; - - public interface Listener { - public void autoFocus(); - public void cancelAutoFocus(); - public boolean capture(); - public void setFocusParameters(); - } - - private class MainHandler extends Handler { - public MainHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case RESET_TOUCH_FOCUS: { - cancelAutoFocus(); - break; - } - } - } - } - - public FocusOverlayManager(Listener listener, Looper looper) { - mHandler = new MainHandler(looper); - mMatrix = new Matrix(); - mListener = listener; - } - - public void setFocusRenderer(PieRenderer renderer) { - mPieRenderer = renderer; - mInitialized = (mMatrix != null); - } - - public void setParameters(Parameters parameters) { - // parameters can only be null when onConfigurationChanged is called - // before camera is open. We will just return in this case, because - // parameters will be set again later with the right parameters after - // camera is open. - if (parameters == null) { - return; - } - mParameters = parameters; - mFocusAreaSupported = isFocusAreaSupported(parameters); - mMeteringAreaSupported = isMeteringAreaSupported(parameters); - mLockAeAwbNeeded = (isAutoExposureLockSupported(mParameters) || - isAutoWhiteBalanceLockSupported(mParameters)); - } - - public void setPreviewSize(int previewWidth, int previewHeight) { - if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) { - mPreviewWidth = previewWidth; - mPreviewHeight = previewHeight; - setMatrix(); - } - } - - public void setMirror(boolean mirror) { - mMirror = mirror; - setMatrix(); - } - - public void setDisplayOrientation(int displayOrientation) { - mDisplayOrientation = displayOrientation; - setMatrix(); - } - - private void setMatrix() { - if (mPreviewWidth != 0 && mPreviewHeight != 0) { - Matrix matrix = new Matrix(); - prepareMatrix(matrix, mMirror, mDisplayOrientation, - mPreviewWidth, mPreviewHeight); - // In face detection, the matrix converts the driver coordinates to UI - // coordinates. In tap focus, the inverted matrix converts the UI - // coordinates to driver coordinates. - matrix.invert(mMatrix); - mInitialized = (mPieRenderer != null); - } - } - - private void lockAeAwbIfNeeded() { - if (mLockAeAwbNeeded && !mAeAwbLock) { - mAeAwbLock = true; - mListener.setFocusParameters(); - } - } - - private void unlockAeAwbIfNeeded() { - if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) { - mAeAwbLock = false; - mListener.setFocusParameters(); - } - } - - public void onShutterDown() { - if (!mInitialized) { - return; - } - - boolean autoFocusCalled = false; - if (needAutoFocusCall()) { - // Do not focus if touch focus has been triggered. - if (mState != STATE_SUCCESS && mState != STATE_FAIL) { - autoFocus(); - autoFocusCalled = true; - } - } - - if (!autoFocusCalled) { - lockAeAwbIfNeeded(); - } - } - - public void onShutterUp() { - if (!mInitialized) { - return; - } - - if (needAutoFocusCall()) { - // User releases half-pressed focus key. - if (mState == STATE_FOCUSING || mState == STATE_SUCCESS - || mState == STATE_FAIL) { - cancelAutoFocus(); - } - } - - // Unlock AE and AWB after cancelAutoFocus. Camera API does not - // guarantee setParameters can be called during autofocus. - unlockAeAwbIfNeeded(); - } - - public void doSnap() { - if (!mInitialized) { - return; - } - - // If the user has half-pressed the shutter and focus is completed, we - // can take the photo right away. If the focus mode is infinity, we can - // also take the photo. - if (!needAutoFocusCall() || (mState == STATE_SUCCESS || mState == STATE_FAIL)) { - capture(); - } else if (mState == STATE_FOCUSING) { - // Half pressing the shutter (i.e. the focus button event) will - // already have requested AF for us, so just request capture on - // focus here. - mState = STATE_FOCUSING_SNAP_ON_FINISH; - } else if (mState == STATE_IDLE) { - // We didn't do focus. This can happen if the user press focus key - // while the snapshot is still in progress. The user probably wants - // the next snapshot as soon as possible, so we just do a snapshot - // without focusing again. - capture(); - } - } - - public void onAutoFocus(boolean focused, boolean shutterButtonPressed) { - if (mState == STATE_FOCUSING_SNAP_ON_FINISH) { - // Take the picture no matter focus succeeds or fails. No need - // to play the AF sound if we're about to play the shutter - // sound. - if (focused) { - mState = STATE_SUCCESS; - } else { - mState = STATE_FAIL; - } - updateFocusUI(); - capture(); - } else if (mState == STATE_FOCUSING) { - // This happens when (1) user is half-pressing the focus key or - // (2) touch focus is triggered. Play the focus tone. Do not - // take the picture now. - if (focused) { - mState = STATE_SUCCESS; - } else { - mState = STATE_FAIL; - } - updateFocusUI(); - // If this is triggered by touch focus, cancel focus after a - // while. - if (mFocusArea != null) { - mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY); - } - if (shutterButtonPressed) { - // Lock AE & AWB so users can half-press shutter and recompose. - lockAeAwbIfNeeded(); - } - } else if (mState == STATE_IDLE) { - // User has released the focus key before focus completes. - // Do nothing. - } - } - - public void onAutoFocusMoving(boolean moving) { - if (!mInitialized) { - return; - } - - // Ignore if we have requested autofocus. This method only handles - // continuous autofocus. - if (mState != STATE_IDLE) { - return; - } - - if (moving) { - mPieRenderer.showStart(); - } else { - mPieRenderer.showSuccess(true); - } - } - - private void initializeFocusAreas(int focusWidth, int focusHeight, - int x, int y, int previewWidth, int previewHeight) { - if (mFocusArea == null) { - mFocusArea = new ArrayList<Object>(); - mFocusArea.add(new Area(new Rect(), 1)); - } - - // Convert the coordinates to driver format. - calculateTapArea(focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight, - ((Area) mFocusArea.get(0)).rect); - } - - private void initializeMeteringAreas(int focusWidth, int focusHeight, - int x, int y, int previewWidth, int previewHeight) { - if (mMeteringArea == null) { - mMeteringArea = new ArrayList<Object>(); - mMeteringArea.add(new Area(new Rect(), 1)); - } - - // Convert the coordinates to driver format. - // AE area is bigger because exposure is sensitive and - // easy to over- or underexposure if area is too small. - calculateTapArea(focusWidth, focusHeight, 1.5f, x, y, previewWidth, previewHeight, - ((Area) mMeteringArea.get(0)).rect); - } - - public void onSingleTapUp(int x, int y) { - if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) { - return; - } - - // Let users be able to cancel previous touch focus. - if ((mFocusArea != null) && (mState == STATE_FOCUSING || - mState == STATE_SUCCESS || mState == STATE_FAIL)) { - cancelAutoFocus(); - } - // Initialize variables. - int focusWidth = mPieRenderer.getSize(); - int focusHeight = mPieRenderer.getSize(); - if (focusWidth == 0 || mPieRenderer.getWidth() == 0 || mPieRenderer.getHeight() == 0) { - return; - } - int previewWidth = mPreviewWidth; - int previewHeight = mPreviewHeight; - // Initialize mFocusArea. - if (mFocusAreaSupported) { - initializeFocusAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight); - } - // Initialize mMeteringArea. - if (mMeteringAreaSupported) { - initializeMeteringAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight); - } - - // Use margin to set the focus indicator to the touched area. - mPieRenderer.setFocus(x, y); - - // Set the focus area and metering area. - mListener.setFocusParameters(); - if (mFocusAreaSupported) { - autoFocus(); - } else { // Just show the indicator in all other cases. - updateFocusUI(); - // Reset the metering area in 3 seconds. - mHandler.removeMessages(RESET_TOUCH_FOCUS); - mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY); - } - } - - public void onPreviewStarted() { - mState = STATE_IDLE; - } - - public void onPreviewStopped() { - // If auto focus was in progress, it would have been stopped. - mState = STATE_IDLE; - resetTouchFocus(); - updateFocusUI(); - } - - public void onCameraReleased() { - onPreviewStopped(); - } - - private void autoFocus() { - LogUtil.v(TAG, "Start autofocus."); - mListener.autoFocus(); - mState = STATE_FOCUSING; - updateFocusUI(); - mHandler.removeMessages(RESET_TOUCH_FOCUS); - } - - private void cancelAutoFocus() { - LogUtil.v(TAG, "Cancel autofocus."); - - // Reset the tap area before calling mListener.cancelAutofocus. - // Otherwise, focus mode stays at auto and the tap area passed to the - // driver is not reset. - resetTouchFocus(); - mListener.cancelAutoFocus(); - mState = STATE_IDLE; - updateFocusUI(); - mHandler.removeMessages(RESET_TOUCH_FOCUS); - } - - private void capture() { - if (mListener.capture()) { - mState = STATE_IDLE; - mHandler.removeMessages(RESET_TOUCH_FOCUS); - } - } - - public String getFocusMode() { - if (mOverrideFocusMode != null) { - return mOverrideFocusMode; - } - List<String> supportedFocusModes = mParameters.getSupportedFocusModes(); - - if (mFocusAreaSupported && mFocusArea != null) { - // Always use autofocus in tap-to-focus. - mFocusMode = Parameters.FOCUS_MODE_AUTO; - } else { - mFocusMode = Parameters.FOCUS_MODE_CONTINUOUS_PICTURE; - } - - if (!isSupported(mFocusMode, supportedFocusModes)) { - // For some reasons, the driver does not support the current - // focus mode. Fall back to auto. - if (isSupported(Parameters.FOCUS_MODE_AUTO, - mParameters.getSupportedFocusModes())) { - mFocusMode = Parameters.FOCUS_MODE_AUTO; - } else { - mFocusMode = mParameters.getFocusMode(); - } - } - return mFocusMode; - } - - public List getFocusAreas() { - return mFocusArea; - } - - public List getMeteringAreas() { - return mMeteringArea; - } - - public void updateFocusUI() { - if (!mInitialized) { - return; - } - FocusIndicator focusIndicator = mPieRenderer; - - if (mState == STATE_IDLE) { - if (mFocusArea == null) { - focusIndicator.clear(); - } else { - // Users touch on the preview and the indicator represents the - // metering area. Either focus area is not supported or - // autoFocus call is not required. - focusIndicator.showStart(); - } - } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) { - focusIndicator.showStart(); - } else { - if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) { - // TODO: check HAL behavior and decide if this can be removed. - focusIndicator.showSuccess(false); - } else if (mState == STATE_SUCCESS) { - focusIndicator.showSuccess(false); - } else if (mState == STATE_FAIL) { - focusIndicator.showFail(false); - } - } - } - - public void resetTouchFocus() { - if (!mInitialized) { - return; - } - - // Put focus indicator to the center. clear reset position - mPieRenderer.clear(); - - mFocusArea = null; - mMeteringArea = null; - } - - private void calculateTapArea(int focusWidth, int focusHeight, float areaMultiple, - int x, int y, int previewWidth, int previewHeight, Rect rect) { - int areaWidth = (int) (focusWidth * areaMultiple); - int areaHeight = (int) (focusHeight * areaMultiple); - int left = clamp(x - areaWidth / 2, 0, previewWidth - areaWidth); - int top = clamp(y - areaHeight / 2, 0, previewHeight - areaHeight); - - RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight); - mMatrix.mapRect(rectF); - rectFToRect(rectF, rect); - } - - /* package */ int getFocusState() { - return mState; - } - - public boolean isFocusCompleted() { - return mState == STATE_SUCCESS || mState == STATE_FAIL; - } - - public boolean isFocusingSnapOnFinish() { - return mState == STATE_FOCUSING_SNAP_ON_FINISH; - } - - public void removeMessages() { - mHandler.removeMessages(RESET_TOUCH_FOCUS); - } - - public void overrideFocusMode(String focusMode) { - mOverrideFocusMode = focusMode; - } - - public void setAeAwbLock(boolean lock) { - mAeAwbLock = lock; - } - - public boolean getAeAwbLock() { - return mAeAwbLock; - } - - private boolean needAutoFocusCall() { - String focusMode = getFocusMode(); - return !(focusMode.equals(Parameters.FOCUS_MODE_INFINITY) - || focusMode.equals(Parameters.FOCUS_MODE_FIXED) - || focusMode.equals(Parameters.FOCUS_MODE_EDOF)); - } - - public static boolean isAutoExposureLockSupported(Parameters params) { - return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED)); - } - - public static boolean isAutoWhiteBalanceLockSupported(Parameters params) { - return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED)); - } - - public static boolean isSupported(String value, List<String> supported) { - return supported != null && supported.indexOf(value) >= 0; - } - - public static boolean isMeteringAreaSupported(Parameters params) { - return params.getMaxNumMeteringAreas() > 0; - } - - public static boolean isFocusAreaSupported(Parameters params) { - return (params.getMaxNumFocusAreas() > 0 - && isSupported(Parameters.FOCUS_MODE_AUTO, - params.getSupportedFocusModes())); - } - - public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation, - int viewWidth, int viewHeight) { - // Need mirror for front camera. - matrix.setScale(mirror ? -1 : 1, 1); - // This is the value for android.hardware.Camera.setDisplayOrientation. - matrix.postRotate(displayOrientation); - // Camera driver coordinates range from (-1000, -1000) to (1000, 1000). - // UI coordinates range from (0, 0) to (width, height). - matrix.postScale(viewWidth / 2000f, viewHeight / 2000f); - matrix.postTranslate(viewWidth / 2f, viewHeight / 2f); - } - - public static int clamp(int x, int min, int max) { - Assert.isTrue(max >= min); - if (x > max) { - return max; - } - if (x < min) { - return min; - } - return x; - } - - public static void rectFToRect(RectF rectF, Rect rect) { - rect.left = Math.round(rectF.left); - rect.top = Math.round(rectF.top); - rect.right = Math.round(rectF.right); - rect.bottom = Math.round(rectF.bottom); - } -} diff --git a/src/com/android/messaging/ui/mediapicker/camerafocus/OverlayRenderer.java b/src/com/android/messaging/ui/mediapicker/camerafocus/OverlayRenderer.java deleted file mode 100644 index df6734f..0000000 --- a/src/com/android/messaging/ui/mediapicker/camerafocus/OverlayRenderer.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.camerafocus; - -import android.content.Context; -import android.graphics.Canvas; -import android.view.MotionEvent; - -public abstract class OverlayRenderer implements RenderOverlay.Renderer { - - private static final String TAG = "CAM OverlayRenderer"; - protected RenderOverlay mOverlay; - - protected int mLeft, mTop, mRight, mBottom; - - protected boolean mVisible; - - public void setVisible(boolean vis) { - mVisible = vis; - update(); - } - - public boolean isVisible() { - return mVisible; - } - - // default does not handle touch - @Override - public boolean handlesTouch() { - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent evt) { - return false; - } - - public abstract void onDraw(Canvas canvas); - - public void draw(Canvas canvas) { - if (mVisible) { - onDraw(canvas); - } - } - - @Override - public void setOverlay(RenderOverlay overlay) { - mOverlay = overlay; - } - - @Override - public void layout(int left, int top, int right, int bottom) { - mLeft = left; - mRight = right; - mTop = top; - mBottom = bottom; - } - - protected Context getContext() { - if (mOverlay != null) { - return mOverlay.getContext(); - } else { - return null; - } - } - - public int getWidth() { - return mRight - mLeft; - } - - public int getHeight() { - return mBottom - mTop; - } - - protected void update() { - if (mOverlay != null) { - mOverlay.update(); - } - } - -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/mediapicker/camerafocus/PieItem.java b/src/com/android/messaging/ui/mediapicker/camerafocus/PieItem.java deleted file mode 100644 index c602852..0000000 --- a/src/com/android/messaging/ui/mediapicker/camerafocus/PieItem.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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.camerafocus; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Path; -import android.graphics.drawable.Drawable; - -import java.util.ArrayList; -import java.util.List; - -/** - * Pie menu item - */ -public class PieItem { - - public static interface OnClickListener { - void onClick(PieItem item); - } - - private Drawable mDrawable; - private int level; - private float mCenter; - private float start; - private float sweep; - private float animate; - private int inner; - private int outer; - private boolean mSelected; - private boolean mEnabled; - private List<PieItem> mItems; - private Path mPath; - private OnClickListener mOnClickListener; - private float mAlpha; - - // Gray out the view when disabled - private static final float ENABLED_ALPHA = 1; - private static final float DISABLED_ALPHA = (float) 0.3; - private boolean mChangeAlphaWhenDisabled = true; - - public PieItem(Drawable drawable, int level) { - mDrawable = drawable; - this.level = level; - setAlpha(1f); - mEnabled = true; - setAnimationAngle(getAnimationAngle()); - start = -1; - mCenter = -1; - } - - public boolean hasItems() { - return mItems != null; - } - - public List<PieItem> getItems() { - return mItems; - } - - public void addItem(PieItem item) { - if (mItems == null) { - mItems = new ArrayList<PieItem>(); - } - mItems.add(item); - } - - public void setPath(Path p) { - mPath = p; - } - - public Path getPath() { - return mPath; - } - - public void setChangeAlphaWhenDisabled (boolean enable) { - mChangeAlphaWhenDisabled = enable; - } - - public void setAlpha(float alpha) { - mAlpha = alpha; - mDrawable.setAlpha((int) (255 * alpha)); - } - - public void setAnimationAngle(float a) { - animate = a; - } - - public float getAnimationAngle() { - return animate; - } - - public void setEnabled(boolean enabled) { - mEnabled = enabled; - if (mChangeAlphaWhenDisabled) { - if (mEnabled) { - setAlpha(ENABLED_ALPHA); - } else { - setAlpha(DISABLED_ALPHA); - } - } - } - - public boolean isEnabled() { - return mEnabled; - } - - public void setSelected(boolean s) { - mSelected = s; - } - - public boolean isSelected() { - return mSelected; - } - - public int getLevel() { - return level; - } - - public void setGeometry(float st, float sw, int inside, int outside) { - start = st; - sweep = sw; - inner = inside; - outer = outside; - } - - public void setFixedSlice(float center, float sweep) { - mCenter = center; - this.sweep = sweep; - } - - public float getCenter() { - return mCenter; - } - - public float getStart() { - return start; - } - - public float getStartAngle() { - return start + animate; - } - - public float getSweep() { - return sweep; - } - - public int getInnerRadius() { - return inner; - } - - public int getOuterRadius() { - return outer; - } - - public void setOnClickListener(OnClickListener listener) { - mOnClickListener = listener; - } - - public void performClick() { - if (mOnClickListener != null) { - mOnClickListener.onClick(this); - } - } - - public int getIntrinsicWidth() { - return mDrawable.getIntrinsicWidth(); - } - - public int getIntrinsicHeight() { - return mDrawable.getIntrinsicHeight(); - } - - public void setBounds(int left, int top, int right, int bottom) { - mDrawable.setBounds(left, top, right, bottom); - } - - public void draw(Canvas canvas) { - mDrawable.draw(canvas); - } - - public void setImageResource(Context context, int resId) { - Drawable d = context.getResources().getDrawable(resId).mutate(); - d.setBounds(mDrawable.getBounds()); - mDrawable = d; - setAlpha(mAlpha); - } - -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/mediapicker/camerafocus/PieRenderer.java b/src/com/android/messaging/ui/mediapicker/camerafocus/PieRenderer.java deleted file mode 100644 index ce8ca00..0000000 --- a/src/com/android/messaging/ui/mediapicker/camerafocus/PieRenderer.java +++ /dev/null @@ -1,825 +0,0 @@ -/* - * 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.camerafocus; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.RectF; -import android.os.Handler; -import android.os.Message; -import android.view.MotionEvent; -import android.view.ViewConfiguration; -import android.view.animation.Animation; -import android.view.animation.Animation.AnimationListener; -import android.view.animation.LinearInterpolator; -import android.view.animation.Transformation; -import com.android.messaging.R; - -import java.util.ArrayList; -import java.util.List; - -public class PieRenderer extends OverlayRenderer - implements FocusIndicator { - // Sometimes continuous autofocus starts and stops several times quickly. - // These states are used to make sure the animation is run for at least some - // time. - private volatile int mState; - private ScaleAnimation mAnimation = new ScaleAnimation(); - private static final int STATE_IDLE = 0; - private static final int STATE_FOCUSING = 1; - private static final int STATE_FINISHING = 2; - private static final int STATE_PIE = 8; - - private Runnable mDisappear = new Disappear(); - private Animation.AnimationListener mEndAction = new EndAction(); - private static final int SCALING_UP_TIME = 600; - private static final int SCALING_DOWN_TIME = 100; - private static final int DISAPPEAR_TIMEOUT = 200; - private static final int DIAL_HORIZONTAL = 157; - - private static final long PIE_FADE_IN_DURATION = 200; - private static final long PIE_XFADE_DURATION = 200; - private static final long PIE_SELECT_FADE_DURATION = 300; - - private static final int MSG_OPEN = 0; - private static final int MSG_CLOSE = 1; - private static final float PIE_SWEEP = (float) (Math.PI * 2 / 3); - // geometry - private Point mCenter; - private int mRadius; - private int mRadiusInc; - - // the detection if touch is inside a slice is offset - // inbounds by this amount to allow the selection to show before the - // finger covers it - private int mTouchOffset; - - private List<PieItem> mItems; - - private PieItem mOpenItem; - - private Paint mSelectedPaint; - private Paint mSubPaint; - - // touch handling - private PieItem mCurrentItem; - - private Paint mFocusPaint; - private int mSuccessColor; - private int mFailColor; - private int mCircleSize; - private int mFocusX; - private int mFocusY; - private int mCenterX; - private int mCenterY; - - private int mDialAngle; - private RectF mCircle; - private RectF mDial; - private Point mPoint1; - private Point mPoint2; - private int mStartAnimationAngle; - private boolean mFocused; - private int mInnerOffset; - private int mOuterStroke; - private int mInnerStroke; - private boolean mTapMode; - private boolean mBlockFocus; - private int mTouchSlopSquared; - private Point mDown; - private boolean mOpening; - private LinearAnimation mXFade; - private LinearAnimation mFadeIn; - private volatile boolean mFocusCancelled; - - private Handler mHandler = new Handler() { - public void handleMessage(Message msg) { - switch(msg.what) { - case MSG_OPEN: - if (mListener != null) { - mListener.onPieOpened(mCenter.x, mCenter.y); - } - break; - case MSG_CLOSE: - if (mListener != null) { - mListener.onPieClosed(); - } - break; - } - } - }; - - private PieListener mListener; - - public static interface PieListener { - public void onPieOpened(int centerX, int centerY); - public void onPieClosed(); - } - - public void setPieListener(PieListener pl) { - mListener = pl; - } - - public PieRenderer(Context context) { - init(context); - } - - private void init(Context ctx) { - setVisible(false); - mItems = new ArrayList<PieItem>(); - Resources res = ctx.getResources(); - mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); - mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); - mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); - mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); - mCenter = new Point(0, 0); - mSelectedPaint = new Paint(); - mSelectedPaint.setColor(Color.argb(255, 51, 181, 229)); - mSelectedPaint.setAntiAlias(true); - mSubPaint = new Paint(); - mSubPaint.setAntiAlias(true); - mSubPaint.setColor(Color.argb(200, 250, 230, 128)); - mFocusPaint = new Paint(); - mFocusPaint.setAntiAlias(true); - mFocusPaint.setColor(Color.WHITE); - mFocusPaint.setStyle(Paint.Style.STROKE); - mSuccessColor = Color.GREEN; - mFailColor = Color.RED; - mCircle = new RectF(); - mDial = new RectF(); - mPoint1 = new Point(); - mPoint2 = new Point(); - mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); - mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); - mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); - mState = STATE_IDLE; - mBlockFocus = false; - mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop(); - mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared; - mDown = new Point(); - } - - public boolean showsItems() { - return mTapMode; - } - - public void addItem(PieItem item) { - // add the item to the pie itself - mItems.add(item); - } - - public void removeItem(PieItem item) { - mItems.remove(item); - } - - public void clearItems() { - mItems.clear(); - } - - public void showInCenter() { - if ((mState == STATE_PIE) && isVisible()) { - mTapMode = false; - show(false); - } else { - if (mState != STATE_IDLE) { - cancelFocus(); - } - mState = STATE_PIE; - setCenter(mCenterX, mCenterY); - mTapMode = true; - show(true); - } - } - - public void hide() { - show(false); - } - - /** - * guaranteed has center set - * @param show - */ - private void show(boolean show) { - if (show) { - mState = STATE_PIE; - // ensure clean state - mCurrentItem = null; - mOpenItem = null; - for (PieItem item : mItems) { - item.setSelected(false); - } - layoutPie(); - fadeIn(); - } else { - mState = STATE_IDLE; - mTapMode = false; - if (mXFade != null) { - mXFade.cancel(); - } - } - setVisible(show); - mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); - } - - private void fadeIn() { - mFadeIn = new LinearAnimation(0, 1); - mFadeIn.setDuration(PIE_FADE_IN_DURATION); - mFadeIn.setAnimationListener(new AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - mFadeIn = null; - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - }); - mFadeIn.startNow(); - mOverlay.startAnimation(mFadeIn); - } - - public void setCenter(int x, int y) { - mCenter.x = x; - mCenter.y = y; - // when using the pie menu, align the focus ring - alignFocus(x, y); - } - - private void layoutPie() { - int rgap = 2; - int inner = mRadius + rgap; - int outer = mRadius + mRadiusInc - rgap; - int gap = 1; - layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap); - } - - private void layoutItems(List<PieItem> items, float centerAngle, int inner, - int outer, int gap) { - float emptyangle = PIE_SWEEP / 16; - float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size(); - float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2; - // check if we have custom geometry - // first item we find triggers custom sweep for all - // this allows us to re-use the path - for (PieItem item : items) { - if (item.getCenter() >= 0) { - sweep = item.getSweep(); - break; - } - } - Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, - outer, inner, mCenter); - for (PieItem item : items) { - // shared between items - item.setPath(path); - if (item.getCenter() >= 0) { - angle = item.getCenter(); - } - int w = item.getIntrinsicWidth(); - int h = item.getIntrinsicHeight(); - // move views to outer border - int r = inner + (outer - inner) * 2 / 3; - int x = (int) (r * Math.cos(angle)); - int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2; - x = mCenter.x + x - w / 2; - item.setBounds(x, y, x + w, y + h); - float itemstart = angle - sweep / 2; - item.setGeometry(itemstart, sweep, inner, outer); - if (item.hasItems()) { - layoutItems(item.getItems(), angle, inner, - outer + mRadiusInc / 2, gap); - } - angle += sweep; - } - } - - private Path makeSlice(float start, float end, int outer, int inner, Point center) { - RectF bb = - new RectF(center.x - outer, center.y - outer, center.x + outer, - center.y + outer); - RectF bbi = - new RectF(center.x - inner, center.y - inner, center.x + inner, - center.y + inner); - Path path = new Path(); - path.arcTo(bb, start, end - start, true); - path.arcTo(bbi, end, start - end); - path.close(); - return path; - } - - /** - * converts a - * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) - * @return skia angle - */ - private float getDegrees(double angle) { - return (float) (360 - 180 * angle / Math.PI); - } - - private void startFadeOut() { - mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - deselect(); - show(false); - mOverlay.setAlpha(1); - super.onAnimationEnd(animation); - } - }).setDuration(PIE_SELECT_FADE_DURATION); - } - - @Override - public void onDraw(Canvas canvas) { - float alpha = 1; - if (mXFade != null) { - alpha = mXFade.getValue(); - } else if (mFadeIn != null) { - alpha = mFadeIn.getValue(); - } - int state = canvas.save(); - if (mFadeIn != null) { - float sf = 0.9f + alpha * 0.1f; - canvas.scale(sf, sf, mCenter.x, mCenter.y); - } - drawFocus(canvas); - if (mState == STATE_FINISHING) { - canvas.restoreToCount(state); - return; - } - if ((mOpenItem == null) || (mXFade != null)) { - // draw base menu - for (PieItem item : mItems) { - drawItem(canvas, item, alpha); - } - } - if (mOpenItem != null) { - for (PieItem inner : mOpenItem.getItems()) { - drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); - } - } - canvas.restoreToCount(state); - } - - private void drawItem(Canvas canvas, PieItem item, float alpha) { - if (mState == STATE_PIE) { - if (item.getPath() != null) { - if (item.isSelected()) { - Paint p = mSelectedPaint; - int state = canvas.save(); - float r = getDegrees(item.getStartAngle()); - canvas.rotate(r, mCenter.x, mCenter.y); - canvas.drawPath(item.getPath(), p); - canvas.restoreToCount(state); - } - alpha = alpha * (item.isEnabled() ? 1 : 0.3f); - // draw the item view - item.setAlpha(alpha); - item.draw(canvas); - } - } - } - - @Override - public boolean onTouchEvent(MotionEvent evt) { - float x = evt.getX(); - float y = evt.getY(); - int action = evt.getActionMasked(); - PointF polar = getPolar(x, y, !(mTapMode)); - if (MotionEvent.ACTION_DOWN == action) { - mDown.x = (int) evt.getX(); - mDown.y = (int) evt.getY(); - mOpening = false; - if (mTapMode) { - PieItem item = findItem(polar); - if ((item != null) && (mCurrentItem != item)) { - mState = STATE_PIE; - onEnter(item); - } - } else { - setCenter((int) x, (int) y); - show(true); - } - return true; - } else if (MotionEvent.ACTION_UP == action) { - if (isVisible()) { - PieItem item = mCurrentItem; - if (mTapMode) { - item = findItem(polar); - if (item != null && mOpening) { - mOpening = false; - return true; - } - } - if (item == null) { - mTapMode = false; - show(false); - } else if (!mOpening - && !item.hasItems()) { - item.performClick(); - startFadeOut(); - mTapMode = false; - } - return true; - } - } else if (MotionEvent.ACTION_CANCEL == action) { - if (isVisible() || mTapMode) { - show(false); - } - deselect(); - return false; - } else if (MotionEvent.ACTION_MOVE == action) { - if (polar.y < mRadius) { - if (mOpenItem != null) { - mOpenItem = null; - } else { - deselect(); - } - return false; - } - PieItem item = findItem(polar); - boolean moved = hasMoved(evt); - if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { - // only select if we didn't just open or have moved past slop - mOpening = false; - if (moved) { - // switch back to swipe mode - mTapMode = false; - } - onEnter(item); - } - } - return false; - } - - private boolean hasMoved(MotionEvent e) { - return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) - + (e.getY() - mDown.y) * (e.getY() - mDown.y); - } - - /** - * enter a slice for a view - * updates model only - * @param item - */ - private void onEnter(PieItem item) { - if (mCurrentItem != null) { - mCurrentItem.setSelected(false); - } - if (item != null && item.isEnabled()) { - item.setSelected(true); - mCurrentItem = item; - if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { - openCurrentItem(); - } - } else { - mCurrentItem = null; - } - } - - private void deselect() { - if (mCurrentItem != null) { - mCurrentItem.setSelected(false); - } - if (mOpenItem != null) { - mOpenItem = null; - } - mCurrentItem = null; - } - - private void openCurrentItem() { - if ((mCurrentItem != null) && mCurrentItem.hasItems()) { - mCurrentItem.setSelected(false); - mOpenItem = mCurrentItem; - mOpening = true; - mXFade = new LinearAnimation(1, 0); - mXFade.setDuration(PIE_XFADE_DURATION); - mXFade.setAnimationListener(new AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - mXFade = null; - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - }); - mXFade.startNow(); - mOverlay.startAnimation(mXFade); - } - } - - private PointF getPolar(float x, float y, boolean useOffset) { - PointF res = new PointF(); - // get angle and radius from x/y - res.x = (float) Math.PI / 2; - x = x - mCenter.x; - y = mCenter.y - y; - res.y = (float) Math.sqrt(x * x + y * y); - if (x != 0) { - res.x = (float) Math.atan2(y, x); - if (res.x < 0) { - res.x = (float) (2 * Math.PI + res.x); - } - } - res.y = res.y + (useOffset ? mTouchOffset : 0); - return res; - } - - /** - * @param polar x: angle, y: dist - * @return the item at angle/dist or null - */ - private PieItem findItem(PointF polar) { - // find the matching item: - List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; - for (PieItem item : items) { - if (inside(polar, item)) { - return item; - } - } - return null; - } - - private boolean inside(PointF polar, PieItem item) { - return (item.getInnerRadius() < polar.y) - && (item.getStartAngle() < polar.x) - && (item.getStartAngle() + item.getSweep() > polar.x) - && (!mTapMode || (item.getOuterRadius() > polar.y)); - } - - @Override - public boolean handlesTouch() { - return true; - } - - // focus specific code - - public void setBlockFocus(boolean blocked) { - mBlockFocus = blocked; - if (blocked) { - clear(); - } - } - - public void setFocus(int x, int y) { - mFocusX = x; - mFocusY = y; - setCircle(mFocusX, mFocusY); - } - - public void alignFocus(int x, int y) { - mOverlay.removeCallbacks(mDisappear); - mAnimation.cancel(); - mAnimation.reset(); - mFocusX = x; - mFocusY = y; - mDialAngle = DIAL_HORIZONTAL; - setCircle(x, y); - mFocused = false; - } - - public int getSize() { - return 2 * mCircleSize; - } - - private int getRandomRange() { - return (int) (-60 + 120 * Math.random()); - } - - @Override - public void layout(int l, int t, int r, int b) { - super.layout(l, t, r, b); - mCenterX = (r - l) / 2; - mCenterY = (b - t) / 2; - mFocusX = mCenterX; - mFocusY = mCenterY; - setCircle(mFocusX, mFocusY); - if (isVisible() && mState == STATE_PIE) { - setCenter(mCenterX, mCenterY); - layoutPie(); - } - } - - private void setCircle(int cx, int cy) { - mCircle.set(cx - mCircleSize, cy - mCircleSize, - cx + mCircleSize, cy + mCircleSize); - mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, - cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); - } - - public void drawFocus(Canvas canvas) { - if (mBlockFocus) { - return; - } - mFocusPaint.setStrokeWidth(mOuterStroke); - canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); - if (mState == STATE_PIE) { - return; - } - int color = mFocusPaint.getColor(); - if (mState == STATE_FINISHING) { - mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); - } - mFocusPaint.setStrokeWidth(mInnerStroke); - drawLine(canvas, mDialAngle, mFocusPaint); - drawLine(canvas, mDialAngle + 45, mFocusPaint); - drawLine(canvas, mDialAngle + 180, mFocusPaint); - drawLine(canvas, mDialAngle + 225, mFocusPaint); - canvas.save(); - // rotate the arc instead of its offset to better use framework's shape caching - canvas.rotate(mDialAngle, mFocusX, mFocusY); - canvas.drawArc(mDial, 0, 45, false, mFocusPaint); - canvas.drawArc(mDial, 180, 45, false, mFocusPaint); - canvas.restore(); - mFocusPaint.setColor(color); - } - - private void drawLine(Canvas canvas, int angle, Paint p) { - convertCart(angle, mCircleSize - mInnerOffset, mPoint1); - convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); - canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, - mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); - } - - private static void convertCart(int angle, int radius, Point out) { - double a = 2 * Math.PI * (angle % 360) / 360; - out.x = (int) (radius * Math.cos(a) + 0.5); - out.y = (int) (radius * Math.sin(a) + 0.5); - } - - @Override - public void showStart() { - if (mState == STATE_PIE) { - return; - } - cancelFocus(); - mStartAnimationAngle = 67; - int range = getRandomRange(); - startAnimation(SCALING_UP_TIME, - false, mStartAnimationAngle, mStartAnimationAngle + range); - mState = STATE_FOCUSING; - } - - @Override - public void showSuccess(boolean timeout) { - if (mState == STATE_FOCUSING) { - startAnimation(SCALING_DOWN_TIME, - timeout, mStartAnimationAngle); - mState = STATE_FINISHING; - mFocused = true; - } - } - - @Override - public void showFail(boolean timeout) { - if (mState == STATE_FOCUSING) { - startAnimation(SCALING_DOWN_TIME, - timeout, mStartAnimationAngle); - mState = STATE_FINISHING; - mFocused = false; - } - } - - private void cancelFocus() { - mFocusCancelled = true; - mOverlay.removeCallbacks(mDisappear); - if (mAnimation != null) { - mAnimation.cancel(); - } - mFocusCancelled = false; - mFocused = false; - mState = STATE_IDLE; - } - - @Override - public void clear() { - if (mState == STATE_PIE) { - return; - } - cancelFocus(); - mOverlay.post(mDisappear); - } - - private void startAnimation(long duration, boolean timeout, - float toScale) { - startAnimation(duration, timeout, mDialAngle, - toScale); - } - - private void startAnimation(long duration, boolean timeout, - float fromScale, float toScale) { - setVisible(true); - mAnimation.reset(); - mAnimation.setDuration(duration); - mAnimation.setScale(fromScale, toScale); - mAnimation.setAnimationListener(timeout ? mEndAction : null); - mOverlay.startAnimation(mAnimation); - update(); - } - - private class EndAction implements Animation.AnimationListener { - @Override - public void onAnimationEnd(Animation animation) { - // Keep the focus indicator for some time. - if (!mFocusCancelled) { - mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); - } - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - - @Override - public void onAnimationStart(Animation animation) { - } - } - - private class Disappear implements Runnable { - @Override - public void run() { - if (mState == STATE_PIE) { - return; - } - setVisible(false); - mFocusX = mCenterX; - mFocusY = mCenterY; - mState = STATE_IDLE; - setCircle(mFocusX, mFocusY); - mFocused = false; - } - } - - private class ScaleAnimation extends Animation { - private float mFrom = 1f; - private float mTo = 1f; - - public ScaleAnimation() { - setFillAfter(true); - } - - public void setScale(float from, float to) { - mFrom = from; - mTo = to; - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - mDialAngle = (int) (mFrom + (mTo - mFrom) * interpolatedTime); - } - } - - - private class LinearAnimation extends Animation { - private float mFrom; - private float mTo; - private float mValue; - - public LinearAnimation(float from, float to) { - setFillAfter(true); - setInterpolator(new LinearInterpolator()); - mFrom = from; - mTo = to; - } - - public float getValue() { - return mValue; - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - mValue = (mFrom + (mTo - mFrom) * interpolatedTime); - } - } - -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/mediapicker/camerafocus/README.txt b/src/com/android/messaging/ui/mediapicker/camerafocus/README.txt deleted file mode 100644 index ed4e783..0000000 --- a/src/com/android/messaging/ui/mediapicker/camerafocus/README.txt +++ /dev/null @@ -1,3 +0,0 @@ -The files in this package were copied from the android-4.4.4_r1 branch of ASOP from the folders -com/android/camera/ and com/android/camera/ui from files with the same name. Some modifications -have been made to remove unneeded features and adjust to our needs.
\ No newline at end of file diff --git a/src/com/android/messaging/ui/mediapicker/camerafocus/RenderOverlay.java b/src/com/android/messaging/ui/mediapicker/camerafocus/RenderOverlay.java deleted file mode 100644 index 95cddc4..0000000 --- a/src/com/android/messaging/ui/mediapicker/camerafocus/RenderOverlay.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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.camerafocus; - -import android.content.Context; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; - -import java.util.ArrayList; -import java.util.List; - -public class RenderOverlay extends FrameLayout { - - interface Renderer { - - public boolean handlesTouch(); - public boolean onTouchEvent(MotionEvent evt); - public void setOverlay(RenderOverlay overlay); - public void layout(int left, int top, int right, int bottom); - public void draw(Canvas canvas); - - } - - private RenderView mRenderView; - private List<Renderer> mClients; - - // reverse list of touch clients - private List<Renderer> mTouchClients; - private int[] mPosition = new int[2]; - - public RenderOverlay(Context context, AttributeSet attrs) { - super(context, attrs); - mRenderView = new RenderView(context); - addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - mClients = new ArrayList<Renderer>(10); - mTouchClients = new ArrayList<Renderer>(10); - setWillNotDraw(false); - - addRenderer(new PieRenderer(context)); - } - - public PieRenderer getPieRenderer() { - for (Renderer renderer : mClients) { - if (renderer instanceof PieRenderer) { - return (PieRenderer) renderer; - } - } - return null; - } - - public void addRenderer(Renderer renderer) { - mClients.add(renderer); - renderer.setOverlay(this); - if (renderer.handlesTouch()) { - mTouchClients.add(0, renderer); - } - renderer.layout(getLeft(), getTop(), getRight(), getBottom()); - } - - public void addRenderer(int pos, Renderer renderer) { - mClients.add(pos, renderer); - renderer.setOverlay(this); - renderer.layout(getLeft(), getTop(), getRight(), getBottom()); - } - - public void remove(Renderer renderer) { - mClients.remove(renderer); - renderer.setOverlay(null); - } - - public int getClientSize() { - return mClients.size(); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent m) { - return false; - } - - public boolean directDispatchTouch(MotionEvent m, Renderer target) { - mRenderView.setTouchTarget(target); - boolean res = super.dispatchTouchEvent(m); - mRenderView.setTouchTarget(null); - return res; - } - - private void adjustPosition() { - getLocationInWindow(mPosition); - } - - public int getWindowPositionX() { - return mPosition[0]; - } - - public int getWindowPositionY() { - return mPosition[1]; - } - - public void update() { - mRenderView.invalidate(); - } - - private class RenderView extends View { - - private Renderer mTouchTarget; - - public RenderView(Context context) { - super(context); - setWillNotDraw(false); - } - - public void setTouchTarget(Renderer target) { - mTouchTarget = target; - } - - @Override - public boolean onTouchEvent(MotionEvent evt) { - if (mTouchTarget != null) { - return mTouchTarget.onTouchEvent(evt); - } - if (mTouchClients != null) { - boolean res = false; - for (Renderer client : mTouchClients) { - res |= client.onTouchEvent(evt); - } - return res; - } - return false; - } - - @Override - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - adjustPosition(); - super.onLayout(changed, left, top, right, bottom); - if (mClients == null) { - return; - } - for (Renderer renderer : mClients) { - renderer.layout(left, top, right, bottom); - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - if (mClients == null) { - return; - } - boolean redraw = false; - for (Renderer renderer : mClients) { - renderer.draw(canvas); - redraw = redraw || ((OverlayRenderer) renderer).isVisible(); - } - if (redraw) { - invalidate(); - } - } - } - -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/photoviewer/BuglePhotoBitmapLoader.java b/src/com/android/messaging/ui/photoviewer/BuglePhotoBitmapLoader.java deleted file mode 100644 index d139a38..0000000 --- a/src/com/android/messaging/ui/photoviewer/BuglePhotoBitmapLoader.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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.photoviewer; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.support.rastermill.FrameSequenceDrawable; -import android.support.v4.content.AsyncTaskLoader; - -import com.android.ex.photo.PhotoViewController; -import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface; -import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; -import com.android.messaging.datamodel.media.ImageRequestDescriptor; -import com.android.messaging.datamodel.media.ImageResource; -import com.android.messaging.datamodel.media.MediaRequest; -import com.android.messaging.datamodel.media.MediaResourceManager; -import com.android.messaging.datamodel.media.UriImageRequestDescriptor; -import com.android.messaging.util.ImageUtils; - -/** - * Loader for the bitmap of a photo. - */ -public class BuglePhotoBitmapLoader extends AsyncTaskLoader<BitmapResult> - implements PhotoBitmapLoaderInterface { - private String mPhotoUri; - private ImageResource mImageResource; - // The drawable that is currently "in use" and being presented to the user. This drawable - // should never exist without the image resource backing it. - private Drawable mDrawable; - - public BuglePhotoBitmapLoader(Context context, String photoUri) { - super(context); - mPhotoUri = photoUri; - } - - @Override - public void setPhotoUri(String photoUri) { - mPhotoUri = photoUri; - } - - @Override - public BitmapResult loadInBackground() { - final BitmapResult result = new BitmapResult(); - final Context context = getContext(); - if (context != null && mPhotoUri != null) { - final ImageRequestDescriptor descriptor = - new UriImageRequestDescriptor(Uri.parse(mPhotoUri), - PhotoViewController.sMaxPhotoSize, PhotoViewController.sMaxPhotoSize, - true /* allowCompression */, false /* isStatic */, - false /* cropToCircle */, - ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */, - ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */); - final MediaRequest<ImageResource> imageRequest = - descriptor.buildSyncMediaRequest(context); - final ImageResource imageResource = - MediaResourceManager.get().requestMediaResourceSync(imageRequest); - if (imageResource != null) { - setImageResource(imageResource); - result.status = BitmapResult.STATUS_SUCCESS; - result.drawable = mImageResource.getDrawable(context.getResources()); - } else { - releaseImageResource(); - result.status = BitmapResult.STATUS_EXCEPTION; - } - } else { - result.status = BitmapResult.STATUS_EXCEPTION; - } - return result; - } - - /** - * Called when there is new data to deliver to the client. The super class will take care of - * delivering it; the implementation here just adds a little more logic. - */ - @Override - public void deliverResult(BitmapResult result) { - final Drawable drawable = result != null ? result.drawable : null; - if (isReset()) { - // An async query came in while the loader is stopped. We don't need the result. - releaseDrawable(drawable); - return; - } - - // We are now going to display this drawable so set to mDrawable - mDrawable = drawable; - - if (isStarted()) { - // If the Loader is currently started, we can immediately deliver its results. - super.deliverResult(result); - } - } - - /** - * Handles a request to start the Loader. - */ - @Override - protected void onStartLoading() { - if (mDrawable != null) { - // If we currently have a result available, deliver it - // immediately. - final BitmapResult result = new BitmapResult(); - result.status = BitmapResult.STATUS_SUCCESS; - result.drawable = mDrawable; - deliverResult(result); - } - - if (takeContentChanged() || (mImageResource == null)) { - // If the data has changed since the last time it was loaded - // or is not currently available, start a load. - forceLoad(); - } - } - - /** - * Handles a request to stop the Loader. - */ - @Override - protected void onStopLoading() { - // Attempt to cancel the current load task if possible. - cancelLoad(); - } - - /** - * Handles a request to cancel a load. - */ - @Override - public void onCanceled(BitmapResult result) { - super.onCanceled(result); - - // At this point we can release the resources associated with 'drawable' if needed. - if (result != null) { - releaseDrawable(result.drawable); - } - } - - /** - * Handles a request to completely reset the Loader. - */ - @Override - protected void onReset() { - super.onReset(); - - // Ensure the loader is stopped - onStopLoading(); - - releaseImageResource(); - } - - private void releaseDrawable(Drawable drawable) { - if (drawable != null && drawable instanceof FrameSequenceDrawable - && !((FrameSequenceDrawable) drawable).isDestroyed()) { - ((FrameSequenceDrawable) drawable).destroy(); - } - - } - - private void setImageResource(final ImageResource resource) { - if (mImageResource != resource) { - // Clear out any information for what is currently used - releaseImageResource(); - mImageResource = resource; - // No need to add ref since a ref is already reserved as a result of - // requestMediaResourceSync. - } - } - - private void releaseImageResource() { - // If we are getting rid of the imageResource backing the drawable, we must also - // destroy the drawable before releasing it. - releaseDrawable(mDrawable); - mDrawable = null; - - if (mImageResource != null) { - mImageResource.release(); - } - mImageResource = null; - } -}
\ No newline at end of file diff --git a/src/com/android/messaging/ui/photoviewer/BuglePhotoPageAdapter.java b/src/com/android/messaging/ui/photoviewer/BuglePhotoPageAdapter.java deleted file mode 100644 index 52498c7..0000000 --- a/src/com/android/messaging/ui/photoviewer/BuglePhotoPageAdapter.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.photoviewer; - -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.support.v4.app.FragmentManager; - -import com.android.ex.photo.adapters.PhotoPagerAdapter; -import com.android.ex.photo.fragments.PhotoViewFragment; - -public class BuglePhotoPageAdapter extends PhotoPagerAdapter { - - public BuglePhotoPageAdapter(Context context, FragmentManager fm, Cursor c, float maxScale, - boolean thumbsFullScreen) { - super(context, fm, c, maxScale, thumbsFullScreen); - } - - @Override - protected PhotoViewFragment createPhotoViewFragment(Intent intent, int position, - boolean onlyShowSpinner) { - return BuglePhotoViewFragment.newInstance(intent, position, onlyShowSpinner); - } -} diff --git a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewActivity.java b/src/com/android/messaging/ui/photoviewer/BuglePhotoViewActivity.java deleted file mode 100644 index 1924a96..0000000 --- a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewActivity.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.photoviewer; - -import com.android.ex.photo.PhotoViewActivity; -import com.android.ex.photo.PhotoViewController; - -/** - * Activity to display the conversation images in full-screen. Most of the customization is in - * {@link BuglePhotoViewController}. - */ -public class BuglePhotoViewActivity extends PhotoViewActivity { - @Override - public PhotoViewController createController() { - return new BuglePhotoViewController(this); - } -} diff --git a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewController.java b/src/com/android/messaging/ui/photoviewer/BuglePhotoViewController.java deleted file mode 100644 index eb39886..0000000 --- a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewController.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.photoviewer; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.content.Loader; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.ShareActionProvider; -import android.widget.Toast; - -import com.android.ex.photo.PhotoViewController; -import com.android.ex.photo.adapters.PhotoPagerAdapter; -import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; -import com.android.messaging.R; -import com.android.messaging.datamodel.ConversationImagePartsView.PhotoViewQuery; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.ui.conversation.ConversationFragment; -import com.android.messaging.util.Dates; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.OsUtil; - -/** - * Customizations for the photoviewer to display conversation images in full screen. - */ -public class BuglePhotoViewController extends PhotoViewController { - private ShareActionProvider mShareActionProvider; - private MenuItem mShareItem; - private MenuItem mSaveItem; - - public BuglePhotoViewController(final ActivityInterface activity) { - super(activity); - } - - @Override - public Loader<BitmapResult> onCreateBitmapLoader( - final int id, final Bundle args, final String uri) { - switch (id) { - case BITMAP_LOADER_AVATAR: - case BITMAP_LOADER_THUMBNAIL: - case BITMAP_LOADER_PHOTO: - return new BuglePhotoBitmapLoader(getActivity().getContext(), uri); - default: - LogUtil.e(LogUtil.BUGLE_TAG, - "Photoviewer unable to open bitmap loader with unknown id: " + id); - return null; - } - } - - @Override - public void updateActionBar() { - final Cursor cursor = getCursorAtProperPosition(); - - if (mSaveItem == null || cursor == null) { - // Load not finished, called from framework code before ready - return; - } - // Show the name as the title - mActionBarTitle = cursor.getString(PhotoViewQuery.INDEX_SENDER_FULL_NAME); - if (TextUtils.isEmpty(mActionBarTitle)) { - // If the name is not known, fall back to the phone number - mActionBarTitle = cursor.getString(PhotoViewQuery.INDEX_DISPLAY_DESTINATION); - } - - // Show the timestamp as the subtitle - final long receivedTimestamp = cursor.getLong(PhotoViewQuery.INDEX_RECEIVED_TIMESTAMP); - mActionBarSubtitle = Dates.getMessageTimeString(receivedTimestamp).toString(); - - setActionBarTitles(getActivity().getActionBarInterface()); - mSaveItem.setVisible(!isTempFile()); - - updateShareActionProvider(); - } - - private void updateShareActionProvider() { - final PhotoPagerAdapter adapter = getAdapter(); - final Cursor cursor = getCursorAtProperPosition(); - if (mShareActionProvider == null || mShareItem == null || adapter == null || - cursor == null) { - // Not enough stuff loaded to update the share action - return; - } - final String photoUri = adapter.getPhotoUri(cursor); - if (isTempFile()) { - mShareItem.setVisible(false); - return; - } - final String contentType = adapter.getContentType(cursor); - - final Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.setType(contentType); - shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(photoUri)); - mShareActionProvider.setShareIntent(shareIntent); - mShareItem.setVisible(true); - } - - /** - * Checks whether the current photo is a temp file. A temp file can be deleted at any time, so - * we need to disable share and save options because the file may no longer be there. - */ - private boolean isTempFile() { - final Cursor cursor = getCursorAtProperPosition(); - final Uri photoUri = Uri.parse(getAdapter().getPhotoUri(cursor)); - return MediaScratchFileProvider.isMediaScratchSpaceUri(photoUri); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - ((Activity) getActivity()).getMenuInflater().inflate(R.menu.photo_view_menu, menu); - - // Get the ShareActionProvider - mShareItem = menu.findItem(R.id.action_share); - mShareActionProvider = (ShareActionProvider) mShareItem.getActionProvider(); - updateShareActionProvider(); - - mSaveItem = menu.findItem(R.id.action_save); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(final Menu menu) { - return !mIsEmpty; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - if (item.getItemId() == R.id.action_save) { - if (OsUtil.hasStoragePermission()) { - final PhotoPagerAdapter adapter = getAdapter(); - final Cursor cursor = getCursorAtProperPosition(); - if (cursor == null) { - final Context context = getActivity().getContext(); - final String error = context.getResources().getQuantityString( - R.plurals.attachment_save_error, 1, 1); - Toast.makeText(context, error, Toast.LENGTH_SHORT).show(); - return true; - } - final String photoUri = adapter.getPhotoUri(cursor); - new ConversationFragment.SaveAttachmentTask(((Activity) getActivity()), - Uri.parse(photoUri), adapter.getContentType(cursor)).executeOnThreadPool(); - } else { - ((Activity)getActivity()).requestPermissions( - new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0); - } - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - @Override - public PhotoPagerAdapter createPhotoPagerAdapter(final Context context, - final FragmentManager fm, final Cursor c, final float maxScale) { - return new BuglePhotoPageAdapter(context, fm, c, maxScale, mDisplayThumbsFullScreen); - } -} diff --git a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewFragment.java b/src/com/android/messaging/ui/photoviewer/BuglePhotoViewFragment.java deleted file mode 100644 index 698c510..0000000 --- a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewFragment.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.photoviewer; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.support.rastermill.FrameSequenceDrawable; -import android.support.v4.content.Loader; - -import com.android.ex.photo.PhotoViewCallbacks; -import com.android.ex.photo.fragments.PhotoViewFragment; -import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; - -public class BuglePhotoViewFragment extends PhotoViewFragment { - - /** Public no-arg constructor for allowing the framework to handle orientation changes */ - public BuglePhotoViewFragment() { - // Do nothing. - } - - public static PhotoViewFragment newInstance(Intent intent, int position, - boolean onlyShowSpinner) { - final PhotoViewFragment f = new BuglePhotoViewFragment(); - initializeArguments(intent, position, onlyShowSpinner, f); - return f; - } - - @Override - public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) { - super.onLoadFinished(loader, result); - // Need to check for the first time when we load the photos - if (PhotoViewCallbacks.BITMAP_LOADER_PHOTO == loader.getId() - && result.status == BitmapResult.STATUS_SUCCESS - && mCallback.isFragmentActive(this)) { - startGif(); - } - } - - @Override - public void onResume() { - super.onResume(); - startGif(); - } - - @Override - public void onPause() { - stopGif(); - super.onPause(); - } - - @Override - public void onViewActivated() { - super.onViewActivated(); - startGif(); - } - - @Override - public void resetViews() { - super.resetViews(); - stopGif(); - } - - private void stopGif() { - final Drawable drawable = getDrawable(); - if (drawable != null && drawable instanceof FrameSequenceDrawable) { - ((FrameSequenceDrawable) drawable).stop(); - } - } - - private void startGif() { - final Drawable drawable = getDrawable(); - if (drawable != null && drawable instanceof FrameSequenceDrawable) { - ((FrameSequenceDrawable) drawable).start(); - } - } -} |