Revert "Initial checkin of AOSP Messaging app."
This reverts commit 461a34b466.
Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user