Revert "Initial checkin of AOSP Messaging app."

This reverts commit 461a34b466.

Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
Mike Dodd
2015-08-12 18:48:39 +00:00
parent 461a34b466
commit ef8c7abbcf
1645 changed files with 0 additions and 186271 deletions
@@ -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();
}
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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;
}
}
}
@@ -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();
}
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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);
}
@@ -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());
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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";
}
@@ -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();
}
};
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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();
}
@@ -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);
}
@@ -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();
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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;
}
}
}
}
@@ -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();
}
}
@@ -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));
}
}
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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();
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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();
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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;
}
}
-314
View File
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
-378
View File
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
}
}
@@ -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;
}
}
@@ -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;
}
}
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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;
}
}
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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];
}
};
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
});
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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);
}
}
File diff suppressed because it is too large Load Diff
@@ -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();
}
}
}
@@ -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;
}
}
}
@@ -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);
}
}
}
@@ -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();
}
}
File diff suppressed because it is too large Load Diff
@@ -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();
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
}
}
@@ -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.
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More