Revert "Initial checkin of AOSP Messaging app."
This reverts commit 461a34b466.
Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* Keeps track of the speech level as last observed by the recognition
|
||||
* engine as microphone data flows through it. Can be polled by the UI to
|
||||
* animate its views.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public class AudioLevelSource {
|
||||
private volatile int mSpeechLevel;
|
||||
private volatile Listener mListener;
|
||||
|
||||
public static final int LEVEL_UNKNOWN = -1;
|
||||
|
||||
public interface Listener {
|
||||
void onSpeechLevel(int speechLevel);
|
||||
}
|
||||
|
||||
public void setSpeechLevel(int speechLevel) {
|
||||
Preconditions.checkArgument(speechLevel >= 0 && speechLevel <= 100 ||
|
||||
speechLevel == LEVEL_UNKNOWN);
|
||||
mSpeechLevel = speechLevel;
|
||||
maybeNotify();
|
||||
}
|
||||
|
||||
public int getSpeechLevel() {
|
||||
return mSpeechLevel;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
setSpeechLevel(LEVEL_UNKNOWN);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return mSpeechLevel > 0;
|
||||
}
|
||||
|
||||
private void maybeNotify() {
|
||||
final Listener l = mListener;
|
||||
if (l != null) {
|
||||
l.onSpeechLevel(mSpeechLevel);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void setListener(Listener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public synchronized void clearListener(Listener listener) {
|
||||
if (mListener == listener) {
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
/**
|
||||
* Chooser which allows the user to record audio
|
||||
*/
|
||||
class AudioMediaChooser extends MediaChooser implements
|
||||
AudioRecordView.HostInterface {
|
||||
private View mEnabledView;
|
||||
private View mMissingPermissionView;
|
||||
|
||||
AudioMediaChooser(final MediaPicker mediaPicker) {
|
||||
super(mediaPicker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSupportedMediaTypes() {
|
||||
return MediaPicker.MEDIA_TYPE_AUDIO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconResource() {
|
||||
return R.drawable.ic_audio_light;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconDescriptionResource() {
|
||||
return R.string.mediapicker_audioChooserDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioRecorded(final MessagePartData item) {
|
||||
mMediaPicker.dispatchItemsSelected(item, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThemeColor(final int color) {
|
||||
if (mView != null) {
|
||||
((AudioRecordView) mView).setThemeColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View createView(final ViewGroup container) {
|
||||
final LayoutInflater inflater = getLayoutInflater();
|
||||
final AudioRecordView view = (AudioRecordView) inflater.inflate(
|
||||
R.layout.mediapicker_audio_chooser,
|
||||
container /* root */,
|
||||
false /* attachToRoot */);
|
||||
view.setHostInterface(this);
|
||||
view.setThemeColor(mMediaPicker.getConversationThemeColor());
|
||||
mEnabledView = view.findViewById(R.id.mediapicker_enabled);
|
||||
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getActionBarTitleResId() {
|
||||
return R.string.mediapicker_audio_title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHandlingTouch() {
|
||||
// Whenever the user is in the process of recording audio, we want to allow the user
|
||||
// to move the finger within the panel without interpreting that as dragging the media
|
||||
// picker panel.
|
||||
return ((AudioRecordView) mView).shouldHandleTouch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopTouchHandling() {
|
||||
((AudioRecordView) mView).stopTouchHandling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (mView != null) {
|
||||
((AudioRecordView) mView).onPause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSelected(final boolean selected) {
|
||||
super.setSelected(selected);
|
||||
if (selected && !OsUtil.hasRecordAudioPermission()) {
|
||||
requestRecordAudioPermission();
|
||||
}
|
||||
}
|
||||
|
||||
private void requestRecordAudioPermission() {
|
||||
mMediaPicker.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO },
|
||||
MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequestPermissionsResult(
|
||||
final int requestCode, final String permissions[], final int[] grantResults) {
|
||||
if (requestCode == MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
|
||||
final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
mEnabledView.setVisibility(permissionGranted ? View.VISIBLE : View.GONE);
|
||||
mMissingPermissionView.setVisibility(permissionGranted ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.media.MediaRecorder;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
|
||||
import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContentType;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.MediaUtil;
|
||||
import com.android.messaging.util.MediaUtil.OnCompletionListener;
|
||||
import com.android.messaging.util.SafeAsyncTask;
|
||||
import com.android.messaging.util.ThreadUtil;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* Hosts an audio recorder with tap and hold to record functionality.
|
||||
*/
|
||||
public class AudioRecordView extends FrameLayout implements
|
||||
MediaRecorder.OnErrorListener,
|
||||
MediaRecorder.OnInfoListener {
|
||||
/**
|
||||
* An interface that communicates with the hosted AudioRecordView.
|
||||
*/
|
||||
public interface HostInterface extends DraftMessageSubscriptionDataProvider {
|
||||
void onAudioRecorded(final MessagePartData item);
|
||||
}
|
||||
|
||||
/** The initial state, the user may press and hold to start recording */
|
||||
private static final int MODE_IDLE = 1;
|
||||
|
||||
/** The user has pressed the record button and we are playing the sound indicating the
|
||||
* start of recording session. Don't record yet since we don't want the beeping sound
|
||||
* to get into the recording. */
|
||||
private static final int MODE_STARTING = 2;
|
||||
|
||||
/** When the user is actively recording */
|
||||
private static final int MODE_RECORDING = 3;
|
||||
|
||||
/** When the user has finished recording, we need to record for some additional time. */
|
||||
private static final int MODE_STOPPING = 4;
|
||||
|
||||
// Bug: 16020175: The framework's MediaRecorder would cut off the ending portion of the
|
||||
// recorded audio by about half a second. To mitigate this issue, we continue the recording
|
||||
// for some extra time before stopping it.
|
||||
private static final int AUDIO_RECORD_ENDING_BUFFER_MILLIS = 500;
|
||||
|
||||
/**
|
||||
* The minimum duration of any recording. Below this threshold, it will be treated as if the
|
||||
* user clicked the record button and inform the user to tap and hold to record.
|
||||
*/
|
||||
private static final int AUDIO_RECORD_MINIMUM_DURATION_MILLIS = 300;
|
||||
|
||||
// For accessibility, the touchable record button is bigger than the record button visual.
|
||||
private ImageView mRecordButtonVisual;
|
||||
private View mRecordButton;
|
||||
private SoundLevels mSoundLevels;
|
||||
private TextView mHintTextView;
|
||||
private PausableChronometer mTimerTextView;
|
||||
private LevelTrackingMediaRecorder mMediaRecorder;
|
||||
private long mAudioRecordStartTimeMillis;
|
||||
|
||||
private int mCurrentMode = MODE_IDLE;
|
||||
private HostInterface mHostInterface;
|
||||
private int mThemeColor;
|
||||
|
||||
public AudioRecordView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mMediaRecorder = new LevelTrackingMediaRecorder();
|
||||
}
|
||||
|
||||
public void setHostInterface(final HostInterface hostInterface) {
|
||||
mHostInterface = hostInterface;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void testSetMediaRecorder(final LevelTrackingMediaRecorder recorder) {
|
||||
mMediaRecorder = recorder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mSoundLevels = (SoundLevels) findViewById(R.id.sound_levels);
|
||||
mRecordButtonVisual = (ImageView) findViewById(R.id.record_button_visual);
|
||||
mRecordButton = findViewById(R.id.record_button);
|
||||
mHintTextView = (TextView) findViewById(R.id.hint_text);
|
||||
mTimerTextView = (PausableChronometer) findViewById(R.id.timer_text);
|
||||
mSoundLevels.setLevelSource(mMediaRecorder.getLevelSource());
|
||||
mRecordButton.setOnTouchListener(new OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(final View v, final MotionEvent event) {
|
||||
final int action = event.getActionMasked();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
onRecordButtonTouchDown();
|
||||
|
||||
// Don't let the record button handle the down event to let it fall through
|
||||
// so that we can handle it for the entire panel in onTouchEvent(). This is
|
||||
// done so that: 1) the user taps on the record button to start recording
|
||||
// 2) the entire panel owns the touch event so we'd keep recording even
|
||||
// if the user moves outside the button region.
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
final int action = event.getActionMasked();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
return shouldHandleTouch();
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
return onRecordButtonTouchUp();
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void onPause() {
|
||||
// The conversation draft cannot take any updates when it's paused. Therefore, forcibly
|
||||
// stop recording on pause.
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
private boolean isRecording() {
|
||||
return mMediaRecorder.isRecording() && mCurrentMode == MODE_RECORDING;
|
||||
}
|
||||
|
||||
public boolean shouldHandleTouch() {
|
||||
return mCurrentMode != MODE_IDLE;
|
||||
}
|
||||
|
||||
public void stopTouchHandling() {
|
||||
setMode(MODE_IDLE);
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
private void setMode(final int mode) {
|
||||
if (mCurrentMode != mode) {
|
||||
mCurrentMode = mode;
|
||||
updateVisualState();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateVisualState() {
|
||||
switch (mCurrentMode) {
|
||||
case MODE_IDLE:
|
||||
mHintTextView.setVisibility(VISIBLE);
|
||||
mHintTextView.setTypeface(null, Typeface.NORMAL);
|
||||
mTimerTextView.setVisibility(GONE);
|
||||
mSoundLevels.setEnabled(false);
|
||||
mTimerTextView.stop();
|
||||
break;
|
||||
|
||||
case MODE_RECORDING:
|
||||
case MODE_STOPPING:
|
||||
mHintTextView.setVisibility(GONE);
|
||||
mTimerTextView.setVisibility(VISIBLE);
|
||||
mSoundLevels.setEnabled(true);
|
||||
mTimerTextView.restart();
|
||||
break;
|
||||
|
||||
case MODE_STARTING:
|
||||
break; // No-Op.
|
||||
|
||||
default:
|
||||
Assert.fail("invalid mode for AudioRecordView!");
|
||||
break;
|
||||
}
|
||||
updateRecordButtonAppearance();
|
||||
}
|
||||
|
||||
public void setThemeColor(final int color) {
|
||||
mThemeColor = color;
|
||||
updateRecordButtonAppearance();
|
||||
}
|
||||
|
||||
private void updateRecordButtonAppearance() {
|
||||
final Drawable foregroundDrawable = getResources().getDrawable(R.drawable.ic_mp_audio_mic);
|
||||
final GradientDrawable backgroundDrawable = ((GradientDrawable) getResources()
|
||||
.getDrawable(R.drawable.audio_record_control_button_background));
|
||||
if (isRecording()) {
|
||||
foregroundDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP);
|
||||
backgroundDrawable.setColor(mThemeColor);
|
||||
} else {
|
||||
foregroundDrawable.setColorFilter(mThemeColor, PorterDuff.Mode.SRC_ATOP);
|
||||
backgroundDrawable.setColor(Color.WHITE);
|
||||
}
|
||||
mRecordButtonVisual.setImageDrawable(foregroundDrawable);
|
||||
mRecordButtonVisual.setBackground(backgroundDrawable);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean onRecordButtonTouchDown() {
|
||||
if (!mMediaRecorder.isRecording() && mCurrentMode == MODE_IDLE) {
|
||||
setMode(MODE_STARTING);
|
||||
playAudioStartSound(new OnCompletionListener() {
|
||||
@Override
|
||||
public void onCompletion() {
|
||||
// Double-check the current mode before recording since the user may have
|
||||
// lifted finger from the button before the beeping sound is played through.
|
||||
final int maxSize = MmsConfig.get(mHostInterface.getConversationSelfSubId())
|
||||
.getMaxMessageSize();
|
||||
if (mCurrentMode == MODE_STARTING &&
|
||||
mMediaRecorder.startRecording(AudioRecordView.this,
|
||||
AudioRecordView.this, maxSize)) {
|
||||
setMode(MODE_RECORDING);
|
||||
}
|
||||
}
|
||||
});
|
||||
mAudioRecordStartTimeMillis = System.currentTimeMillis();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean onRecordButtonTouchUp() {
|
||||
if (System.currentTimeMillis() - mAudioRecordStartTimeMillis <
|
||||
AUDIO_RECORD_MINIMUM_DURATION_MILLIS) {
|
||||
// The recording is too short, bolden the hint text to instruct the user to
|
||||
// "tap+hold" to record audio.
|
||||
final Uri outputUri = stopRecording();
|
||||
if (outputUri != null) {
|
||||
SafeAsyncTask.executeOnThreadPool(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Factory.get().getApplicationContext().getContentResolver().delete(
|
||||
outputUri, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
setMode(MODE_IDLE);
|
||||
mHintTextView.setTypeface(null, Typeface.BOLD);
|
||||
} else if (isRecording()) {
|
||||
// Record for some extra time to ensure the ending part is saved.
|
||||
setMode(MODE_STOPPING);
|
||||
ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onFinishedRecording();
|
||||
}
|
||||
}, AUDIO_RECORD_ENDING_BUFFER_MILLIS);
|
||||
} else {
|
||||
setMode(MODE_IDLE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Uri stopRecording() {
|
||||
if (mMediaRecorder.isRecording()) {
|
||||
return mMediaRecorder.stopRecording();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override // From MediaRecorder.OnInfoListener
|
||||
public void onInfo(final MediaRecorder mr, final int what, final int extra) {
|
||||
if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
|
||||
// Max size reached. Finish recording immediately.
|
||||
LogUtil.i(LogUtil.BUGLE_TAG, "Max size reached while recording audio");
|
||||
onFinishedRecording();
|
||||
} else {
|
||||
// These are unknown errors.
|
||||
onErrorWhileRecording(what, extra);
|
||||
}
|
||||
}
|
||||
|
||||
@Override // From MediaRecorder.OnErrorListener
|
||||
public void onError(final MediaRecorder mr, final int what, final int extra) {
|
||||
onErrorWhileRecording(what, extra);
|
||||
}
|
||||
|
||||
private void onErrorWhileRecording(final int what, final int extra) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Error occurred during audio recording what=" + what +
|
||||
", extra=" + extra);
|
||||
UiUtils.showToastAtBottom(R.string.audio_recording_error);
|
||||
setMode(MODE_IDLE);
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
private void onFinishedRecording() {
|
||||
final Uri outputUri = stopRecording();
|
||||
if (outputUri != null) {
|
||||
final Rect startRect = new Rect();
|
||||
mRecordButtonVisual.getGlobalVisibleRect(startRect);
|
||||
final MediaPickerMessagePartData audioItem =
|
||||
new MediaPickerMessagePartData(startRect,
|
||||
ContentType.AUDIO_3GPP, outputUri, 0, 0);
|
||||
mHostInterface.onAudioRecorded(audioItem);
|
||||
}
|
||||
playAudioEndSound();
|
||||
setMode(MODE_IDLE);
|
||||
}
|
||||
|
||||
private void playAudioStartSound(final OnCompletionListener completionListener) {
|
||||
MediaUtil.get().playSound(getContext(), R.raw.audio_initiate, completionListener);
|
||||
}
|
||||
|
||||
private void playAudioEndSound() {
|
||||
MediaUtil.get().playSound(getContext(), R.raw.audio_end, null);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,481 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.widget.Chronometer;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
|
||||
import com.android.messaging.ui.mediapicker.CameraManager.MediaCallback;
|
||||
import com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
|
||||
/**
|
||||
* Chooser which allows the user to take pictures or video without leaving the current app/activity
|
||||
*/
|
||||
class CameraMediaChooser extends MediaChooser implements
|
||||
CameraManager.CameraManagerListener {
|
||||
private CameraPreview.CameraPreviewHost mCameraPreviewHost;
|
||||
private ImageButton mFullScreenButton;
|
||||
private ImageButton mSwapCameraButton;
|
||||
private ImageButton mSwapModeButton;
|
||||
private ImageButton mCaptureButton;
|
||||
private ImageButton mCancelVideoButton;
|
||||
private Chronometer mVideoCounter;
|
||||
private boolean mVideoCancelled;
|
||||
private int mErrorToast;
|
||||
private View mEnabledView;
|
||||
private View mMissingPermissionView;
|
||||
|
||||
CameraMediaChooser(final MediaPicker mediaPicker) {
|
||||
super(mediaPicker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSupportedMediaTypes() {
|
||||
if (CameraManager.get().hasAnyCamera()) {
|
||||
return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO;
|
||||
} else {
|
||||
return MediaPicker.MEDIA_TYPE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View destroyView() {
|
||||
CameraManager.get().closeCamera();
|
||||
CameraManager.get().setListener(null);
|
||||
CameraManager.get().setSubscriptionDataProvider(null);
|
||||
return super.destroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View createView(final ViewGroup container) {
|
||||
CameraManager.get().setListener(this);
|
||||
CameraManager.get().setSubscriptionDataProvider(this);
|
||||
CameraManager.get().setVideoMode(false);
|
||||
final LayoutInflater inflater = getLayoutInflater();
|
||||
final CameraMediaChooserView view = (CameraMediaChooserView) inflater.inflate(
|
||||
R.layout.mediapicker_camera_chooser,
|
||||
container /* root */,
|
||||
false /* attachToRoot */);
|
||||
mCameraPreviewHost = (CameraPreview.CameraPreviewHost) view.findViewById(
|
||||
R.id.camera_preview);
|
||||
mCameraPreviewHost.getView().setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(final View view, final MotionEvent motionEvent) {
|
||||
if (CameraManager.get().isVideoMode()) {
|
||||
// Prevent the swipe down in video mode because video is always captured in
|
||||
// full screen
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
final View shutterVisual = view.findViewById(R.id.camera_shutter_visual);
|
||||
|
||||
mFullScreenButton = (ImageButton) view.findViewById(R.id.camera_fullScreen_button);
|
||||
mFullScreenButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
mMediaPicker.setFullScreen(true);
|
||||
}
|
||||
});
|
||||
|
||||
mSwapCameraButton = (ImageButton) view.findViewById(R.id.camera_swapCamera_button);
|
||||
mSwapCameraButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
CameraManager.get().swapCamera();
|
||||
}
|
||||
});
|
||||
|
||||
mCaptureButton = (ImageButton) view.findViewById(R.id.camera_capture_button);
|
||||
mCaptureButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
final float heightPercent = Math.min(mMediaPicker.getViewPager().getHeight() /
|
||||
(float) mCameraPreviewHost.getView().getHeight(), 1);
|
||||
|
||||
if (CameraManager.get().isRecording()) {
|
||||
CameraManager.get().stopVideo();
|
||||
} else {
|
||||
final CameraManager.MediaCallback callback = new CameraManager.MediaCallback() {
|
||||
@Override
|
||||
public void onMediaReady(
|
||||
final Uri uriToVideo, final String contentType,
|
||||
final int width, final int height) {
|
||||
mVideoCounter.stop();
|
||||
if (mVideoCancelled || uriToVideo == null) {
|
||||
mVideoCancelled = false;
|
||||
} else {
|
||||
final Rect startRect = new Rect();
|
||||
// It's possible to throw out the chooser while taking the
|
||||
// picture/video. In that case, still use the attachment, just
|
||||
// skip the startRect
|
||||
if (mView != null) {
|
||||
mView.getGlobalVisibleRect(startRect);
|
||||
}
|
||||
mMediaPicker.dispatchItemsSelected(
|
||||
new MediaPickerMessagePartData(startRect, contentType,
|
||||
uriToVideo, width, height),
|
||||
true /* dismissMediaPicker */);
|
||||
}
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaFailed(final Exception exception) {
|
||||
UiUtils.showToastAtBottom(R.string.camera_media_failure);
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaInfo(final int what) {
|
||||
if (what == MediaCallback.MEDIA_NO_DATA) {
|
||||
UiUtils.showToastAtBottom(R.string.camera_media_failure);
|
||||
}
|
||||
updateViewState();
|
||||
}
|
||||
};
|
||||
if (CameraManager.get().isVideoMode()) {
|
||||
CameraManager.get().startVideo(callback);
|
||||
mVideoCounter.setBase(SystemClock.elapsedRealtime());
|
||||
mVideoCounter.start();
|
||||
updateViewState();
|
||||
} else {
|
||||
showShutterEffect(shutterVisual);
|
||||
CameraManager.get().takePicture(heightPercent, callback);
|
||||
updateViewState();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mSwapModeButton = (ImageButton) view.findViewById(R.id.camera_swap_mode_button);
|
||||
mSwapModeButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final boolean isSwitchingToVideo = !CameraManager.get().isVideoMode();
|
||||
if (isSwitchingToVideo && !OsUtil.hasRecordAudioPermission()) {
|
||||
requestRecordAudioPermission();
|
||||
} else {
|
||||
onSwapMode();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mCancelVideoButton = (ImageButton) view.findViewById(R.id.camera_cancel_button);
|
||||
mCancelVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
mVideoCancelled = true;
|
||||
CameraManager.get().stopVideo();
|
||||
mMediaPicker.dismiss(true);
|
||||
}
|
||||
});
|
||||
|
||||
mVideoCounter = (Chronometer) view.findViewById(R.id.camera_video_counter);
|
||||
|
||||
CameraManager.get().setRenderOverlay((RenderOverlay) view.findViewById(R.id.focus_visual));
|
||||
|
||||
mEnabledView = view.findViewById(R.id.mediapicker_enabled);
|
||||
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
|
||||
|
||||
// Must set mView before calling updateViewState because it operates on mView
|
||||
mView = view;
|
||||
updateViewState();
|
||||
updateForPermissionState(CameraManager.hasCameraPermission());
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconResource() {
|
||||
return R.drawable.ic_camera_light;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconDescriptionResource() {
|
||||
return R.string.mediapicker_cameraChooserDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the view when entering or leaving full-screen camera mode
|
||||
* @param fullScreen
|
||||
*/
|
||||
@Override
|
||||
void onFullScreenChanged(final boolean fullScreen) {
|
||||
super.onFullScreenChanged(fullScreen);
|
||||
if (!fullScreen && CameraManager.get().isVideoMode()) {
|
||||
CameraManager.get().setVideoMode(false);
|
||||
}
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the control to a default state when it is opened / closed
|
||||
* @param open True if the control is opened
|
||||
*/
|
||||
@Override
|
||||
void onOpenedChanged(final boolean open) {
|
||||
super.onOpenedChanged(open);
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSelected(final boolean selected) {
|
||||
super.setSelected(selected);
|
||||
if (selected) {
|
||||
if (CameraManager.hasCameraPermission()) {
|
||||
// If an error occurred before the chooser was selected, show it now
|
||||
showErrorToastIfNeeded();
|
||||
} else {
|
||||
requestCameraPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requestCameraPermission() {
|
||||
mMediaPicker.requestPermissions(new String[] { Manifest.permission.CAMERA },
|
||||
MediaPicker.CAMERA_PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
private void requestRecordAudioPermission() {
|
||||
mMediaPicker.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO },
|
||||
MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequestPermissionsResult(
|
||||
final int requestCode, final String permissions[], final int[] grantResults) {
|
||||
if (requestCode == MediaPicker.CAMERA_PERMISSION_REQUEST_CODE) {
|
||||
final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
updateForPermissionState(permissionGranted);
|
||||
if (permissionGranted) {
|
||||
mCameraPreviewHost.onCameraPermissionGranted();
|
||||
}
|
||||
} else if (requestCode == MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
|
||||
Assert.isFalse(CameraManager.get().isVideoMode());
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// Switch to video mode
|
||||
onSwapMode();
|
||||
} else {
|
||||
// Stay in still-photo mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateForPermissionState(final boolean granted) {
|
||||
// onRequestPermissionsResult can sometimes get called before createView().
|
||||
if (mEnabledView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mEnabledView.setVisibility(granted ? View.VISIBLE : View.GONE);
|
||||
mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSwipeDown() {
|
||||
if (CameraManager.get().isVideoMode()) {
|
||||
return true;
|
||||
}
|
||||
return super.canSwipeDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an error from the camera manager by showing the appropriate error message to the user
|
||||
* @param errorCode One of the CameraManager.ERROR_* constants
|
||||
* @param e The exception which caused the error, if any
|
||||
*/
|
||||
@Override
|
||||
public void onCameraError(final int errorCode, final Exception e) {
|
||||
switch (errorCode) {
|
||||
case CameraManager.ERROR_OPENING_CAMERA:
|
||||
case CameraManager.ERROR_SHOWING_PREVIEW:
|
||||
mErrorToast = R.string.camera_error_opening;
|
||||
break;
|
||||
case CameraManager.ERROR_INITIALIZING_VIDEO:
|
||||
mErrorToast = R.string.camera_error_video_init_fail;
|
||||
updateViewState();
|
||||
break;
|
||||
case CameraManager.ERROR_STORAGE_FAILURE:
|
||||
mErrorToast = R.string.camera_error_storage_fail;
|
||||
updateViewState();
|
||||
break;
|
||||
case CameraManager.ERROR_TAKING_PICTURE:
|
||||
mErrorToast = R.string.camera_error_failure_taking_picture;
|
||||
break;
|
||||
default:
|
||||
mErrorToast = R.string.camera_error_unknown;
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Unknown camera error:" + errorCode);
|
||||
break;
|
||||
}
|
||||
showErrorToastIfNeeded();
|
||||
}
|
||||
|
||||
private void showErrorToastIfNeeded() {
|
||||
if (mErrorToast != 0 && mSelected) {
|
||||
UiUtils.showToastAtBottom(mErrorToast);
|
||||
mErrorToast = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraChanged() {
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
private void onSwapMode() {
|
||||
CameraManager.get().setVideoMode(!CameraManager.get().isVideoMode());
|
||||
if (CameraManager.get().isVideoMode()) {
|
||||
mMediaPicker.setFullScreen(true);
|
||||
|
||||
// For now we start recording immediately
|
||||
mCaptureButton.performClick();
|
||||
}
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
private void showShutterEffect(final View shutterVisual) {
|
||||
final float maxAlpha = getContext().getResources().getFraction(
|
||||
R.fraction.camera_shutter_max_alpha, 1 /* base */, 1 /* pBase */);
|
||||
|
||||
// Divide by 2 so each half of the animation adds up to the full duration
|
||||
final int animationDuration = getContext().getResources().getInteger(
|
||||
R.integer.camera_shutter_duration) / 2;
|
||||
|
||||
final AnimationSet animation = new AnimationSet(false /* shareInterpolator */);
|
||||
final Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha);
|
||||
alphaInAnimation.setDuration(animationDuration);
|
||||
animation.addAnimation(alphaInAnimation);
|
||||
|
||||
final Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f);
|
||||
alphaOutAnimation.setStartOffset(animationDuration);
|
||||
alphaOutAnimation.setDuration(animationDuration);
|
||||
animation.addAnimation(alphaOutAnimation);
|
||||
|
||||
animation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(final Animation animation) {
|
||||
shutterVisual.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(final Animation animation) {
|
||||
shutterVisual.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(final Animation animation) {
|
||||
}
|
||||
});
|
||||
shutterVisual.startAnimation(animation);
|
||||
}
|
||||
|
||||
/** Updates the state of the buttons and overlays based on the current state of the view */
|
||||
private void updateViewState() {
|
||||
if (mView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Context context = getContext();
|
||||
if (context == null) {
|
||||
// Context is null if the fragment was already removed from the activity
|
||||
return;
|
||||
}
|
||||
final boolean fullScreen = mMediaPicker.isFullScreen();
|
||||
final boolean videoMode = CameraManager.get().isVideoMode();
|
||||
final boolean isRecording = CameraManager.get().isRecording();
|
||||
final boolean isCameraAvailable = isCameraAvailable();
|
||||
final Camera.CameraInfo cameraInfo = CameraManager.get().getCameraInfo();
|
||||
final boolean frontCamera = cameraInfo != null && cameraInfo.facing ==
|
||||
Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
|
||||
mView.setSystemUiVisibility(
|
||||
fullScreen ? View.SYSTEM_UI_FLAG_LOW_PROFILE :
|
||||
View.SYSTEM_UI_FLAG_VISIBLE);
|
||||
|
||||
mFullScreenButton.setVisibility(!fullScreen ? View.VISIBLE : View.GONE);
|
||||
mFullScreenButton.setEnabled(isCameraAvailable);
|
||||
mSwapCameraButton.setVisibility(
|
||||
fullScreen && !isRecording && CameraManager.get().hasFrontAndBackCamera() ?
|
||||
View.VISIBLE : View.GONE);
|
||||
mSwapCameraButton.setImageResource(frontCamera ?
|
||||
R.drawable.ic_camera_front_light :
|
||||
R.drawable.ic_camera_rear_light);
|
||||
mSwapCameraButton.setEnabled(isCameraAvailable);
|
||||
|
||||
mCancelVideoButton.setVisibility(isRecording ? View.VISIBLE : View.GONE);
|
||||
mVideoCounter.setVisibility(isRecording ? View.VISIBLE : View.GONE);
|
||||
|
||||
mSwapModeButton.setImageResource(videoMode ?
|
||||
R.drawable.ic_mp_camera_small_light :
|
||||
R.drawable.ic_mp_video_small_light);
|
||||
mSwapModeButton.setContentDescription(context.getString(videoMode ?
|
||||
R.string.camera_switch_to_still_mode : R.string.camera_switch_to_video_mode));
|
||||
mSwapModeButton.setVisibility(isRecording ? View.GONE : View.VISIBLE);
|
||||
mSwapModeButton.setEnabled(isCameraAvailable);
|
||||
|
||||
if (isRecording) {
|
||||
mCaptureButton.setImageResource(R.drawable.ic_mp_capture_stop_large_light);
|
||||
mCaptureButton.setContentDescription(context.getString(
|
||||
R.string.camera_stop_recording));
|
||||
} else if (videoMode) {
|
||||
mCaptureButton.setImageResource(R.drawable.ic_mp_video_large_light);
|
||||
mCaptureButton.setContentDescription(context.getString(
|
||||
R.string.camera_start_recording));
|
||||
} else {
|
||||
mCaptureButton.setImageResource(R.drawable.ic_checkmark_large_light);
|
||||
mCaptureButton.setContentDescription(context.getString(
|
||||
R.string.camera_take_picture));
|
||||
}
|
||||
mCaptureButton.setEnabled(isCameraAvailable);
|
||||
}
|
||||
|
||||
@Override
|
||||
int getActionBarTitleResId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the camera is currently ready camera is loaded and not taking a picture.
|
||||
* otherwise we should avoid taking another picture, swapping camera or recording video.
|
||||
*/
|
||||
private boolean isCameraAvailable() {
|
||||
return CameraManager.get().isCameraAvailable();
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.hardware.Camera;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.PersistentInstanceState;
|
||||
import com.android.messaging.util.ThreadUtil;
|
||||
|
||||
public class CameraMediaChooserView extends FrameLayout implements PersistentInstanceState {
|
||||
private static final String KEY_CAMERA_INDEX = "camera_index";
|
||||
|
||||
// True if we have at least queued an update to the view tree to support software rendering
|
||||
// fallback
|
||||
private boolean mIsSoftwareFallbackActive;
|
||||
|
||||
public CameraMediaChooserView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putInt(KEY_CAMERA_INDEX, CameraManager.get().getCameraIndex());
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Parcelable state) {
|
||||
if (!(state instanceof Bundle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Bundle bundle = (Bundle) state;
|
||||
CameraManager.get().selectCameraByIndex(bundle.getInt(KEY_CAMERA_INDEX));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable saveState() {
|
||||
return onSaveInstanceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(final Parcelable restoredState) {
|
||||
onRestoreInstanceState(restoredState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetState() {
|
||||
CameraManager.get().selectCamera(Camera.CameraInfo.CAMERA_FACING_BACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(final Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
// If the canvas isn't hardware accelerated, we have to replace the HardwareCameraPreview
|
||||
// with a SoftwareCameraPreview which supports software rendering
|
||||
if (!canvas.isHardwareAccelerated() && !mIsSoftwareFallbackActive) {
|
||||
mIsSoftwareFallbackActive = true;
|
||||
// Post modifying the tree since we can't modify the view tree during a draw pass
|
||||
ThreadUtil.getMainThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final HardwareCameraPreview cameraPreview =
|
||||
(HardwareCameraPreview) findViewById(R.id.camera_preview);
|
||||
if (cameraPreview == null) {
|
||||
return;
|
||||
}
|
||||
final ViewGroup parent = ((ViewGroup) cameraPreview.getParent());
|
||||
final int index = parent.indexOfChild(cameraPreview);
|
||||
final SoftwareCameraPreview softwareCameraPreview =
|
||||
new SoftwareCameraPreview(getContext());
|
||||
// Be sure to remove the hardware view before adding the software view to
|
||||
// prevent having 2 camera previews active at the same time
|
||||
parent.removeView(cameraPreview);
|
||||
parent.addView(softwareCameraPreview, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.hardware.Camera;
|
||||
import android.view.View;
|
||||
import android.view.View.MeasureSpec;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Contains shared code for SoftwareCameraPreview and HardwareCameraPreview, cannot use inheritance
|
||||
* because those classes must inherit from separate Views, so those classes delegate calls to this
|
||||
* helper class. Specifics for each implementation are in CameraPreviewHost
|
||||
*/
|
||||
public class CameraPreview {
|
||||
public interface CameraPreviewHost {
|
||||
View getView();
|
||||
boolean isValid();
|
||||
void startPreview(final Camera camera) throws IOException;
|
||||
void onCameraPermissionGranted();
|
||||
|
||||
}
|
||||
|
||||
private int mCameraWidth = -1;
|
||||
private int mCameraHeight = -1;
|
||||
|
||||
private final CameraPreviewHost mHost;
|
||||
|
||||
public CameraPreview(final CameraPreviewHost host) {
|
||||
Assert.notNull(host);
|
||||
Assert.notNull(host.getView());
|
||||
mHost = host;
|
||||
}
|
||||
|
||||
public void setSize(final Camera.Size size, final int orientation) {
|
||||
switch (orientation) {
|
||||
case 0:
|
||||
case 180:
|
||||
mCameraWidth = size.width;
|
||||
mCameraHeight = size.height;
|
||||
break;
|
||||
case 90:
|
||||
case 270:
|
||||
default:
|
||||
mCameraWidth = size.height;
|
||||
mCameraHeight = size.width;
|
||||
}
|
||||
mHost.getView().requestLayout();
|
||||
}
|
||||
|
||||
public int getWidthMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
if (mCameraHeight >= 0) {
|
||||
final int width = View.MeasureSpec.getSize(widthMeasureSpec);
|
||||
return MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
|
||||
} else {
|
||||
return widthMeasureSpec;
|
||||
}
|
||||
}
|
||||
|
||||
public int getHeightMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
if (mCameraHeight >= 0) {
|
||||
final int orientation = getContext().getResources().getConfiguration().orientation;
|
||||
final int width = View.MeasureSpec.getSize(widthMeasureSpec);
|
||||
final float aspectRatio = (float) mCameraWidth / (float) mCameraHeight;
|
||||
int height;
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
height = (int) (width * aspectRatio);
|
||||
} else {
|
||||
height = (int) (width / aspectRatio);
|
||||
}
|
||||
return View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
|
||||
} else {
|
||||
return heightMeasureSpec;
|
||||
}
|
||||
}
|
||||
|
||||
public void onVisibilityChanged(final int visibility) {
|
||||
if (CameraManager.hasCameraPermission()) {
|
||||
if (visibility == View.VISIBLE) {
|
||||
CameraManager.get().openCamera();
|
||||
} else {
|
||||
CameraManager.get().closeCamera();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mHost.getView().getContext();
|
||||
}
|
||||
|
||||
public void setOnTouchListener(final View.OnTouchListener listener) {
|
||||
mHost.getView().setOnTouchListener(listener);
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return mHost.getView().getHeight();
|
||||
}
|
||||
|
||||
public void onAttachedToWindow() {
|
||||
if (CameraManager.hasCameraPermission()) {
|
||||
CameraManager.get().openCamera();
|
||||
}
|
||||
}
|
||||
|
||||
public void onDetachedFromWindow() {
|
||||
CameraManager.get().closeCamera();
|
||||
}
|
||||
|
||||
public void onRestoreInstanceState() {
|
||||
if (CameraManager.hasCameraPermission()) {
|
||||
CameraManager.get().openCamera();
|
||||
}
|
||||
}
|
||||
|
||||
public void onCameraPermissionGranted() {
|
||||
CameraManager.get().openCamera();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the view is valid and prepared for the camera to start showing the preview
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return mHost.isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the camera preview on the current surface. Abstracts out the differences in API
|
||||
* from the CameraManager
|
||||
* @throws IOException Which is caught by the CameraManager to display an error
|
||||
*/
|
||||
public void startPreview(final Camera camera) throws IOException {
|
||||
mHost.startPreview(camera);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.data.PendingAttachmentData;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
import com.android.messaging.util.SafeAsyncTask;
|
||||
|
||||
/**
|
||||
* Wraps around the functionalities to allow the user to pick images from the document
|
||||
* picker. Instances of this class must be tied to a Fragment which is able to delegate activity
|
||||
* result callbacks.
|
||||
*/
|
||||
public class DocumentImagePicker {
|
||||
|
||||
/**
|
||||
* An interface for a listener that listens for when a document has been picked.
|
||||
*/
|
||||
public interface SelectionListener {
|
||||
/**
|
||||
* Called when an document is selected from picker. At this point, the file hasn't been
|
||||
* actually loaded and staged in the temp directory, so we are passing in a pending
|
||||
* MessagePartData, which the consumer should use to display a placeholder image.
|
||||
* @param pendingItem a temporary attachment data for showing the placeholder state.
|
||||
*/
|
||||
void onDocumentSelected(PendingAttachmentData pendingItem);
|
||||
}
|
||||
|
||||
// The owning fragment.
|
||||
private final Fragment mFragment;
|
||||
|
||||
// The listener on the picker events.
|
||||
private final SelectionListener mListener;
|
||||
|
||||
private static final String EXTRA_PHOTO_URL = "photo_url";
|
||||
|
||||
/**
|
||||
* Creates a new instance of DocumentImagePicker.
|
||||
* @param activity The activity that owns the picker, or the activity that hosts the owning
|
||||
* fragment.
|
||||
*/
|
||||
public DocumentImagePicker(final Fragment fragment,
|
||||
final SelectionListener listener) {
|
||||
mFragment = fragment;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent out to open an image/video from document picker.
|
||||
*/
|
||||
public void launchPicker() {
|
||||
UIIntents.get().launchDocumentImagePicker(mFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called from the fragment/activity's onActivityResult().
|
||||
*/
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
if (requestCode == UIIntents.REQUEST_PICK_IMAGE_FROM_DOCUMENT_PICKER &&
|
||||
resultCode == Activity.RESULT_OK) {
|
||||
// Sometimes called after media item has been picked from the document picker.
|
||||
String url = data.getStringExtra(EXTRA_PHOTO_URL);
|
||||
if (url == null) {
|
||||
// we're using the builtin photo picker which supplies the return
|
||||
// url as it's "data"
|
||||
url = data.getDataString();
|
||||
if (url == null) {
|
||||
final Bundle extras = data.getExtras();
|
||||
if (extras != null) {
|
||||
final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
|
||||
if (uri != null) {
|
||||
url = uri.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against null uri cases for when the activity returns a null/invalid intent.
|
||||
if (url != null) {
|
||||
final Uri uri = Uri.parse(url);
|
||||
prepareDocumentForAttachment(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareDocumentForAttachment(final Uri documentUri) {
|
||||
// Notify our listener with a PendingAttachmentData containing the metadata.
|
||||
// Asynchronously get the content type for the picked image since
|
||||
// ImageUtils.getContentType() potentially involves I/O and can be expensive.
|
||||
new SafeAsyncTask<Void, Void, String>() {
|
||||
@Override
|
||||
protected String doInBackgroundTimed(final Void... params) {
|
||||
return ImageUtils.getContentType(
|
||||
Factory.get().getApplicationContext().getContentResolver(), documentUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final String contentType) {
|
||||
// Ask the listener to create a temporary placeholder item to show the progress.
|
||||
final PendingAttachmentData pendingItem =
|
||||
PendingAttachmentData.createPendingAttachmentData(contentType,
|
||||
documentUri);
|
||||
mListener.onDocumentSelected(pendingItem);
|
||||
}
|
||||
}.executeOnThreadPool();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CursorAdapter;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.mediapicker.GalleryGridItemView.HostInterface;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Bridges between the image cursor loaded by GalleryBoundCursorLoader and the GalleryGridView.
|
||||
*/
|
||||
public class GalleryGridAdapter extends CursorAdapter {
|
||||
private GalleryGridItemView.HostInterface mGgivHostInterface;
|
||||
|
||||
public GalleryGridAdapter(final Context context, final Cursor cursor) {
|
||||
super(context, cursor, 0);
|
||||
}
|
||||
|
||||
public void setHostInterface(final HostInterface ggivHostInterface) {
|
||||
mGgivHostInterface = ggivHostInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void bindView(final View view, final Context context, final Cursor cursor) {
|
||||
Assert.isTrue(view instanceof GalleryGridItemView);
|
||||
final GalleryGridItemView galleryImageView = (GalleryGridItemView) view;
|
||||
galleryImageView.bind(cursor, mGgivHostInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
|
||||
final LayoutInflater layoutInflater = LayoutInflater.from(context);
|
||||
return layoutInflater.inflate(R.layout.gallery_grid_item_view, parent, false);
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView.ScaleType;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.data.GalleryGridItemData;
|
||||
import com.android.messaging.ui.AsyncImageView;
|
||||
import com.android.messaging.ui.ConversationDrawables;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Shows an item in the gallery picker grid view. Hosts an FileImageView with a checkbox.
|
||||
*/
|
||||
public class GalleryGridItemView extends FrameLayout {
|
||||
/**
|
||||
* Implemented by the owner of this GalleryGridItemView instance to communicate on media
|
||||
* picking and selection events.
|
||||
*/
|
||||
public interface HostInterface {
|
||||
void onItemClicked(View view, GalleryGridItemData data, boolean longClick);
|
||||
boolean isItemSelected(GalleryGridItemData data);
|
||||
boolean isMultiSelectEnabled();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
GalleryGridItemData mData;
|
||||
private AsyncImageView mImageView;
|
||||
private CheckBox mCheckBox;
|
||||
private HostInterface mHostInterface;
|
||||
private final OnClickListener mOnClickListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
mHostInterface.onItemClicked(GalleryGridItemView.this, mData, false /*longClick*/);
|
||||
}
|
||||
};
|
||||
|
||||
public GalleryGridItemView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mData = DataModel.get().createGalleryGridItemData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mImageView = (AsyncImageView) findViewById(R.id.image);
|
||||
mCheckBox = (CheckBox) findViewById(R.id.checkbox);
|
||||
mCheckBox.setOnClickListener(mOnClickListener);
|
||||
setOnClickListener(mOnClickListener);
|
||||
final OnLongClickListener longClickListener = new OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(final View v) {
|
||||
mHostInterface.onItemClicked(v, mData, true /* longClick */);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
setOnLongClickListener(longClickListener);
|
||||
mCheckBox.setOnLongClickListener(longClickListener);
|
||||
addOnLayoutChangeListener(new OnLayoutChangeListener() {
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
// Enlarge the clickable region for the checkbox to fill the entire view.
|
||||
final Rect region = new Rect(0, 0, getWidth(), getHeight());
|
||||
setTouchDelegate(new TouchDelegate(region, mCheckBox) {
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
setPressed(true);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
setPressed(false);
|
||||
break;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
// The grid view auto-fit the columns, so we want to let the height match the width
|
||||
// to make the image square.
|
||||
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor, final HostInterface hostInterface) {
|
||||
final int desiredSize = getResources()
|
||||
.getDimensionPixelSize(R.dimen.gallery_image_cell_size);
|
||||
mData.bind(cursor, desiredSize, desiredSize);
|
||||
mHostInterface = hostInterface;
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
private void updateViewState() {
|
||||
updateImageView();
|
||||
if (mHostInterface.isMultiSelectEnabled() && !mData.isDocumentPickerItem()) {
|
||||
mCheckBox.setVisibility(VISIBLE);
|
||||
mCheckBox.setClickable(true);
|
||||
mCheckBox.setChecked(mHostInterface.isItemSelected(mData));
|
||||
} else {
|
||||
mCheckBox.setVisibility(GONE);
|
||||
mCheckBox.setClickable(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateImageView() {
|
||||
if (mData.isDocumentPickerItem()) {
|
||||
mImageView.setScaleType(ScaleType.CENTER);
|
||||
setBackgroundColor(ConversationDrawables.get().getConversationThemeColor());
|
||||
mImageView.setImageResourceId(null);
|
||||
mImageView.setImageResource(R.drawable.ic_photo_library_light);
|
||||
mImageView.setContentDescription(getResources().getString(
|
||||
R.string.pick_image_from_document_library_content_description));
|
||||
} else {
|
||||
mImageView.setScaleType(ScaleType.CENTER_CROP);
|
||||
setBackgroundColor(getResources().getColor(R.color.gallery_image_default_background));
|
||||
mImageView.setImageResourceId(mData.getImageRequestDescriptor());
|
||||
final long dateSeconds = mData.getDateSeconds();
|
||||
final boolean isValidDate = (dateSeconds > 0);
|
||||
final int templateId = isValidDate ?
|
||||
R.string.mediapicker_gallery_image_item_description :
|
||||
R.string.mediapicker_gallery_image_item_description_no_date;
|
||||
String contentDescription = String.format(getResources().getString(templateId),
|
||||
dateSeconds * TimeUnit.SECONDS.toMillis(1));
|
||||
mImageView.setContentDescription(contentDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.util.ArrayMap;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData;
|
||||
import com.android.messaging.datamodel.data.GalleryGridItemData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
|
||||
import com.android.messaging.ui.PersistentInstanceState;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContentType;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Shows a list of galley images from external storage in a GridView with multi-select
|
||||
* capabilities, and with the option to intent out to a standalone image picker.
|
||||
*/
|
||||
public class GalleryGridView extends MediaPickerGridView implements
|
||||
GalleryGridItemView.HostInterface,
|
||||
PersistentInstanceState,
|
||||
DraftMessageDataListener {
|
||||
/**
|
||||
* Implemented by the owner of this GalleryGridView instance to communicate on image
|
||||
* picking and multi-image selection events.
|
||||
*/
|
||||
public interface GalleryGridViewListener {
|
||||
void onDocumentPickerItemClicked();
|
||||
void onItemSelected(MessagePartData item);
|
||||
void onItemUnselected(MessagePartData item);
|
||||
void onConfirmSelection();
|
||||
void onUpdate();
|
||||
}
|
||||
|
||||
private GalleryGridViewListener mListener;
|
||||
|
||||
// TODO: Consider putting this into the data model object if we add more states.
|
||||
private final ArrayMap<Uri, MessagePartData> mSelectedImages;
|
||||
private boolean mIsMultiSelectMode = false;
|
||||
private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel;
|
||||
|
||||
public GalleryGridView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mSelectedImages = new ArrayMap<Uri, MessagePartData>();
|
||||
}
|
||||
|
||||
public void setHostInterface(final GalleryGridViewListener hostInterface) {
|
||||
mListener = hostInterface;
|
||||
}
|
||||
|
||||
public void setDraftMessageDataModel(final BindingBase<DraftMessageData> dataModel) {
|
||||
mDraftMessageDataModel = BindingBase.createBindingReference(dataModel);
|
||||
mDraftMessageDataModel.getData().addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClicked(final View view, final GalleryGridItemData data,
|
||||
final boolean longClick) {
|
||||
if (data.isDocumentPickerItem()) {
|
||||
mListener.onDocumentPickerItemClicked();
|
||||
} else if (ContentType.isMediaType(data.getContentType())) {
|
||||
if (longClick) {
|
||||
// Turn on multi-select mode when an item is long-pressed.
|
||||
setMultiSelectEnabled(true);
|
||||
}
|
||||
|
||||
final Rect startRect = new Rect();
|
||||
view.getGlobalVisibleRect(startRect);
|
||||
if (isMultiSelectEnabled()) {
|
||||
toggleItemSelection(startRect, data);
|
||||
} else {
|
||||
mListener.onItemSelected(data.constructMessagePartData(startRect));
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG,
|
||||
"Selected item has invalid contentType " + data.getContentType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemSelected(final GalleryGridItemData data) {
|
||||
return mSelectedImages.containsKey(data.getImageUri());
|
||||
}
|
||||
|
||||
int getSelectionCount() {
|
||||
return mSelectedImages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMultiSelectEnabled() {
|
||||
return mIsMultiSelectMode;
|
||||
}
|
||||
|
||||
private void toggleItemSelection(final Rect startRect, final GalleryGridItemData data) {
|
||||
Assert.isTrue(isMultiSelectEnabled());
|
||||
if (isItemSelected(data)) {
|
||||
final MessagePartData item = mSelectedImages.remove(data.getImageUri());
|
||||
mListener.onItemUnselected(item);
|
||||
if (mSelectedImages.size() == 0) {
|
||||
// No image is selected any more, turn off multi-select mode.
|
||||
setMultiSelectEnabled(false);
|
||||
}
|
||||
} else {
|
||||
final MessagePartData item = data.constructMessagePartData(startRect);
|
||||
mSelectedImages.put(data.getImageUri(), item);
|
||||
mListener.onItemSelected(item);
|
||||
}
|
||||
invalidateViews();
|
||||
}
|
||||
|
||||
private void toggleMultiSelect() {
|
||||
mIsMultiSelectMode = !mIsMultiSelectMode;
|
||||
invalidateViews();
|
||||
}
|
||||
|
||||
private void setMultiSelectEnabled(final boolean enabled) {
|
||||
if (mIsMultiSelectMode != enabled) {
|
||||
toggleMultiSelect();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canToggleMultiSelect() {
|
||||
// We allow the user to toggle multi-select mode only when nothing has selected. If
|
||||
// something has been selected, we show a confirm button instead.
|
||||
return mSelectedImages.size() == 0;
|
||||
}
|
||||
|
||||
public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) {
|
||||
inflater.inflate(R.menu.gallery_picker_menu, menu);
|
||||
final MenuItem toggleMultiSelect = menu.findItem(R.id.action_multiselect);
|
||||
final MenuItem confirmMultiSelect = menu.findItem(R.id.action_confirm_multiselect);
|
||||
final boolean canToggleMultiSelect = canToggleMultiSelect();
|
||||
toggleMultiSelect.setVisible(canToggleMultiSelect);
|
||||
confirmMultiSelect.setVisible(!canToggleMultiSelect);
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_multiselect:
|
||||
Assert.isTrue(canToggleMultiSelect());
|
||||
toggleMultiSelect();
|
||||
return true;
|
||||
|
||||
case R.id.action_confirm_multiselect:
|
||||
Assert.isTrue(!canToggleMultiSelect());
|
||||
mListener.onConfirmSelection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDraftChanged(final DraftMessageData data, final int changeFlags) {
|
||||
mDraftMessageDataModel.ensureBound(data);
|
||||
// Whenever attachment changed, refresh selection state to remove those that are not
|
||||
// selected.
|
||||
if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) ==
|
||||
DraftMessageData.ATTACHMENTS_CHANGED) {
|
||||
refreshImageSelectionStateOnAttachmentChange();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraftAttachmentLimitReached(final DraftMessageData data) {
|
||||
mDraftMessageDataModel.ensureBound(data);
|
||||
// Whenever draft attachment limit is reach, refresh selection state to remove those
|
||||
// not actually added to draft.
|
||||
refreshImageSelectionStateOnAttachmentChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraftAttachmentLoadFailed() {
|
||||
// Nothing to do since the failed attachment gets removed automatically.
|
||||
}
|
||||
|
||||
private void refreshImageSelectionStateOnAttachmentChange() {
|
||||
boolean changed = false;
|
||||
final Iterator<Map.Entry<Uri, MessagePartData>> iterator =
|
||||
mSelectedImages.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<Uri, MessagePartData> entry = iterator.next();
|
||||
if (!mDraftMessageDataModel.getData().containsAttachment(entry.getKey())) {
|
||||
iterator.remove();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
mListener.onUpdate();
|
||||
invalidateViews();
|
||||
}
|
||||
}
|
||||
|
||||
@Override // PersistentInstanceState
|
||||
public Parcelable saveState() {
|
||||
return onSaveInstanceState();
|
||||
}
|
||||
|
||||
@Override // PersistentInstanceState
|
||||
public void restoreState(final Parcelable restoredState) {
|
||||
onRestoreInstanceState(restoredState);
|
||||
invalidateViews();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState savedState = new SavedState(superState);
|
||||
savedState.isMultiSelectMode = mIsMultiSelectMode;
|
||||
savedState.selectedImages = mSelectedImages.values()
|
||||
.toArray(new MessagePartData[mSelectedImages.size()]);
|
||||
return savedState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
if (!(state instanceof SavedState)) {
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
final SavedState savedState = (SavedState) state;
|
||||
super.onRestoreInstanceState(savedState.getSuperState());
|
||||
mIsMultiSelectMode = savedState.isMultiSelectMode;
|
||||
mSelectedImages.clear();
|
||||
for (int i = 0; i < savedState.selectedImages.length; i++) {
|
||||
final MessagePartData selectedImage = savedState.selectedImages[i];
|
||||
mSelectedImages.put(selectedImage.getContentUri(), selectedImage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override // PersistentInstanceState
|
||||
public void resetState() {
|
||||
mSelectedImages.clear();
|
||||
mIsMultiSelectMode = false;
|
||||
invalidateViews();
|
||||
}
|
||||
|
||||
public static class SavedState extends BaseSavedState {
|
||||
boolean isMultiSelectMode;
|
||||
MessagePartData[] selectedImages;
|
||||
|
||||
SavedState(final Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
private SavedState(final Parcel in) {
|
||||
super(in);
|
||||
isMultiSelectMode = in.readInt() == 1 ? true : false;
|
||||
|
||||
// Read parts
|
||||
final int partCount = in.readInt();
|
||||
selectedImages = new MessagePartData[partCount];
|
||||
for (int i = 0; i < partCount; i++) {
|
||||
selectedImages[i] = ((MessagePartData) in.readParcelable(
|
||||
MessagePartData.class.getClassLoader()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel out, final int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt(isMultiSelectMode ? 1 : 0);
|
||||
|
||||
// Write parts
|
||||
out.writeInt(selectedImages.length);
|
||||
for (final MessagePartData image : selectedImages) {
|
||||
out.writeParcelable(image, flags);
|
||||
}
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<SavedState> CREATOR =
|
||||
new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(final Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
@Override
|
||||
public SavedState[] newArray(final int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.GalleryGridItemData;
|
||||
import com.android.messaging.datamodel.data.MediaPickerData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.MediaPickerData.MediaPickerDataListener;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
/**
|
||||
* Chooser which allows the user to select one or more existing images or videos
|
||||
*/
|
||||
class GalleryMediaChooser extends MediaChooser implements
|
||||
GalleryGridView.GalleryGridViewListener, MediaPickerDataListener {
|
||||
private final GalleryGridAdapter mAdapter;
|
||||
private GalleryGridView mGalleryGridView;
|
||||
private View mMissingPermissionView;
|
||||
|
||||
GalleryMediaChooser(final MediaPicker mediaPicker) {
|
||||
super(mediaPicker);
|
||||
mAdapter = new GalleryGridAdapter(Factory.get().getApplicationContext(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSupportedMediaTypes() {
|
||||
return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View destroyView() {
|
||||
mGalleryGridView.setAdapter(null);
|
||||
mAdapter.setHostInterface(null);
|
||||
// The loader is started only if startMediaPickerDataLoader() is called
|
||||
if (OsUtil.hasStoragePermission()) {
|
||||
mBindingRef.getData().destroyLoader(MediaPickerData.GALLERY_IMAGE_LOADER);
|
||||
}
|
||||
return super.destroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconResource() {
|
||||
return R.drawable.ic_image_light;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconDescriptionResource() {
|
||||
return R.string.mediapicker_galleryChooserDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSwipeDown() {
|
||||
return mGalleryGridView.canSwipeDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemSelected(final MessagePartData item) {
|
||||
mMediaPicker.dispatchItemsSelected(item, !mGalleryGridView.isMultiSelectEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemUnselected(final MessagePartData item) {
|
||||
mMediaPicker.dispatchItemUnselected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirmSelection() {
|
||||
// The user may only confirm if multiselect is enabled.
|
||||
Assert.isTrue(mGalleryGridView.isMultiSelectEnabled());
|
||||
mMediaPicker.dispatchConfirmItemSelection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
mMediaPicker.invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) {
|
||||
if (mView != null) {
|
||||
mGalleryGridView.onCreateOptionsMenu(inflater, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
return (mView != null) ? mGalleryGridView.onOptionsItemSelected(item) : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View createView(final ViewGroup container) {
|
||||
final LayoutInflater inflater = getLayoutInflater();
|
||||
final View view = inflater.inflate(
|
||||
R.layout.mediapicker_image_chooser,
|
||||
container /* root */,
|
||||
false /* attachToRoot */);
|
||||
|
||||
mGalleryGridView = (GalleryGridView) view.findViewById(R.id.gallery_grid_view);
|
||||
mAdapter.setHostInterface(mGalleryGridView);
|
||||
mGalleryGridView.setAdapter(mAdapter);
|
||||
mGalleryGridView.setHostInterface(this);
|
||||
mGalleryGridView.setDraftMessageDataModel(mMediaPicker.getDraftMessageDataModel());
|
||||
if (OsUtil.hasStoragePermission()) {
|
||||
startMediaPickerDataLoader();
|
||||
}
|
||||
|
||||
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
|
||||
updateForPermissionState(OsUtil.hasStoragePermission());
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getActionBarTitleResId() {
|
||||
return R.string.mediapicker_gallery_title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDocumentPickerItemClicked() {
|
||||
mMediaPicker.launchDocumentPicker();
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateActionBar(final ActionBar actionBar) {
|
||||
super.updateActionBar(actionBar);
|
||||
if (mGalleryGridView == null) {
|
||||
return;
|
||||
}
|
||||
final int selectionCount = mGalleryGridView.getSelectionCount();
|
||||
if (selectionCount > 0 && mGalleryGridView.isMultiSelectEnabled()) {
|
||||
actionBar.setTitle(getContext().getResources().getString(
|
||||
R.string.mediapicker_gallery_title_selection,
|
||||
selectionCount));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaPickerDataUpdated(final MediaPickerData mediaPickerData, final Object data,
|
||||
final int loaderId) {
|
||||
mBindingRef.ensureBound(mediaPickerData);
|
||||
Assert.equals(MediaPickerData.GALLERY_IMAGE_LOADER, loaderId);
|
||||
Cursor rawCursor = null;
|
||||
if (data instanceof Cursor) {
|
||||
rawCursor = (Cursor) data;
|
||||
}
|
||||
// Before delivering the cursor, wrap around the local gallery cursor
|
||||
// with an extra item for document picker integration in the front.
|
||||
final MatrixCursor specialItemsCursor =
|
||||
new MatrixCursor(GalleryGridItemData.SPECIAL_ITEM_COLUMNS);
|
||||
specialItemsCursor.addRow(new Object[] { GalleryGridItemData.ID_DOCUMENT_PICKER_ITEM });
|
||||
final MergeCursor cursor =
|
||||
new MergeCursor(new Cursor[] { specialItemsCursor, rawCursor });
|
||||
mAdapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
if (OsUtil.hasStoragePermission()) {
|
||||
// Work around a bug in MediaStore where cursors querying the Files provider don't get
|
||||
// updated for changes to Images.Media or Video.Media.
|
||||
startMediaPickerDataLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSelected(final boolean selected) {
|
||||
super.setSelected(selected);
|
||||
if (selected && !OsUtil.hasStoragePermission()) {
|
||||
mMediaPicker.requestPermissions(
|
||||
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
|
||||
MediaPicker.GALLERY_PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
private void startMediaPickerDataLoader() {
|
||||
mBindingRef.getData().startLoader(MediaPickerData.GALLERY_IMAGE_LOADER, mBindingRef, null,
|
||||
this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequestPermissionsResult(
|
||||
final int requestCode, final String permissions[], final int[] grantResults) {
|
||||
if (requestCode == MediaPicker.GALLERY_PERMISSION_REQUEST_CODE) {
|
||||
final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
if (permissionGranted) {
|
||||
startMediaPickerDataLoader();
|
||||
}
|
||||
updateForPermissionState(permissionGranted);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateForPermissionState(final boolean granted) {
|
||||
// onRequestPermissionsResult can sometimes get called before createView().
|
||||
if (mGalleryGridView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mGalleryGridView.setVisibility(granted ? View.VISIBLE : View.GONE);
|
||||
mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.hardware.Camera;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A hardware accelerated preview texture for the camera. This is the preferred CameraPreview
|
||||
* because it animates smoother. When hardware acceleration isn't available, SoftwareCameraPreview
|
||||
* is used.
|
||||
*
|
||||
* There is a significant amount of duplication between HardwareCameraPreview and
|
||||
* SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
|
||||
* implementations of the shared methods are delegated to CameraPreview
|
||||
*/
|
||||
public class HardwareCameraPreview extends TextureView implements CameraPreview.CameraPreviewHost {
|
||||
private CameraPreview mPreview;
|
||||
|
||||
public HardwareCameraPreview(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mPreview = new CameraPreview(this);
|
||||
setSurfaceTextureListener(new SurfaceTextureListener() {
|
||||
@Override
|
||||
public void onSurfaceTextureAvailable(final SurfaceTexture surfaceTexture, final int i, final int i2) {
|
||||
CameraManager.get().setSurface(mPreview);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureSizeChanged(final SurfaceTexture surfaceTexture, final int i, final int i2) {
|
||||
CameraManager.get().setSurface(mPreview);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) {
|
||||
CameraManager.get().setSurface(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) {
|
||||
CameraManager.get().setSurface(mPreview);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onVisibilityChanged(final View changedView, final int visibility) {
|
||||
super.onVisibilityChanged(changedView, visibility);
|
||||
mPreview.onVisibilityChanged(visibility);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mPreview.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
mPreview.onAttachedToWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Parcelable state) {
|
||||
super.onRestoreInstanceState(state);
|
||||
mPreview.onRestoreInstanceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
|
||||
heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return getSurfaceTexture() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPreview(final Camera camera) throws IOException {
|
||||
camera.setPreviewTexture(getSurfaceTexture());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraPermissionGranted() {
|
||||
mPreview.onCameraPermissionGranted();
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContentType;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.SafeAsyncTask;
|
||||
import com.android.messaging.util.exif.ExifInterface;
|
||||
import com.android.messaging.util.exif.ExifTag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class ImagePersistTask extends SafeAsyncTask<Void, Void, Void> {
|
||||
private static final String JPEG_EXTENSION = "jpg";
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
private int mWidth;
|
||||
private int mHeight;
|
||||
private final float mHeightPercent;
|
||||
private final byte[] mBytes;
|
||||
private final Context mContext;
|
||||
private final CameraManager.MediaCallback mCallback;
|
||||
private Uri mOutputUri;
|
||||
private Exception mException;
|
||||
|
||||
public ImagePersistTask(
|
||||
final int width,
|
||||
final int height,
|
||||
final float heightPercent,
|
||||
final byte[] bytes,
|
||||
final Context context,
|
||||
final CameraManager.MediaCallback callback) {
|
||||
Assert.isTrue(heightPercent >= 0 && heightPercent <= 1);
|
||||
Assert.notNull(bytes);
|
||||
Assert.notNull(context);
|
||||
Assert.notNull(callback);
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
mHeightPercent = heightPercent;
|
||||
mBytes = bytes;
|
||||
mContext = context;
|
||||
mCallback = callback;
|
||||
// TODO: We probably want to store directly in MMS storage to prevent this
|
||||
// intermediate step
|
||||
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(JPEG_EXTENSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackgroundTimed(final Void... params) {
|
||||
OutputStream outputStream = null;
|
||||
Bitmap bitmap = null;
|
||||
Bitmap clippedBitmap = null;
|
||||
try {
|
||||
outputStream =
|
||||
mContext.getContentResolver().openOutputStream(mOutputUri);
|
||||
if (mHeightPercent != 1.0f) {
|
||||
int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
|
||||
final ExifInterface exifInterface = new ExifInterface();
|
||||
try {
|
||||
exifInterface.readExif(mBytes);
|
||||
final Integer orientationValue =
|
||||
exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION);
|
||||
if (orientationValue != null) {
|
||||
orientation = orientationValue.intValue();
|
||||
}
|
||||
// The thumbnail is of the full image, but we're cropping it, so just clear
|
||||
// the thumbnail
|
||||
exifInterface.setCompressedThumbnail((byte[]) null);
|
||||
} catch (IOException e) {
|
||||
// Couldn't get exif tags, not the end of the world
|
||||
}
|
||||
bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length);
|
||||
final int clippedWidth;
|
||||
final int clippedHeight;
|
||||
if (ExifInterface.getOrientationParams(orientation).invertDimensions) {
|
||||
Assert.equals(mWidth, bitmap.getHeight());
|
||||
Assert.equals(mHeight, bitmap.getWidth());
|
||||
clippedWidth = (int) (mHeight * mHeightPercent);
|
||||
clippedHeight = mWidth;
|
||||
} else {
|
||||
Assert.equals(mWidth, bitmap.getWidth());
|
||||
Assert.equals(mHeight, bitmap.getHeight());
|
||||
clippedWidth = mWidth;
|
||||
clippedHeight = (int) (mHeight * mHeightPercent);
|
||||
}
|
||||
final int offsetTop = (bitmap.getHeight() - clippedHeight) / 2;
|
||||
final int offsetLeft = (bitmap.getWidth() - clippedWidth) / 2;
|
||||
mWidth = clippedWidth;
|
||||
mHeight = clippedHeight;
|
||||
clippedBitmap = Bitmap.createBitmap(clippedWidth, clippedHeight,
|
||||
Bitmap.Config.ARGB_8888);
|
||||
clippedBitmap.setDensity(bitmap.getDensity());
|
||||
final Canvas clippedBitmapCanvas = new Canvas(clippedBitmap);
|
||||
final Matrix matrix = new Matrix();
|
||||
matrix.postTranslate(-offsetLeft, -offsetTop);
|
||||
clippedBitmapCanvas.drawBitmap(bitmap, matrix, null /* paint */);
|
||||
clippedBitmapCanvas.save();
|
||||
// EXIF data can take a big chunk of the file size and is often cleared by the
|
||||
// carrier, only store orientation since that's critical
|
||||
ExifTag orientationTag = exifInterface.getTag(ExifInterface.TAG_ORIENTATION);
|
||||
exifInterface.clearExif();
|
||||
exifInterface.setTag(orientationTag);
|
||||
exifInterface.writeExif(clippedBitmap, outputStream);
|
||||
} else {
|
||||
outputStream.write(mBytes);
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
mOutputUri = null;
|
||||
mException = e;
|
||||
LogUtil.e(TAG, "Unable to persist image to temp storage " + e);
|
||||
} finally {
|
||||
if (bitmap != null) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
|
||||
if (clippedBitmap != null) {
|
||||
clippedBitmap.recycle();
|
||||
}
|
||||
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.flush();
|
||||
} catch (final IOException e) {
|
||||
mOutputUri = null;
|
||||
mException = e;
|
||||
LogUtil.e(TAG, "error trying to flush and close the outputStream" + e);
|
||||
} finally {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (final IOException e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Void aVoid) {
|
||||
if (mOutputUri != null) {
|
||||
mCallback.onMediaReady(mOutputUri, ContentType.IMAGE_JPEG, mWidth, mHeight);
|
||||
} else {
|
||||
Assert.notNull(mException);
|
||||
mCallback.onMediaFailed(mException);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.media.MediaRecorder;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContentType;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.SafeAsyncTask;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Wraps around the functionalities of MediaRecorder, performs routine setup for audio recording
|
||||
* and updates the audio level to be displayed in UI.
|
||||
*
|
||||
* During the start and end of a recording session, we kick off a thread that polls for audio
|
||||
* levels, and updates the thread-safe AudioLevelSource instance. Consumers may bind to the
|
||||
* sound level by either polling from the level source, or register for a level change callback
|
||||
* on the level source object. In Bugle, the UI element (SoundLevels) polls for the sound level
|
||||
* on the UI thread by using animation ticks and invalidating itself.
|
||||
*
|
||||
* Aside from tracking sound levels, this also encapsulates the functionality to save the file
|
||||
* to the scratch space. The saved file is returned by calling stopRecording().
|
||||
*/
|
||||
public class LevelTrackingMediaRecorder {
|
||||
// We refresh sound level every 100ms during a recording session.
|
||||
private static final int REFRESH_INTERVAL_MILLIS = 100;
|
||||
|
||||
// The native amplitude returned from MediaRecorder ranges from 0~32768 (unfortunately, this
|
||||
// is not a constant that's defined anywhere, but the framework's Recorder app is using the
|
||||
// same hard-coded number). Therefore, a constant is needed in order to make it 0~100.
|
||||
private static final int MAX_AMPLITUDE_FACTOR = 32768 / 100;
|
||||
|
||||
// We want to limit the max audio file size by the max message size allowed by MmsConfig,
|
||||
// plus multiplied by this fudge ratio to guarantee that we don't go over limit.
|
||||
private static final float MAX_SIZE_RATIO = 0.8f;
|
||||
|
||||
// Default recorder settings for Bugle.
|
||||
// TODO: Do we want these to be tweakable?
|
||||
private static final int MEDIA_RECORDER_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
|
||||
private static final int MEDIA_RECORDER_OUTPUT_FORMAT = MediaRecorder.OutputFormat.THREE_GPP;
|
||||
private static final int MEDIA_RECORDER_AUDIO_ENCODER = MediaRecorder.AudioEncoder.AMR_NB;
|
||||
|
||||
private final AudioLevelSource mLevelSource;
|
||||
private Thread mRefreshLevelThread;
|
||||
private MediaRecorder mRecorder;
|
||||
private Uri mOutputUri;
|
||||
private ParcelFileDescriptor mOutputFD;
|
||||
|
||||
public LevelTrackingMediaRecorder() {
|
||||
mLevelSource = new AudioLevelSource();
|
||||
}
|
||||
|
||||
public AudioLevelSource getLevelSource() {
|
||||
return mLevelSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if we are currently in a recording session.
|
||||
*/
|
||||
public boolean isRecording() {
|
||||
return mRecorder != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new recording session.
|
||||
* @return true if a session is successfully started; false if something went wrong or if
|
||||
* we are already recording.
|
||||
*/
|
||||
public boolean startRecording(final MediaRecorder.OnErrorListener errorListener,
|
||||
final MediaRecorder.OnInfoListener infoListener, int maxSize) {
|
||||
synchronized (LevelTrackingMediaRecorder.class) {
|
||||
if (mRecorder == null) {
|
||||
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(
|
||||
ContentType.THREE_GPP_EXTENSION);
|
||||
mRecorder = new MediaRecorder();
|
||||
try {
|
||||
// The scratch space file is a Uri, however MediaRecorder
|
||||
// API only accepts absolute FD's. Therefore, get the
|
||||
// FileDescriptor from the content resolver to ensure the
|
||||
// directory is created and get the file path to output the
|
||||
// audio to.
|
||||
maxSize *= MAX_SIZE_RATIO;
|
||||
mOutputFD = Factory.get().getApplicationContext()
|
||||
.getContentResolver().openFileDescriptor(mOutputUri, "w");
|
||||
mRecorder.setAudioSource(MEDIA_RECORDER_AUDIO_SOURCE);
|
||||
mRecorder.setOutputFormat(MEDIA_RECORDER_OUTPUT_FORMAT);
|
||||
mRecorder.setAudioEncoder(MEDIA_RECORDER_AUDIO_ENCODER);
|
||||
mRecorder.setOutputFile(mOutputFD.getFileDescriptor());
|
||||
mRecorder.setMaxFileSize(maxSize);
|
||||
mRecorder.setOnErrorListener(errorListener);
|
||||
mRecorder.setOnInfoListener(infoListener);
|
||||
mRecorder.prepare();
|
||||
mRecorder.start();
|
||||
startTrackingSoundLevel();
|
||||
return true;
|
||||
} catch (final Exception e) {
|
||||
// There may be a device failure or I/O failure, record the error but
|
||||
// don't fail.
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Something went wrong when starting " +
|
||||
"media recorder. " + e);
|
||||
UiUtils.showToastAtBottom(R.string.audio_recording_start_failed);
|
||||
stopRecording();
|
||||
}
|
||||
} else {
|
||||
Assert.fail("Trying to start a new recording session while already recording!");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current recording session.
|
||||
* @return the Uri of the output file, or null if not currently recording.
|
||||
*/
|
||||
public Uri stopRecording() {
|
||||
synchronized (LevelTrackingMediaRecorder.class) {
|
||||
if (mRecorder != null) {
|
||||
try {
|
||||
mRecorder.stop();
|
||||
} catch (final RuntimeException ex) {
|
||||
// This may happen when the recording is too short, so just drop the recording
|
||||
// in this case.
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Something went wrong when stopping " +
|
||||
"media recorder. " + ex);
|
||||
if (mOutputUri != null) {
|
||||
final Uri outputUri = mOutputUri;
|
||||
SafeAsyncTask.executeOnThreadPool(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Factory.get().getApplicationContext().getContentResolver().delete(
|
||||
outputUri, null, null);
|
||||
}
|
||||
});
|
||||
mOutputUri = null;
|
||||
}
|
||||
} finally {
|
||||
mRecorder.release();
|
||||
mRecorder = null;
|
||||
}
|
||||
} else {
|
||||
Assert.fail("Not currently recording!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (mOutputFD != null) {
|
||||
try {
|
||||
mOutputFD.close();
|
||||
} catch (final IOException e) {
|
||||
// Nothing to do
|
||||
}
|
||||
mOutputFD = null;
|
||||
}
|
||||
|
||||
stopTrackingSoundLevel();
|
||||
return mOutputUri;
|
||||
}
|
||||
|
||||
private int getAmplitude() {
|
||||
synchronized (LevelTrackingMediaRecorder.class) {
|
||||
if (mRecorder != null) {
|
||||
final int maxAmplitude = mRecorder.getMaxAmplitude() / MAX_AMPLITUDE_FACTOR;
|
||||
return Math.min(maxAmplitude, 100);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startTrackingSoundLevel() {
|
||||
stopTrackingSoundLevel();
|
||||
mRefreshLevelThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (true) {
|
||||
synchronized (LevelTrackingMediaRecorder.class) {
|
||||
if (mRecorder != null) {
|
||||
mLevelSource.setSpeechLevel(getAmplitude());
|
||||
} else {
|
||||
// The recording session is over, finish the thread.
|
||||
return;
|
||||
}
|
||||
}
|
||||
Thread.sleep(REFRESH_INTERVAL_MILLIS);
|
||||
}
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
};
|
||||
mRefreshLevelThread.start();
|
||||
}
|
||||
|
||||
private void stopTrackingSoundLevel() {
|
||||
if (mRefreshLevelThread != null && mRefreshLevelThread.isAlive()) {
|
||||
mRefreshLevelThread.interrupt();
|
||||
mRefreshLevelThread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.app.FragmentManager;
|
||||
import android.content.Context;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
|
||||
import com.android.messaging.datamodel.data.MediaPickerData;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
|
||||
import com.android.messaging.ui.BasePagerViewHolder;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
abstract class MediaChooser extends BasePagerViewHolder
|
||||
implements DraftMessageSubscriptionDataProvider {
|
||||
/** The media picker that the chooser is hosted in */
|
||||
protected final MediaPicker mMediaPicker;
|
||||
|
||||
/** Referencing the main media picker binding to perform data loading */
|
||||
protected final ImmutableBindingRef<MediaPickerData> mBindingRef;
|
||||
|
||||
/** True if this is the selected chooser */
|
||||
protected boolean mSelected;
|
||||
|
||||
/** True if this chooser is open */
|
||||
protected boolean mOpen;
|
||||
|
||||
/** The button to show in the tab strip */
|
||||
private ImageButton mTabButton;
|
||||
|
||||
/** Used by subclasses to indicate that no loader is required from the data model in order for
|
||||
* this chooser to function.
|
||||
*/
|
||||
public static final int NO_LOADER_REQUIRED = -1;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the Chooser class
|
||||
* @param mediaPicker The media picker that the chooser is hosted in
|
||||
*/
|
||||
MediaChooser(final MediaPicker mediaPicker) {
|
||||
Assert.notNull(mediaPicker);
|
||||
mMediaPicker = mediaPicker;
|
||||
mBindingRef = mediaPicker.getMediaPickerDataBinding();
|
||||
mSelected = false;
|
||||
}
|
||||
|
||||
protected void setSelected(final boolean selected) {
|
||||
mSelected = selected;
|
||||
if (selected) {
|
||||
// If we're selected, it must be open
|
||||
mOpen = true;
|
||||
}
|
||||
if (mTabButton != null) {
|
||||
mTabButton.setSelected(selected);
|
||||
mTabButton.setAlpha(selected ? 1 : 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton getTabButton() {
|
||||
return mTabButton;
|
||||
}
|
||||
|
||||
void onCreateTabButton(final LayoutInflater inflater, final ViewGroup parent) {
|
||||
mTabButton = (ImageButton) inflater.inflate(
|
||||
R.layout.mediapicker_tab_button,
|
||||
parent,
|
||||
false /* addToParent */);
|
||||
mTabButton.setImageResource(getIconResource());
|
||||
mTabButton.setContentDescription(
|
||||
inflater.getContext().getResources().getString(getIconDescriptionResource()));
|
||||
setSelected(mSelected);
|
||||
mTabButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
mMediaPicker.selectChooser(MediaChooser.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return mMediaPicker.getActivity();
|
||||
}
|
||||
|
||||
protected FragmentManager getFragmentManager() {
|
||||
return OsUtil.isAtLeastJB_MR1() ? mMediaPicker.getChildFragmentManager() :
|
||||
mMediaPicker.getFragmentManager();
|
||||
}
|
||||
protected LayoutInflater getLayoutInflater() {
|
||||
return LayoutInflater.from(getContext());
|
||||
}
|
||||
|
||||
/** Allows the chooser to handle full screen change */
|
||||
void onFullScreenChanged(final boolean fullScreen) {}
|
||||
|
||||
/** Allows the chooser to handle the chooser being opened or closed */
|
||||
void onOpenedChanged(final boolean open) {
|
||||
mOpen = open;
|
||||
}
|
||||
|
||||
/** @return The bit field of media types that this chooser can pick */
|
||||
public abstract int getSupportedMediaTypes();
|
||||
|
||||
/** @return The resource id of the icon for the chooser */
|
||||
abstract int getIconResource();
|
||||
|
||||
/** @return The resource id of the string to use for the accessibility text of the icon */
|
||||
abstract int getIconDescriptionResource();
|
||||
|
||||
/**
|
||||
* Sets up the action bar to show the current state of the full-screen chooser
|
||||
* @param actionBar The action bar to populate
|
||||
*/
|
||||
void updateActionBar(final ActionBar actionBar) {
|
||||
final int actionBarTitleResId = getActionBarTitleResId();
|
||||
if (actionBarTitleResId == 0) {
|
||||
actionBar.hide();
|
||||
} else {
|
||||
actionBar.setCustomView(null);
|
||||
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.show();
|
||||
// Use X instead of <- in the action bar
|
||||
actionBar.setHomeAsUpIndicator(R.drawable.ic_remove_small_light);
|
||||
actionBar.setTitle(actionBarTitleResId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource Id used for the action bar title.
|
||||
*/
|
||||
abstract int getActionBarTitleResId();
|
||||
|
||||
/**
|
||||
* Throws an exception if the media chooser object doesn't require data support.
|
||||
*/
|
||||
public void onDataUpdated(final Object data, final int loaderId) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the MediaPicker to determine whether this panel can be swiped down further. If
|
||||
* not, then a swipe down gestured will be captured by the MediaPickerPanel to shrink the
|
||||
* entire panel.
|
||||
*/
|
||||
public boolean canSwipeDown() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typically the media picker is closed when the IME is opened, but this allows the chooser to
|
||||
* specify that showing the IME is okay while the chooser is up
|
||||
*/
|
||||
public boolean canShowIme() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean onBackPressed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) {
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setThemeColor(final int color) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the chooser is owning any incoming touch events, so that the media picker
|
||||
* panel won't process it and slide the panel.
|
||||
*/
|
||||
public boolean isHandlingTouch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void stopTouchHandling() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConversationSelfSubId() {
|
||||
return mMediaPicker.getConversationSelfSubId();
|
||||
}
|
||||
|
||||
/** Optional activity life-cycle methods to be overridden by subclasses */
|
||||
public void onPause() { }
|
||||
public void onResume() { }
|
||||
protected void onRequestPermissionsResult(
|
||||
final int requestCode, final String permissions[], final int[] grantResults) { }
|
||||
}
|
||||
@@ -1,736 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData;
|
||||
import com.android.messaging.datamodel.data.MediaPickerData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.PendingAttachmentData;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.FixedViewPagerAdapter;
|
||||
import com.android.messaging.ui.mediapicker.DocumentImagePicker.SelectionListener;
|
||||
import com.android.messaging.util.AccessibilityUtil;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Fragment used to select or capture media to be added to the message
|
||||
*/
|
||||
public class MediaPicker extends Fragment implements DraftMessageSubscriptionDataProvider {
|
||||
/** The listener interface for events from the media picker */
|
||||
public interface MediaPickerListener {
|
||||
/** Called when the media picker is opened so the host can accommodate the UI */
|
||||
void onOpened();
|
||||
|
||||
/**
|
||||
* Called when the media picker goes into or leaves full screen mode so the host can
|
||||
* accommodate the fullscreen UI
|
||||
*/
|
||||
void onFullScreenChanged(boolean fullScreen);
|
||||
|
||||
/**
|
||||
* Called when the user selects one or more items
|
||||
* @param items The list of items which were selected
|
||||
*/
|
||||
void onItemsSelected(Collection<MessagePartData> items, boolean dismissMediaPicker);
|
||||
|
||||
/**
|
||||
* Called when the user unselects one item.
|
||||
*/
|
||||
void onItemUnselected(MessagePartData item);
|
||||
|
||||
/**
|
||||
* Called when the media picker is closed. Always called immediately after onItemsSelected
|
||||
*/
|
||||
void onDismissed();
|
||||
|
||||
/**
|
||||
* Called when media item selection is confirmed in a multi-select action.
|
||||
*/
|
||||
void onConfirmItemSelection();
|
||||
|
||||
/**
|
||||
* Called when a pending attachment is added.
|
||||
* @param pendingItem the pending attachment data being loaded.
|
||||
*/
|
||||
void onPendingItemAdded(PendingAttachmentData pendingItem);
|
||||
|
||||
/**
|
||||
* Called when a new media chooser is selected.
|
||||
*/
|
||||
void onChooserSelected(final int chooserIndex);
|
||||
}
|
||||
|
||||
/** The tag used when registering and finding this fragment */
|
||||
public static final String FRAGMENT_TAG = "mediapicker";
|
||||
|
||||
// Media type constants that the media picker supports
|
||||
public static final int MEDIA_TYPE_DEFAULT = 0x0000;
|
||||
public static final int MEDIA_TYPE_NONE = 0x0000;
|
||||
public static final int MEDIA_TYPE_IMAGE = 0x0001;
|
||||
public static final int MEDIA_TYPE_VIDEO = 0x0002;
|
||||
public static final int MEDIA_TYPE_AUDIO = 0x0004;
|
||||
public static final int MEDIA_TYPE_VCARD = 0x0008;
|
||||
public static final int MEDIA_TYPE_LOCATION = 0x0010;
|
||||
private static final int MEDA_TYPE_INVALID = 0x0020;
|
||||
public static final int MEDIA_TYPE_ALL = 0xFFFF;
|
||||
|
||||
/** The listener to call when events occur */
|
||||
private MediaPickerListener mListener;
|
||||
|
||||
/** The handler used to dispatch calls to the listener */
|
||||
private Handler mListenerHandler;
|
||||
|
||||
/** The bit flags of media types supported */
|
||||
private int mSupportedMediaTypes;
|
||||
|
||||
/** The list of choosers which could be within the media picker */
|
||||
private final MediaChooser[] mChoosers;
|
||||
|
||||
/** The list of currently enabled choosers */
|
||||
private final ArrayList<MediaChooser> mEnabledChoosers;
|
||||
|
||||
/** The currently selected chooser */
|
||||
private MediaChooser mSelectedChooser;
|
||||
|
||||
/** The main panel that controls the custom layout */
|
||||
private MediaPickerPanel mMediaPickerPanel;
|
||||
|
||||
/** The linear layout that holds the icons to select individual chooser tabs */
|
||||
private LinearLayout mTabStrip;
|
||||
|
||||
/** The view pager to swap between choosers */
|
||||
private ViewPager mViewPager;
|
||||
|
||||
/** The current pager adapter for the view pager */
|
||||
private FixedViewPagerAdapter<MediaChooser> mPagerAdapter;
|
||||
|
||||
/** True if the media picker is visible */
|
||||
private boolean mOpen;
|
||||
|
||||
/** The theme color to use to make the media picker match the rest of the UI */
|
||||
private int mThemeColor;
|
||||
|
||||
@VisibleForTesting
|
||||
final Binding<MediaPickerData> mBinding = BindingBase.createBinding(this);
|
||||
|
||||
/** Handles picking image from the document picker */
|
||||
private DocumentImagePicker mDocumentImagePicker;
|
||||
|
||||
/** Provides subscription-related data to access per-subscription configurations. */
|
||||
private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider;
|
||||
|
||||
/** Provides access to DraftMessageData associated with the current conversation */
|
||||
private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel;
|
||||
|
||||
public MediaPicker() {
|
||||
this(Factory.get().getApplicationContext());
|
||||
}
|
||||
|
||||
public MediaPicker(final Context context) {
|
||||
mBinding.bind(DataModel.get().createMediaPickerData(context));
|
||||
mEnabledChoosers = new ArrayList<MediaChooser>();
|
||||
mChoosers = new MediaChooser[] {
|
||||
new CameraMediaChooser(this),
|
||||
new GalleryMediaChooser(this),
|
||||
new AudioMediaChooser(this),
|
||||
};
|
||||
|
||||
mOpen = false;
|
||||
setSupportedMediaTypes(MEDIA_TYPE_ALL);
|
||||
}
|
||||
|
||||
private boolean mIsAttached;
|
||||
private int mStartingMediaTypeOnAttach = MEDA_TYPE_INVALID;
|
||||
private boolean mAnimateOnAttach;
|
||||
|
||||
@Override
|
||||
public void onAttach (final Activity activity) {
|
||||
super.onAttach(activity);
|
||||
mIsAttached = true;
|
||||
if (mStartingMediaTypeOnAttach != MEDA_TYPE_INVALID) {
|
||||
// open() was previously called. Do the pending open now.
|
||||
doOpen(mStartingMediaTypeOnAttach, mAnimateOnAttach);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mBinding.getData().init(getLoaderManager());
|
||||
mDocumentImagePicker = new DocumentImagePicker(this,
|
||||
new SelectionListener() {
|
||||
@Override
|
||||
public void onDocumentSelected(final PendingAttachmentData data) {
|
||||
if (mBinding.isBound()) {
|
||||
dispatchPendingItemAdded(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
final LayoutInflater inflater,
|
||||
final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
mMediaPickerPanel = (MediaPickerPanel) inflater.inflate(
|
||||
R.layout.mediapicker_fragment,
|
||||
container,
|
||||
false);
|
||||
mMediaPickerPanel.setMediaPicker(this);
|
||||
mTabStrip = (LinearLayout) mMediaPickerPanel.findViewById(R.id.mediapicker_tabstrip);
|
||||
mTabStrip.setBackgroundColor(mThemeColor);
|
||||
for (final MediaChooser chooser : mChoosers) {
|
||||
chooser.onCreateTabButton(inflater, mTabStrip);
|
||||
final boolean enabled = (chooser.getSupportedMediaTypes() & mSupportedMediaTypes) !=
|
||||
MEDIA_TYPE_NONE;
|
||||
final ImageButton tabButton = chooser.getTabButton();
|
||||
if (tabButton != null) {
|
||||
tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
mTabStrip.addView(tabButton);
|
||||
}
|
||||
}
|
||||
|
||||
mViewPager = (ViewPager) mMediaPickerPanel.findViewById(R.id.mediapicker_view_pager);
|
||||
mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(
|
||||
final int position,
|
||||
final float positionOffset,
|
||||
final int positionOffsetPixels) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
// The position returned is relative to if we are in RtL mode. This class never
|
||||
// switches the indices of the elements if we are in RtL mode so we need to
|
||||
// translate the index back. For example, if the user clicked the item most to the
|
||||
// right in RtL mode we would want the index to appear as 0 here, however the
|
||||
// position returned would the last possible index.
|
||||
if (UiUtils.isRtlMode()) {
|
||||
position = mEnabledChoosers.size() - 1 - position;
|
||||
}
|
||||
selectChooser(mEnabledChoosers.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(final int state) {
|
||||
}
|
||||
});
|
||||
// Camera initialization is expensive, so don't realize offscreen pages if not needed.
|
||||
mViewPager.setOffscreenPageLimit(0);
|
||||
mViewPager.setAdapter(mPagerAdapter);
|
||||
final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled(
|
||||
getActivity());
|
||||
mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled);
|
||||
mMediaPickerPanel.setExpanded(mOpen, true, mEnabledChoosers.indexOf(mSelectedChooser));
|
||||
return mMediaPickerPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
CameraManager.get().onPause();
|
||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||
chooser.onPause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
CameraManager.get().onResume();
|
||||
|
||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||
chooser.onResume();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
mDocumentImagePicker.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mBinding.unbind();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme color to make the media picker match the surrounding UI
|
||||
* @param themeColor The new theme color
|
||||
*/
|
||||
public void setConversationThemeColor(final int themeColor) {
|
||||
mThemeColor = themeColor;
|
||||
if (mTabStrip != null) {
|
||||
mTabStrip.setBackgroundColor(mThemeColor);
|
||||
}
|
||||
|
||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||
chooser.setThemeColor(mThemeColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current conversation theme color.
|
||||
*/
|
||||
public int getConversationThemeColor() {
|
||||
return mThemeColor;
|
||||
}
|
||||
|
||||
public void setDraftMessageDataModel(final BindingBase<DraftMessageData> draftBinding) {
|
||||
mDraftMessageDataModel = Binding.createBindingReference(draftBinding);
|
||||
}
|
||||
|
||||
public ImmutableBindingRef<DraftMessageData> getDraftMessageDataModel() {
|
||||
return mDraftMessageDataModel;
|
||||
}
|
||||
|
||||
public void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) {
|
||||
mSubscriptionDataProvider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConversationSelfSubId() {
|
||||
return mSubscriptionDataProvider.getConversationSelfSubId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the media picker and optionally shows the chooser for the supplied media type
|
||||
* @param startingMediaType The media type of the chooser to open if {@link #MEDIA_TYPE_DEFAULT}
|
||||
* is used, then the default chooser from saved shared prefs is opened
|
||||
*/
|
||||
public void open(final int startingMediaType, final boolean animate) {
|
||||
mOpen = true;
|
||||
if (mIsAttached) {
|
||||
doOpen(startingMediaType, animate);
|
||||
} else {
|
||||
// open() can get called immediately after the MediaPicker is created. In that case,
|
||||
// we defer doing work as it may require an attached fragment (eg. calling
|
||||
// Fragment#requestPermission)
|
||||
mStartingMediaTypeOnAttach = startingMediaType;
|
||||
mAnimateOnAttach = animate;
|
||||
}
|
||||
}
|
||||
|
||||
private void doOpen(int startingMediaType, final boolean animate) {
|
||||
final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled(
|
||||
// getActivity() will be null at this point
|
||||
Factory.get().getApplicationContext());
|
||||
|
||||
// If no specific starting type is specified (i.e. MEDIA_TYPE_DEFAULT), try to get the
|
||||
// last opened chooser index from shared prefs.
|
||||
if (startingMediaType == MEDIA_TYPE_DEFAULT) {
|
||||
final int selectedChooserIndex = mBinding.getData().getSelectedChooserIndex();
|
||||
if (selectedChooserIndex >= 0 && selectedChooserIndex < mEnabledChoosers.size()) {
|
||||
selectChooser(mEnabledChoosers.get(selectedChooserIndex));
|
||||
} else {
|
||||
// This is the first time the picker is being used
|
||||
if (isTouchExplorationEnabled) {
|
||||
// Accessibility defaults to audio attachment mode.
|
||||
startingMediaType = MEDIA_TYPE_AUDIO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mSelectedChooser == null) {
|
||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||
if (startingMediaType == MEDIA_TYPE_DEFAULT ||
|
||||
(startingMediaType & chooser.getSupportedMediaTypes()) != MEDIA_TYPE_NONE) {
|
||||
selectChooser(chooser);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mSelectedChooser == null) {
|
||||
// Fall back to the first chooser.
|
||||
selectChooser(mEnabledChoosers.get(0));
|
||||
}
|
||||
|
||||
if (mMediaPickerPanel != null) {
|
||||
mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled);
|
||||
mMediaPickerPanel.setExpanded(true, animate,
|
||||
mEnabledChoosers.indexOf(mSelectedChooser));
|
||||
}
|
||||
}
|
||||
|
||||
/** @return True if the media picker is open */
|
||||
public boolean isOpen() {
|
||||
return mOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of media types to allow the user to select
|
||||
* @param mediaTypes The bit flags of media types to allow. Can be any combination of the
|
||||
* MEDIA_TYPE_* values
|
||||
*/
|
||||
void setSupportedMediaTypes(final int mediaTypes) {
|
||||
mSupportedMediaTypes = mediaTypes;
|
||||
mEnabledChoosers.clear();
|
||||
boolean selectNextChooser = false;
|
||||
for (final MediaChooser chooser : mChoosers) {
|
||||
final boolean enabled = (chooser.getSupportedMediaTypes() & mSupportedMediaTypes) !=
|
||||
MEDIA_TYPE_NONE;
|
||||
if (enabled) {
|
||||
// TODO Add a way to inform the chooser which media types are supported
|
||||
mEnabledChoosers.add(chooser);
|
||||
if (selectNextChooser) {
|
||||
selectChooser(chooser);
|
||||
selectNextChooser = false;
|
||||
}
|
||||
} else if (mSelectedChooser == chooser) {
|
||||
selectNextChooser = true;
|
||||
}
|
||||
final ImageButton tabButton = chooser.getTabButton();
|
||||
if (tabButton != null) {
|
||||
tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectNextChooser && mEnabledChoosers.size() > 0) {
|
||||
selectChooser(mEnabledChoosers.get(0));
|
||||
}
|
||||
final MediaChooser[] enabledChoosers = new MediaChooser[mEnabledChoosers.size()];
|
||||
mEnabledChoosers.toArray(enabledChoosers);
|
||||
mPagerAdapter = new FixedViewPagerAdapter<MediaChooser>(enabledChoosers);
|
||||
if (mViewPager != null) {
|
||||
mViewPager.setAdapter(mPagerAdapter);
|
||||
}
|
||||
|
||||
// Only rebind data if we are currently bound. Otherwise, we must have not
|
||||
// bound to any data yet and should wait until onCreate() to bind data.
|
||||
if (mBinding.isBound() && getActivity() != null) {
|
||||
mBinding.unbind();
|
||||
mBinding.bind(DataModel.get().createMediaPickerData(getActivity()));
|
||||
mBinding.getData().init(getLoaderManager());
|
||||
}
|
||||
}
|
||||
|
||||
ViewPager getViewPager() {
|
||||
return mViewPager;
|
||||
}
|
||||
|
||||
/** Hides the media picker, and frees up any resources it’s using */
|
||||
public void dismiss(final boolean animate) {
|
||||
mOpen = false;
|
||||
if (mMediaPickerPanel != null) {
|
||||
mMediaPickerPanel.setExpanded(false, animate, MediaPickerPanel.PAGE_NOT_SET);
|
||||
}
|
||||
mSelectedChooser = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener for the media picker events
|
||||
* @param listener The listener which will receive events
|
||||
*/
|
||||
public void setListener(final MediaPickerListener listener) {
|
||||
Assert.isMainThread();
|
||||
mListener = listener;
|
||||
mListenerHandler = listener != null ? new Handler() : null;
|
||||
}
|
||||
|
||||
/** @return True if the media picker is in full-screen mode */
|
||||
public boolean isFullScreen() {
|
||||
return mMediaPickerPanel != null && mMediaPickerPanel.isFullScreen();
|
||||
}
|
||||
|
||||
public void setFullScreen(final boolean fullScreen) {
|
||||
mMediaPickerPanel.setFullScreenView(fullScreen, true);
|
||||
}
|
||||
|
||||
public void updateActionBar(final ActionBar actionBar) {
|
||||
if (getActivity() == null) {
|
||||
return;
|
||||
}
|
||||
if (isFullScreen() && mSelectedChooser != null) {
|
||||
mSelectedChooser.updateActionBar(actionBar);
|
||||
} else {
|
||||
actionBar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a new chooser
|
||||
* @param newSelectedChooser The newly selected chooser
|
||||
*/
|
||||
void selectChooser(final MediaChooser newSelectedChooser) {
|
||||
if (mSelectedChooser == newSelectedChooser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.setSelected(false);
|
||||
}
|
||||
mSelectedChooser = newSelectedChooser;
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.setSelected(true);
|
||||
}
|
||||
|
||||
final int chooserIndex = mEnabledChoosers.indexOf(mSelectedChooser);
|
||||
if (mViewPager != null) {
|
||||
mViewPager.setCurrentItem(chooserIndex, true /* smoothScroll */);
|
||||
}
|
||||
|
||||
if (isFullScreen()) {
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
// Save the newly selected chooser's index so we may directly switch to it the
|
||||
// next time user opens the media picker.
|
||||
mBinding.getData().saveSelectedChooserIndex(chooserIndex);
|
||||
if (mMediaPickerPanel != null) {
|
||||
mMediaPickerPanel.onChooserChanged();
|
||||
}
|
||||
dispatchChooserSelected(chooserIndex);
|
||||
}
|
||||
|
||||
public boolean canShowIme() {
|
||||
if (mSelectedChooser != null) {
|
||||
return mSelectedChooser.canShowIme();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean onBackPressed() {
|
||||
return mSelectedChooser != null && mSelectedChooser.onBackPressed();
|
||||
}
|
||||
|
||||
void invalidateOptionsMenu() {
|
||||
((BugleActionBarActivity) getActivity()).supportInvalidateOptionsMenu();
|
||||
}
|
||||
|
||||
void dispatchOpened() {
|
||||
setHasOptionsMenu(false);
|
||||
mOpen = true;
|
||||
mPagerAdapter.notifyDataSetChanged();
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onOpened();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.onFullScreenChanged(false);
|
||||
mSelectedChooser.onOpenedChanged(true);
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchDismissed() {
|
||||
setHasOptionsMenu(false);
|
||||
mOpen = false;
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onDismissed();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.onOpenedChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchFullScreen(final boolean fullScreen) {
|
||||
setHasOptionsMenu(fullScreen);
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onFullScreenChanged(fullScreen);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.onFullScreenChanged(fullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchItemsSelected(final MessagePartData item, final boolean dismissMediaPicker) {
|
||||
final List<MessagePartData> items = new ArrayList<MessagePartData>(1);
|
||||
items.add(item);
|
||||
dispatchItemsSelected(items, dismissMediaPicker);
|
||||
}
|
||||
|
||||
void dispatchItemsSelected(final Collection<MessagePartData> items,
|
||||
final boolean dismissMediaPicker) {
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onItemsSelected(items, dismissMediaPicker);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isFullScreen() && !dismissMediaPicker) {
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchItemUnselected(final MessagePartData item) {
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onItemUnselected(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isFullScreen()) {
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchConfirmItemSelection() {
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onConfirmItemSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchPendingItemAdded(final PendingAttachmentData pendingItem) {
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onPendingItemAdded(pendingItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isFullScreen()) {
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchChooserSelected(final int chooserIndex) {
|
||||
if (mListener != null) {
|
||||
mListenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mListener.onChooserSelected(chooserIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canSwipeDownChooser() {
|
||||
return mSelectedChooser == null ? false : mSelectedChooser.canSwipeDown();
|
||||
}
|
||||
|
||||
public boolean isChooserHandlingTouch() {
|
||||
return mSelectedChooser == null ? false : mSelectedChooser.isHandlingTouch();
|
||||
}
|
||||
|
||||
public void stopChooserTouchHandling() {
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.stopTouchHandling();
|
||||
}
|
||||
}
|
||||
|
||||
boolean getChooserShowsActionBarInFullScreen() {
|
||||
return mSelectedChooser == null ? false : mSelectedChooser.getActionBarTitleResId() != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.onCreateOptionsMenu(inflater, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
return (mSelectedChooser != null && mSelectedChooser.onOptionsItemSelected(item)) ||
|
||||
super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
PagerAdapter getPagerAdapter() {
|
||||
return mPagerAdapter;
|
||||
}
|
||||
|
||||
public void resetViewHolderState() {
|
||||
mPagerAdapter.resetState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch an external picker to pick item from document picker as attachment.
|
||||
*/
|
||||
public void launchDocumentPicker() {
|
||||
mDocumentImagePicker.launchPicker();
|
||||
}
|
||||
|
||||
public ImmutableBindingRef<MediaPickerData> getMediaPickerDataBinding() {
|
||||
return BindingBase.createBindingReference(mBinding);
|
||||
}
|
||||
|
||||
protected static final int CAMERA_PERMISSION_REQUEST_CODE = 1;
|
||||
protected static final int LOCATION_PERMISSION_REQUEST_CODE = 2;
|
||||
protected static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 3;
|
||||
protected static final int GALLERY_PERMISSION_REQUEST_CODE = 4;
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
final int requestCode, final String permissions[], final int[] grantResults) {
|
||||
if (mSelectedChooser != null) {
|
||||
mSelectedChooser.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.GridView;
|
||||
|
||||
public class MediaPickerGridView extends GridView {
|
||||
|
||||
public MediaPickerGridView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the grid view can be swiped down further. It cannot be swiped down
|
||||
* if there's no item or if we are already at the top.
|
||||
*/
|
||||
public boolean canSwipeDown() {
|
||||
if (getAdapter() == null || getAdapter().getCount() == 0 || getChildCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int firstVisiblePosition = getFirstVisiblePosition();
|
||||
if (firstVisiblePosition == 0 && getChildAt(0).getTop() >= 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,563 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Transformation;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.PagingAwareViewPager;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
|
||||
/**
|
||||
* Custom layout panel which makes the MediaPicker animations seamless and synchronized
|
||||
* Designed to be very specific to the MediaPicker's usage
|
||||
*/
|
||||
public class MediaPickerPanel extends ViewGroup {
|
||||
/**
|
||||
* The window of time in which we might to decide to reinterpret the intent of a gesture.
|
||||
*/
|
||||
private static final long TOUCH_RECAPTURE_WINDOW_MS = 500L;
|
||||
|
||||
// The two view components to layout
|
||||
private LinearLayout mTabStrip;
|
||||
private boolean mFullScreenOnly;
|
||||
private PagingAwareViewPager mViewPager;
|
||||
|
||||
/**
|
||||
* True if the MediaPicker is full screen or animating into it
|
||||
*/
|
||||
private boolean mFullScreen;
|
||||
|
||||
/**
|
||||
* True if the MediaPicker is open at all
|
||||
*/
|
||||
private boolean mExpanded;
|
||||
|
||||
/**
|
||||
* The current desired height of the MediaPicker. This property may be animated and the
|
||||
* measure pass uses it to determine what size the components are.
|
||||
*/
|
||||
private int mCurrentDesiredHeight;
|
||||
|
||||
private final Handler mHandler = new Handler();
|
||||
|
||||
/**
|
||||
* The media picker for dispatching events to the MediaPicker's listener
|
||||
*/
|
||||
private MediaPicker mMediaPicker;
|
||||
|
||||
/**
|
||||
* The computed default "half-screen" height of the view pager in px
|
||||
*/
|
||||
private final int mDefaultViewPagerHeight;
|
||||
|
||||
/**
|
||||
* The action bar height used to compute the padding on the view pager when it's full screen.
|
||||
*/
|
||||
private final int mActionBarHeight;
|
||||
|
||||
private TouchHandler mTouchHandler;
|
||||
|
||||
static final int PAGE_NOT_SET = -1;
|
||||
|
||||
public MediaPickerPanel(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
// Cache the computed dimension
|
||||
mDefaultViewPagerHeight = getResources().getDimensionPixelSize(
|
||||
R.dimen.mediapicker_default_chooser_height);
|
||||
mActionBarHeight = getResources().getDimensionPixelSize(R.dimen.action_bar_height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mTabStrip = (LinearLayout) findViewById(R.id.mediapicker_tabstrip);
|
||||
mViewPager = (PagingAwareViewPager) findViewById(R.id.mediapicker_view_pager);
|
||||
mTouchHandler = new TouchHandler();
|
||||
setOnTouchListener(mTouchHandler);
|
||||
mViewPager.setOnTouchListener(mTouchHandler);
|
||||
|
||||
// Make sure full screen mode is updated in landscape mode change when the panel is open.
|
||||
addOnLayoutChangeListener(new OnLayoutChangeListener() {
|
||||
private boolean mLandscapeMode = UiUtils.isLandscapeMode();
|
||||
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
final boolean newLandscapeMode = UiUtils.isLandscapeMode();
|
||||
if (mLandscapeMode != newLandscapeMode) {
|
||||
mLandscapeMode = newLandscapeMode;
|
||||
if (mExpanded) {
|
||||
setExpanded(mExpanded, false /* animate */, mViewPager.getCurrentItem(),
|
||||
true /* force */);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
int requestedHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
if (mMediaPicker.getChooserShowsActionBarInFullScreen()) {
|
||||
requestedHeight -= mActionBarHeight;
|
||||
}
|
||||
int desiredHeight = Math.min(mCurrentDesiredHeight, requestedHeight);
|
||||
if (mExpanded && desiredHeight == 0) {
|
||||
// If we want to be shown, we have to have a non-0 height. Returning a height of 0 will
|
||||
// cause the framework to abort the animation from 0, so we must always have some
|
||||
// height once we start expanding
|
||||
desiredHeight = 1;
|
||||
} else if (!mExpanded && desiredHeight == 0) {
|
||||
mViewPager.setVisibility(View.GONE);
|
||||
mViewPager.setAdapter(null);
|
||||
}
|
||||
|
||||
measureChild(mTabStrip, widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int tabStripHeight;
|
||||
if (requiresFullScreen()) {
|
||||
// Ensure that the tab strip is always visible, even in full screen.
|
||||
tabStripHeight = mTabStrip.getMeasuredHeight();
|
||||
} else {
|
||||
// Slide out the tab strip at the end of the animation to full screen.
|
||||
tabStripHeight = Math.min(mTabStrip.getMeasuredHeight(),
|
||||
requestedHeight - desiredHeight);
|
||||
}
|
||||
|
||||
// If we are animating and have an interim desired height, use the default height. We can't
|
||||
// take the max here as on some devices the mDefaultViewPagerHeight may be too big in
|
||||
// landscape mode after animation.
|
||||
final int tabAdjustedDesiredHeight = desiredHeight - tabStripHeight;
|
||||
final int viewPagerHeight =
|
||||
tabAdjustedDesiredHeight <= 1 ? mDefaultViewPagerHeight : tabAdjustedDesiredHeight;
|
||||
|
||||
int viewPagerHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
|
||||
viewPagerHeight, MeasureSpec.EXACTLY);
|
||||
measureChild(mViewPager, widthMeasureSpec, viewPagerHeightMeasureSpec);
|
||||
setMeasuredDimension(mViewPager.getMeasuredWidth(), desiredHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int left, final int top, final int right,
|
||||
final int bottom) {
|
||||
int y = top;
|
||||
final int width = right - left;
|
||||
|
||||
final int viewPagerHeight = mViewPager.getMeasuredHeight();
|
||||
mViewPager.layout(0, y, width, y + viewPagerHeight);
|
||||
y += viewPagerHeight;
|
||||
|
||||
mTabStrip.layout(0, y, width, y + mTabStrip.getMeasuredHeight());
|
||||
}
|
||||
|
||||
void onChooserChanged() {
|
||||
if (mFullScreen) {
|
||||
setDesiredHeight(getDesiredHeight(), true);
|
||||
}
|
||||
}
|
||||
|
||||
void setFullScreenOnly(boolean fullScreenOnly) {
|
||||
mFullScreenOnly = fullScreenOnly;
|
||||
}
|
||||
|
||||
boolean isFullScreen() {
|
||||
return mFullScreen;
|
||||
}
|
||||
|
||||
void setMediaPicker(final MediaPicker mediaPicker) {
|
||||
mMediaPicker = mediaPicker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired height of the media picker panel for when the panel is not in motion (i.e.
|
||||
* not being dragged by the user).
|
||||
*/
|
||||
private int getDesiredHeight() {
|
||||
if (mFullScreen) {
|
||||
int fullHeight = getContext().getResources().getDisplayMetrics().heightPixels;
|
||||
if (OsUtil.isAtLeastKLP() && isAttachedToWindow()) {
|
||||
// When we're attached to the window, we can get an accurate height, not necessary
|
||||
// on older API level devices because they don't include the action bar height
|
||||
View composeContainer =
|
||||
getRootView().findViewById(R.id.conversation_and_compose_container);
|
||||
if (composeContainer != null) {
|
||||
// protect against composeContainer having been unloaded already
|
||||
fullHeight -= UiUtils.getMeasuredBoundsOnScreen(composeContainer).top;
|
||||
}
|
||||
}
|
||||
if (mMediaPicker.getChooserShowsActionBarInFullScreen()) {
|
||||
return fullHeight - mActionBarHeight;
|
||||
} else {
|
||||
return fullHeight;
|
||||
}
|
||||
} else if (mExpanded) {
|
||||
return LayoutParams.WRAP_CONTENT;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupViewPager(final int startingPage) {
|
||||
mViewPager.setVisibility(View.VISIBLE);
|
||||
if (startingPage >= 0 && startingPage < mMediaPicker.getPagerAdapter().getCount()) {
|
||||
mViewPager.setAdapter(mMediaPicker.getPagerAdapter());
|
||||
mViewPager.setCurrentItem(startingPage);
|
||||
}
|
||||
updateViewPager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the media picker panel. Since we always set the pager adapter to null when the panel
|
||||
* is collapsed, we need to restore the adapter and the starting page.
|
||||
* @param expanded expanded or collapsed
|
||||
* @param animate need animation
|
||||
* @param startingPage the desired selected page to start
|
||||
*/
|
||||
void setExpanded(final boolean expanded, final boolean animate, final int startingPage) {
|
||||
setExpanded(expanded, animate, startingPage, false /* force */);
|
||||
}
|
||||
|
||||
private void setExpanded(final boolean expanded, final boolean animate, final int startingPage,
|
||||
final boolean force) {
|
||||
if (expanded == mExpanded && !force) {
|
||||
return;
|
||||
}
|
||||
mFullScreen = false;
|
||||
mExpanded = expanded;
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setDesiredHeight(getDesiredHeight(), animate);
|
||||
}
|
||||
});
|
||||
if (expanded) {
|
||||
setupViewPager(startingPage);
|
||||
mMediaPicker.dispatchOpened();
|
||||
} else {
|
||||
mMediaPicker.dispatchDismissed();
|
||||
}
|
||||
|
||||
// Call setFullScreenView() when we are in landscape mode so it can go full screen as
|
||||
// soon as it is expanded.
|
||||
if (expanded && requiresFullScreen()) {
|
||||
setFullScreenView(true, animate);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean requiresFullScreen() {
|
||||
return mFullScreenOnly || UiUtils.isLandscapeMode();
|
||||
}
|
||||
|
||||
private void setDesiredHeight(int height, final boolean animate) {
|
||||
final int startHeight = mCurrentDesiredHeight;
|
||||
if (height == LayoutParams.WRAP_CONTENT) {
|
||||
height = measureHeight();
|
||||
}
|
||||
clearAnimation();
|
||||
if (animate) {
|
||||
final int deltaHeight = height - startHeight;
|
||||
final Animation animation = new Animation() {
|
||||
@Override
|
||||
protected void applyTransformation(final float interpolatedTime,
|
||||
final Transformation t) {
|
||||
mCurrentDesiredHeight = (int) (startHeight + deltaHeight * interpolatedTime);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willChangeBounds() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
animation.setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION);
|
||||
animation.setInterpolator(UiUtils.EASE_OUT_INTERPOLATOR);
|
||||
startAnimation(animation);
|
||||
} else {
|
||||
mCurrentDesiredHeight = height;
|
||||
}
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The minimum total height of the view
|
||||
*/
|
||||
private int measureHeight() {
|
||||
final int measureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST);
|
||||
measureChild(mTabStrip, measureSpec, measureSpec);
|
||||
return mDefaultViewPagerHeight + mTabStrip.getMeasuredHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters or leaves full screen view
|
||||
*
|
||||
* @param fullScreen True to enter full screen view, false to leave
|
||||
* @param animate True to animate the transition
|
||||
*/
|
||||
void setFullScreenView(final boolean fullScreen, final boolean animate) {
|
||||
if (fullScreen == mFullScreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiresFullScreen() && !fullScreen) {
|
||||
setExpanded(false /* expanded */, true /* animate */, PAGE_NOT_SET);
|
||||
return;
|
||||
}
|
||||
mFullScreen = fullScreen;
|
||||
setDesiredHeight(getDesiredHeight(), animate);
|
||||
mMediaPicker.dispatchFullScreen(mFullScreen);
|
||||
updateViewPager();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewPager should have its paging disabled when in full screen mode.
|
||||
*/
|
||||
private void updateViewPager() {
|
||||
mViewPager.setPagingEnabled(!mFullScreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(final MotionEvent ev) {
|
||||
return mTouchHandler.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to handle touch events and swipe gestures
|
||||
*/
|
||||
private class TouchHandler implements OnTouchListener {
|
||||
/**
|
||||
* The height of the view when the touch press started
|
||||
*/
|
||||
private int mDownHeight = -1;
|
||||
|
||||
/**
|
||||
* True if the panel moved at all (changed height) during the drag
|
||||
*/
|
||||
private boolean mMoved = false;
|
||||
|
||||
// The threshold constants converted from DP to px
|
||||
private final float mFlingThresholdPx;
|
||||
private final float mBigFlingThresholdPx;
|
||||
|
||||
// The system defined pixel size to determine when a movement is considered a drag.
|
||||
private final int mTouchSlop;
|
||||
|
||||
/**
|
||||
* A copy of the MotionEvent that started the drag/swipe gesture
|
||||
*/
|
||||
private MotionEvent mDownEvent;
|
||||
|
||||
/**
|
||||
* Whether we are currently moving down. We may not be able to move down in full screen
|
||||
* mode when the child view can swipe down (such as a list view).
|
||||
*/
|
||||
private boolean mMovedDown = false;
|
||||
|
||||
/**
|
||||
* Indicates whether the child view contained in the panel can swipe down at the beginning
|
||||
* of the drag event (i.e. the initial down). The MediaPanel can contain
|
||||
* scrollable children such as a list view / grid view. If the child view can swipe down,
|
||||
* We want to let the child view handle the scroll first instead of handling it ourselves.
|
||||
*/
|
||||
private boolean mCanChildViewSwipeDown = false;
|
||||
|
||||
/**
|
||||
* Necessary direction ratio for a fling to be considered in one direction this prevents
|
||||
* horizontal swipes with small vertical components from triggering vertical swipe actions
|
||||
*/
|
||||
private static final float DIRECTION_RATIO = 1.1f;
|
||||
|
||||
TouchHandler() {
|
||||
final Resources resources = getContext().getResources();
|
||||
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
|
||||
mFlingThresholdPx = resources.getDimensionPixelSize(
|
||||
R.dimen.mediapicker_fling_threshold);
|
||||
mBigFlingThresholdPx = resources.getDimensionPixelSize(
|
||||
R.dimen.mediapicker_big_fling_threshold);
|
||||
mTouchSlop = configuration.getScaledTouchSlop();
|
||||
}
|
||||
|
||||
/**
|
||||
* The media picker panel may contain scrollable children such as a GridView, which eats
|
||||
* all touch events before we get to it. Therefore, we'd like to intercept these events
|
||||
* before the children to determine if we should handle swiping down in full screen mode.
|
||||
* In non-full screen mode, we should handle all vertical scrolling events and leave
|
||||
* horizontal scrolling to the view pager.
|
||||
*/
|
||||
public boolean onInterceptTouchEvent(final MotionEvent ev) {
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Never capture the initial down, so that the children may handle it
|
||||
// as well. Let the touch handler know about the down event as well.
|
||||
mTouchHandler.onTouch(MediaPickerPanel.this, ev);
|
||||
|
||||
// Ask the MediaPicker whether the contained view can be swiped down.
|
||||
// We record the value at the start of the drag to decide the swiping mode
|
||||
// for the entire motion.
|
||||
mCanChildViewSwipeDown = mMediaPicker.canSwipeDownChooser();
|
||||
return false;
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (mMediaPicker.isChooserHandlingTouch()) {
|
||||
if (shouldAllowRecaptureTouch(ev)) {
|
||||
mMediaPicker.stopChooserTouchHandling();
|
||||
mViewPager.setPagingEnabled(true);
|
||||
return false;
|
||||
}
|
||||
// If the chooser is claiming ownership on all touch events, then we
|
||||
// shouldn't try to handle them (neither should the view pager).
|
||||
mViewPager.setPagingEnabled(false);
|
||||
return false;
|
||||
} else if (mCanChildViewSwipeDown) {
|
||||
// Never capture event if the child view can swipe down.
|
||||
return false;
|
||||
} else if (!mFullScreen && mMoved) {
|
||||
// When we are not fullscreen, we own any vertical drag motion.
|
||||
return true;
|
||||
} else if (mMovedDown) {
|
||||
// We are currently handling the down swipe ourselves, so always
|
||||
// capture this event.
|
||||
return true;
|
||||
} else {
|
||||
// The current interaction mode is undetermined, so always let the
|
||||
// touch handler know about this event. However, don't capture this
|
||||
// event so the child may handle it as well.
|
||||
mTouchHandler.onTouch(MediaPickerPanel.this, ev);
|
||||
|
||||
// Capture the touch event from now on if we are handling the drag.
|
||||
return mFullScreen ? mMovedDown : mMoved;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether we think the user is actually trying to expand or slide despite the
|
||||
* fact that they touched first on a chooser that captured the input.
|
||||
*/
|
||||
private boolean shouldAllowRecaptureTouch(MotionEvent ev) {
|
||||
final long elapsedMs = ev.getEventTime() - ev.getDownTime();
|
||||
if (mDownEvent == null || elapsedMs == 0 || elapsedMs > TOUCH_RECAPTURE_WINDOW_MS) {
|
||||
// Either we don't have info to decide or it's been long enough that we no longer
|
||||
// want to reinterpret user intent.
|
||||
return false;
|
||||
}
|
||||
final float dx = ev.getRawX() - mDownEvent.getRawX();
|
||||
final float dy = ev.getRawY() - mDownEvent.getRawY();
|
||||
final float dt = elapsedMs / 1000.0f;
|
||||
final float maxAbsDelta = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
final float velocity = maxAbsDelta / dt;
|
||||
return velocity > mFlingThresholdPx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(final View view, final MotionEvent motionEvent) {
|
||||
switch (motionEvent.getAction()) {
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (!mMoved || mDownEvent == null) {
|
||||
return false;
|
||||
}
|
||||
final float dx = motionEvent.getRawX() - mDownEvent.getRawX();
|
||||
final float dy = motionEvent.getRawY() - mDownEvent.getRawY();
|
||||
|
||||
final float dt =
|
||||
(motionEvent.getEventTime() - mDownEvent.getEventTime()) / 1000.0f;
|
||||
final float yVelocity = dy / dt;
|
||||
|
||||
boolean handled = false;
|
||||
|
||||
// Vertical swipe occurred if the direction is as least mostly in the y
|
||||
// component and has the required velocity (px/sec)
|
||||
if ((dx == 0 || (Math.abs(dy) / Math.abs(dx)) > DIRECTION_RATIO) &&
|
||||
Math.abs(yVelocity) > mFlingThresholdPx) {
|
||||
if (yVelocity < 0 && mExpanded) {
|
||||
setFullScreenView(true, true);
|
||||
handled = true;
|
||||
} else if (yVelocity > 0) {
|
||||
if (mFullScreen && yVelocity < mBigFlingThresholdPx) {
|
||||
setFullScreenView(false, true);
|
||||
} else {
|
||||
setExpanded(false, true, PAGE_NOT_SET);
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
// If they didn't swipe enough, animate back to resting state
|
||||
setDesiredHeight(getDesiredHeight(), true);
|
||||
}
|
||||
resetState();
|
||||
break;
|
||||
}
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
mDownHeight = getHeight();
|
||||
mDownEvent = MotionEvent.obtain(motionEvent);
|
||||
// If we are here and care about the return value (i.e. this is not called
|
||||
// from onInterceptTouchEvent), then presumably no children view in the panel
|
||||
// handles the down event. We'd like to handle future ACTION_MOVE events, so
|
||||
// always claim ownership on this event so it doesn't fall through and gets
|
||||
// cancelled by the framework.
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (mDownEvent == null) {
|
||||
return mMoved;
|
||||
}
|
||||
|
||||
final float dx = mDownEvent.getRawX() - motionEvent.getRawX();
|
||||
final float dy = mDownEvent.getRawY() - motionEvent.getRawY();
|
||||
// Don't act if the move is mostly horizontal
|
||||
if (Math.abs(dy) > mTouchSlop &&
|
||||
(Math.abs(dy) / Math.abs(dx)) > DIRECTION_RATIO) {
|
||||
setDesiredHeight((int) (mDownHeight + dy), false);
|
||||
mMoved = true;
|
||||
if (dy < -mTouchSlop) {
|
||||
mMovedDown = true;
|
||||
}
|
||||
}
|
||||
return mMoved;
|
||||
}
|
||||
|
||||
}
|
||||
return mMoved;
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
mDownEvent = null;
|
||||
mDownHeight = -1;
|
||||
mMoved = false;
|
||||
mMovedDown = false;
|
||||
mCanChildViewSwipeDown = false;
|
||||
updateViewPager();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.hardware.Camera;
|
||||
import android.media.CamcorderProfile;
|
||||
import android.media.MediaRecorder;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||
import com.android.messaging.util.ContentType;
|
||||
import com.android.messaging.util.SafeAsyncTask;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
class MmsVideoRecorder extends MediaRecorder {
|
||||
private static final float VIDEO_OVERSHOOT_SLOP = .85F;
|
||||
|
||||
private static final int BITS_PER_BYTE = 8;
|
||||
|
||||
// We think user will expect to be able to record videos at least this long
|
||||
private static final long MIN_DURATION_LIMIT_SECONDS = 25;
|
||||
|
||||
/** The uri where video is being recorded to */
|
||||
private Uri mTempVideoUri;
|
||||
|
||||
/** The settings used for video recording */
|
||||
private final CamcorderProfile mCamcorderProfile;
|
||||
|
||||
public MmsVideoRecorder(final Camera camera, final int cameraIndex, final int orientation,
|
||||
final int maxMessageSize)
|
||||
throws FileNotFoundException {
|
||||
mCamcorderProfile =
|
||||
CamcorderProfile.get(cameraIndex, CamcorderProfile.QUALITY_LOW);
|
||||
mTempVideoUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(
|
||||
ContentType.getExtension(getContentType()));
|
||||
|
||||
// The video recorder can sometimes return a file that's larger than the max we
|
||||
// say we can handle. Try to handle that overshoot by specifying an 85% limit.
|
||||
final long sizeLimit = (long) (maxMessageSize * VIDEO_OVERSHOOT_SLOP);
|
||||
|
||||
// The QUALITY_LOW profile might not be low enough to allow for video of a reasonable
|
||||
// minimum duration. Adjust a/v bitrates to allow at least MIN_DURATION_LIMIT video
|
||||
// to be recorded.
|
||||
int audioBitRate = mCamcorderProfile.audioBitRate;
|
||||
int videoBitRate = mCamcorderProfile.videoBitRate;
|
||||
final double initialDurationLimit = sizeLimit * BITS_PER_BYTE
|
||||
/ (double) (audioBitRate + videoBitRate);
|
||||
if (initialDurationLimit < MIN_DURATION_LIMIT_SECONDS) {
|
||||
// Reduce the suggested bitrates. These bitrates are only requests, if implementation
|
||||
// can't actually hit these goals it will still record video at higher rate and stop when
|
||||
// it hits the size limit.
|
||||
final double bitRateAdjustmentFactor = initialDurationLimit / MIN_DURATION_LIMIT_SECONDS;
|
||||
audioBitRate *= bitRateAdjustmentFactor;
|
||||
videoBitRate *= bitRateAdjustmentFactor;
|
||||
}
|
||||
|
||||
setCamera(camera);
|
||||
setOrientationHint(orientation);
|
||||
setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
|
||||
setVideoSource(MediaRecorder.VideoSource.CAMERA);
|
||||
setOutputFormat(mCamcorderProfile.fileFormat);
|
||||
setOutputFile(
|
||||
Factory.get().getApplicationContext().getContentResolver().openFileDescriptor(
|
||||
mTempVideoUri, "w").getFileDescriptor());
|
||||
|
||||
// Copy settings from CamcorderProfile to MediaRecorder
|
||||
setAudioEncodingBitRate(audioBitRate);
|
||||
setAudioChannels(mCamcorderProfile.audioChannels);
|
||||
setAudioEncoder(mCamcorderProfile.audioCodec);
|
||||
setAudioSamplingRate(mCamcorderProfile.audioSampleRate);
|
||||
setVideoEncodingBitRate(videoBitRate);
|
||||
setVideoEncoder(mCamcorderProfile.videoCodec);
|
||||
setVideoFrameRate(mCamcorderProfile.videoFrameRate);
|
||||
setVideoSize(
|
||||
mCamcorderProfile.videoFrameWidth, mCamcorderProfile.videoFrameHeight);
|
||||
setMaxFileSize(sizeLimit);
|
||||
}
|
||||
|
||||
Uri getVideoUri() {
|
||||
return mTempVideoUri;
|
||||
}
|
||||
|
||||
int getVideoWidth() {
|
||||
return mCamcorderProfile.videoFrameWidth;
|
||||
}
|
||||
|
||||
int getVideoHeight() {
|
||||
return mCamcorderProfile.videoFrameHeight;
|
||||
}
|
||||
|
||||
void cleanupTempFile() {
|
||||
final Uri tempUri = mTempVideoUri;
|
||||
SafeAsyncTask.executeOnThreadPool(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Factory.get().getApplicationContext().getContentResolver().delete(
|
||||
tempUri, null, null);
|
||||
}
|
||||
});
|
||||
mTempVideoUri = null;
|
||||
}
|
||||
|
||||
String getContentType() {
|
||||
if (mCamcorderProfile.fileFormat == OutputFormat.MPEG_4) {
|
||||
return ContentType.VIDEO_MP4;
|
||||
} else {
|
||||
// 3GPP is the only other video format with a constant in OutputFormat
|
||||
return ContentType.VIDEO_3GPP;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Chronometer;
|
||||
|
||||
import com.android.messaging.ui.PlaybackStateView;
|
||||
|
||||
/**
|
||||
* A pausable Chronometer implementation. The default Chronometer in Android only stops the UI
|
||||
* from updating when you call stop(), but doesn't actually pause it. This implementation adds an
|
||||
* additional timestamp that tracks the timespan for the pause and compensate for that.
|
||||
*/
|
||||
public class PausableChronometer extends Chronometer implements PlaybackStateView {
|
||||
// Keeps track of how far long the Chronometer has been tracking when it's paused. We'd like
|
||||
// to start from this time the next time it's resumed.
|
||||
private long mTimeWhenPaused = 0;
|
||||
|
||||
public PausableChronometer(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the timer and start counting from zero.
|
||||
*/
|
||||
@Override
|
||||
public void restart() {
|
||||
reset();
|
||||
start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the timer to zero, but don't start it.
|
||||
*/
|
||||
@Override
|
||||
public void reset() {
|
||||
stop();
|
||||
setBase(SystemClock.elapsedRealtime());
|
||||
mTimeWhenPaused = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the timer after a previous pause.
|
||||
*/
|
||||
@Override
|
||||
public void resume() {
|
||||
setBase(SystemClock.elapsedRealtime() - mTimeWhenPaused);
|
||||
start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the timer.
|
||||
*/
|
||||
@Override
|
||||
public void pause() {
|
||||
stop();
|
||||
mTimeWhenPaused = SystemClock.elapsedRealtime() - getBase();
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Camera;
|
||||
import android.os.Parcelable;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A software rendered preview surface for the camera. This renders slower and causes more jank, so
|
||||
* HardwareCameraPreview is preferred if possible.
|
||||
*
|
||||
* There is a significant amount of duplication between HardwareCameraPreview and
|
||||
* SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
|
||||
* implementations of the shared methods are delegated to CameraPreview
|
||||
*/
|
||||
public class SoftwareCameraPreview extends SurfaceView implements CameraPreview.CameraPreviewHost {
|
||||
private final CameraPreview mPreview;
|
||||
|
||||
public SoftwareCameraPreview(final Context context) {
|
||||
super(context);
|
||||
mPreview = new CameraPreview(this);
|
||||
getHolder().addCallback(new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceCreated(final SurfaceHolder surfaceHolder) {
|
||||
CameraManager.get().setSurface(mPreview);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(final SurfaceHolder surfaceHolder, final int format, final int width,
|
||||
final int height) {
|
||||
CameraManager.get().setSurface(mPreview);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(final SurfaceHolder surfaceHolder) {
|
||||
CameraManager.get().setSurface(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onVisibilityChanged(final View changedView, final int visibility) {
|
||||
super.onVisibilityChanged(changedView, visibility);
|
||||
mPreview.onVisibilityChanged(visibility);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mPreview.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
mPreview.onAttachedToWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Parcelable state) {
|
||||
super.onRestoreInstanceState(state);
|
||||
mPreview.onRestoreInstanceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
|
||||
heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return getHolder() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPreview(final Camera camera) throws IOException {
|
||||
camera.setPreviewDisplay(getHolder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraPermissionGranted() {
|
||||
mPreview.onCameraPermissionGranted();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.mediapicker;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TimeAnimator;
|
||||
import android.animation.TimeAnimator.TimeListener;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* This view draws circular sound levels. By default the sound levels are black, unless
|
||||
* otherwise defined via {@link #mPrimaryLevelPaint}.
|
||||
*/
|
||||
public class SoundLevels extends View {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private boolean mCenterDefined;
|
||||
private int mCenterX;
|
||||
private int mCenterY;
|
||||
|
||||
// Paint for the main level meter, most closely follows the mic.
|
||||
private final Paint mPrimaryLevelPaint;
|
||||
|
||||
// The minimum size of the levels as a percentage of the max, that is the size when volume is 0.
|
||||
private final float mMinimumLevel;
|
||||
|
||||
// The minimum size of the levels, that is the size when volume is 0.
|
||||
private final float mMinimumLevelSize;
|
||||
|
||||
// The maximum size of the levels, that is the size when volume is 100.
|
||||
private final float mMaximumLevelSize;
|
||||
|
||||
// Generates clock ticks for the animation using the global animation loop.
|
||||
private final TimeAnimator mSpeechLevelsAnimator;
|
||||
|
||||
private float mCurrentVolume;
|
||||
|
||||
// Indicates whether we should be animating the sound level.
|
||||
private boolean mIsEnabled;
|
||||
|
||||
// Input level is pulled from here.
|
||||
private AudioLevelSource mLevelSource;
|
||||
|
||||
public SoundLevels(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SoundLevels(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public SoundLevels(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
// Safe source, replaced with system one when attached.
|
||||
mLevelSource = new AudioLevelSource();
|
||||
mLevelSource.setSpeechLevel(0);
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SoundLevels,
|
||||
defStyle, 0);
|
||||
|
||||
mMaximumLevelSize = a.getDimensionPixelOffset(
|
||||
R.styleable.SoundLevels_maxLevelRadius, 0);
|
||||
mMinimumLevelSize = a.getDimensionPixelOffset(
|
||||
R.styleable.SoundLevels_minLevelRadius, 0);
|
||||
mMinimumLevel = mMinimumLevelSize / mMaximumLevelSize;
|
||||
|
||||
mPrimaryLevelPaint = new Paint();
|
||||
mPrimaryLevelPaint.setColor(
|
||||
a.getColor(R.styleable.SoundLevels_primaryColor, Color.BLACK));
|
||||
mPrimaryLevelPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
a.recycle();
|
||||
|
||||
// This animator generates ticks that invalidate the
|
||||
// view so that the animation is synced with the global animation loop.
|
||||
// TODO: We could probably remove this in favor of using postInvalidateOnAnimation
|
||||
// which might improve things further.
|
||||
mSpeechLevelsAnimator = new TimeAnimator();
|
||||
mSpeechLevelsAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
mSpeechLevelsAnimator.setTimeListener(new TimeListener() {
|
||||
@Override
|
||||
public void onTimeUpdate(final TimeAnimator animation, final long totalTime,
|
||||
final long deltaTime) {
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(final Canvas canvas) {
|
||||
if (!mIsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mCenterDefined) {
|
||||
// One time computation here, because we can't rely on getWidth() to be computed at
|
||||
// constructor time or in onFinishInflate :(.
|
||||
mCenterX = getWidth() / 2;
|
||||
mCenterY = getWidth() / 2;
|
||||
mCenterDefined = true;
|
||||
}
|
||||
|
||||
final int level = mLevelSource.getSpeechLevel();
|
||||
// Either ease towards the target level, or decay away from it depending on whether
|
||||
// its higher or lower than the current.
|
||||
if (level > mCurrentVolume) {
|
||||
mCurrentVolume = mCurrentVolume + ((level - mCurrentVolume) / 4);
|
||||
} else {
|
||||
mCurrentVolume = mCurrentVolume * 0.95f;
|
||||
}
|
||||
|
||||
final float radius = mMinimumLevel + (1f - mMinimumLevel) * mCurrentVolume / 100;
|
||||
mPrimaryLevelPaint.setStyle(Style.FILL);
|
||||
canvas.drawCircle(mCenterX, mCenterY, radius * mMaximumLevelSize, mPrimaryLevelPaint);
|
||||
}
|
||||
|
||||
public void setLevelSource(final AudioLevelSource source) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Speech source set.");
|
||||
}
|
||||
mLevelSource = source;
|
||||
}
|
||||
|
||||
private void startSpeechLevelsAnimator() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "startAnimator()");
|
||||
}
|
||||
if (!mSpeechLevelsAnimator.isStarted()) {
|
||||
mSpeechLevelsAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSpeechLevelsAnimator() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stopAnimator()");
|
||||
}
|
||||
if (mSpeechLevelsAnimator.isStarted()) {
|
||||
mSpeechLevelsAnimator.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
stopSpeechLevelsAnimator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(final boolean enabled) {
|
||||
if (enabled == mIsEnabled) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d("TAG", "setEnabled: " + enabled);
|
||||
}
|
||||
super.setEnabled(enabled);
|
||||
mIsEnabled = enabled;
|
||||
setKeepScreenOn(enabled);
|
||||
updateSpeechLevelsAnimatorState();
|
||||
}
|
||||
|
||||
private void updateSpeechLevelsAnimatorState() {
|
||||
if (mIsEnabled) {
|
||||
startSpeechLevelsAnimator();
|
||||
} else {
|
||||
stopSpeechLevelsAnimator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is required to make the View findable by uiautomator
|
||||
*/
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(final AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setClassName(SoundLevels.class.getCanonicalName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the alpha level of the sound circles.
|
||||
*/
|
||||
public void setPrimaryColorAlpha(final int alpha) {
|
||||
mPrimaryLevelPaint.setAlpha(alpha);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker.camerafocus;
|
||||
|
||||
public interface FocusIndicator {
|
||||
public void showStart();
|
||||
public void showSuccess(boolean timeout);
|
||||
public void showFail(boolean timeout);
|
||||
public void clear();
|
||||
}
|
||||
@@ -1,589 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker.camerafocus;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.hardware.Camera.Area;
|
||||
import android.hardware.Camera.Parameters;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/* A class that handles everything about focus in still picture mode.
|
||||
* This also handles the metering area because it is the same as focus area.
|
||||
*
|
||||
* The test cases:
|
||||
* (1) The camera has continuous autofocus. Move the camera. Take a picture when
|
||||
* CAF is not in progress.
|
||||
* (2) The camera has continuous autofocus. Move the camera. Take a picture when
|
||||
* CAF is in progress.
|
||||
* (3) The camera has face detection. Point the camera at some faces. Hold the
|
||||
* shutter. Release to take a picture.
|
||||
* (4) The camera has face detection. Point the camera at some faces. Single tap
|
||||
* the shutter to take a picture.
|
||||
* (5) The camera has autofocus. Single tap the shutter to take a picture.
|
||||
* (6) The camera has autofocus. Hold the shutter. Release to take a picture.
|
||||
* (7) The camera has no autofocus. Single tap the shutter and take a picture.
|
||||
* (8) The camera has autofocus and supports focus area. Touch the screen to
|
||||
* trigger autofocus. Take a picture.
|
||||
* (9) The camera has autofocus and supports focus area. Touch the screen to
|
||||
* trigger autofocus. Wait until it times out.
|
||||
* (10) The camera has no autofocus and supports metering area. Touch the screen
|
||||
* to change metering area.
|
||||
*/
|
||||
public class FocusOverlayManager {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
private static final String TRUE = "true";
|
||||
private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
|
||||
private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED =
|
||||
"auto-whitebalance-lock-supported";
|
||||
|
||||
private static final int RESET_TOUCH_FOCUS = 0;
|
||||
private static final int RESET_TOUCH_FOCUS_DELAY = 3000;
|
||||
|
||||
private int mState = STATE_IDLE;
|
||||
private static final int STATE_IDLE = 0; // Focus is not active.
|
||||
private static final int STATE_FOCUSING = 1; // Focus is in progress.
|
||||
// Focus is in progress and the camera should take a picture after focus finishes.
|
||||
private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
|
||||
private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
|
||||
private static final int STATE_FAIL = 4; // Focus finishes and fails.
|
||||
|
||||
private boolean mInitialized;
|
||||
private boolean mFocusAreaSupported;
|
||||
private boolean mMeteringAreaSupported;
|
||||
private boolean mLockAeAwbNeeded;
|
||||
private boolean mAeAwbLock;
|
||||
private Matrix mMatrix;
|
||||
|
||||
private PieRenderer mPieRenderer;
|
||||
|
||||
private int mPreviewWidth; // The width of the preview frame layout.
|
||||
private int mPreviewHeight; // The height of the preview frame layout.
|
||||
private boolean mMirror; // true if the camera is front-facing.
|
||||
private int mDisplayOrientation;
|
||||
private List<Object> mFocusArea; // focus area in driver format
|
||||
private List<Object> mMeteringArea; // metering area in driver format
|
||||
private String mFocusMode;
|
||||
private String mOverrideFocusMode;
|
||||
private Parameters mParameters;
|
||||
private Handler mHandler;
|
||||
Listener mListener;
|
||||
|
||||
public interface Listener {
|
||||
public void autoFocus();
|
||||
public void cancelAutoFocus();
|
||||
public boolean capture();
|
||||
public void setFocusParameters();
|
||||
}
|
||||
|
||||
private class MainHandler extends Handler {
|
||||
public MainHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case RESET_TOUCH_FOCUS: {
|
||||
cancelAutoFocus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FocusOverlayManager(Listener listener, Looper looper) {
|
||||
mHandler = new MainHandler(looper);
|
||||
mMatrix = new Matrix();
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void setFocusRenderer(PieRenderer renderer) {
|
||||
mPieRenderer = renderer;
|
||||
mInitialized = (mMatrix != null);
|
||||
}
|
||||
|
||||
public void setParameters(Parameters parameters) {
|
||||
// parameters can only be null when onConfigurationChanged is called
|
||||
// before camera is open. We will just return in this case, because
|
||||
// parameters will be set again later with the right parameters after
|
||||
// camera is open.
|
||||
if (parameters == null) {
|
||||
return;
|
||||
}
|
||||
mParameters = parameters;
|
||||
mFocusAreaSupported = isFocusAreaSupported(parameters);
|
||||
mMeteringAreaSupported = isMeteringAreaSupported(parameters);
|
||||
mLockAeAwbNeeded = (isAutoExposureLockSupported(mParameters) ||
|
||||
isAutoWhiteBalanceLockSupported(mParameters));
|
||||
}
|
||||
|
||||
public void setPreviewSize(int previewWidth, int previewHeight) {
|
||||
if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) {
|
||||
mPreviewWidth = previewWidth;
|
||||
mPreviewHeight = previewHeight;
|
||||
setMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMirror(boolean mirror) {
|
||||
mMirror = mirror;
|
||||
setMatrix();
|
||||
}
|
||||
|
||||
public void setDisplayOrientation(int displayOrientation) {
|
||||
mDisplayOrientation = displayOrientation;
|
||||
setMatrix();
|
||||
}
|
||||
|
||||
private void setMatrix() {
|
||||
if (mPreviewWidth != 0 && mPreviewHeight != 0) {
|
||||
Matrix matrix = new Matrix();
|
||||
prepareMatrix(matrix, mMirror, mDisplayOrientation,
|
||||
mPreviewWidth, mPreviewHeight);
|
||||
// In face detection, the matrix converts the driver coordinates to UI
|
||||
// coordinates. In tap focus, the inverted matrix converts the UI
|
||||
// coordinates to driver coordinates.
|
||||
matrix.invert(mMatrix);
|
||||
mInitialized = (mPieRenderer != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void lockAeAwbIfNeeded() {
|
||||
if (mLockAeAwbNeeded && !mAeAwbLock) {
|
||||
mAeAwbLock = true;
|
||||
mListener.setFocusParameters();
|
||||
}
|
||||
}
|
||||
|
||||
private void unlockAeAwbIfNeeded() {
|
||||
if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) {
|
||||
mAeAwbLock = false;
|
||||
mListener.setFocusParameters();
|
||||
}
|
||||
}
|
||||
|
||||
public void onShutterDown() {
|
||||
if (!mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean autoFocusCalled = false;
|
||||
if (needAutoFocusCall()) {
|
||||
// Do not focus if touch focus has been triggered.
|
||||
if (mState != STATE_SUCCESS && mState != STATE_FAIL) {
|
||||
autoFocus();
|
||||
autoFocusCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!autoFocusCalled) {
|
||||
lockAeAwbIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
public void onShutterUp() {
|
||||
if (!mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (needAutoFocusCall()) {
|
||||
// User releases half-pressed focus key.
|
||||
if (mState == STATE_FOCUSING || mState == STATE_SUCCESS
|
||||
|| mState == STATE_FAIL) {
|
||||
cancelAutoFocus();
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock AE and AWB after cancelAutoFocus. Camera API does not
|
||||
// guarantee setParameters can be called during autofocus.
|
||||
unlockAeAwbIfNeeded();
|
||||
}
|
||||
|
||||
public void doSnap() {
|
||||
if (!mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has half-pressed the shutter and focus is completed, we
|
||||
// can take the photo right away. If the focus mode is infinity, we can
|
||||
// also take the photo.
|
||||
if (!needAutoFocusCall() || (mState == STATE_SUCCESS || mState == STATE_FAIL)) {
|
||||
capture();
|
||||
} else if (mState == STATE_FOCUSING) {
|
||||
// Half pressing the shutter (i.e. the focus button event) will
|
||||
// already have requested AF for us, so just request capture on
|
||||
// focus here.
|
||||
mState = STATE_FOCUSING_SNAP_ON_FINISH;
|
||||
} else if (mState == STATE_IDLE) {
|
||||
// We didn't do focus. This can happen if the user press focus key
|
||||
// while the snapshot is still in progress. The user probably wants
|
||||
// the next snapshot as soon as possible, so we just do a snapshot
|
||||
// without focusing again.
|
||||
capture();
|
||||
}
|
||||
}
|
||||
|
||||
public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
|
||||
if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
|
||||
// Take the picture no matter focus succeeds or fails. No need
|
||||
// to play the AF sound if we're about to play the shutter
|
||||
// sound.
|
||||
if (focused) {
|
||||
mState = STATE_SUCCESS;
|
||||
} else {
|
||||
mState = STATE_FAIL;
|
||||
}
|
||||
updateFocusUI();
|
||||
capture();
|
||||
} else if (mState == STATE_FOCUSING) {
|
||||
// This happens when (1) user is half-pressing the focus key or
|
||||
// (2) touch focus is triggered. Play the focus tone. Do not
|
||||
// take the picture now.
|
||||
if (focused) {
|
||||
mState = STATE_SUCCESS;
|
||||
} else {
|
||||
mState = STATE_FAIL;
|
||||
}
|
||||
updateFocusUI();
|
||||
// If this is triggered by touch focus, cancel focus after a
|
||||
// while.
|
||||
if (mFocusArea != null) {
|
||||
mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
|
||||
}
|
||||
if (shutterButtonPressed) {
|
||||
// Lock AE & AWB so users can half-press shutter and recompose.
|
||||
lockAeAwbIfNeeded();
|
||||
}
|
||||
} else if (mState == STATE_IDLE) {
|
||||
// User has released the focus key before focus completes.
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
public void onAutoFocusMoving(boolean moving) {
|
||||
if (!mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if we have requested autofocus. This method only handles
|
||||
// continuous autofocus.
|
||||
if (mState != STATE_IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (moving) {
|
||||
mPieRenderer.showStart();
|
||||
} else {
|
||||
mPieRenderer.showSuccess(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeFocusAreas(int focusWidth, int focusHeight,
|
||||
int x, int y, int previewWidth, int previewHeight) {
|
||||
if (mFocusArea == null) {
|
||||
mFocusArea = new ArrayList<Object>();
|
||||
mFocusArea.add(new Area(new Rect(), 1));
|
||||
}
|
||||
|
||||
// Convert the coordinates to driver format.
|
||||
calculateTapArea(focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight,
|
||||
((Area) mFocusArea.get(0)).rect);
|
||||
}
|
||||
|
||||
private void initializeMeteringAreas(int focusWidth, int focusHeight,
|
||||
int x, int y, int previewWidth, int previewHeight) {
|
||||
if (mMeteringArea == null) {
|
||||
mMeteringArea = new ArrayList<Object>();
|
||||
mMeteringArea.add(new Area(new Rect(), 1));
|
||||
}
|
||||
|
||||
// Convert the coordinates to driver format.
|
||||
// AE area is bigger because exposure is sensitive and
|
||||
// easy to over- or underexposure if area is too small.
|
||||
calculateTapArea(focusWidth, focusHeight, 1.5f, x, y, previewWidth, previewHeight,
|
||||
((Area) mMeteringArea.get(0)).rect);
|
||||
}
|
||||
|
||||
public void onSingleTapUp(int x, int y) {
|
||||
if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Let users be able to cancel previous touch focus.
|
||||
if ((mFocusArea != null) && (mState == STATE_FOCUSING ||
|
||||
mState == STATE_SUCCESS || mState == STATE_FAIL)) {
|
||||
cancelAutoFocus();
|
||||
}
|
||||
// Initialize variables.
|
||||
int focusWidth = mPieRenderer.getSize();
|
||||
int focusHeight = mPieRenderer.getSize();
|
||||
if (focusWidth == 0 || mPieRenderer.getWidth() == 0 || mPieRenderer.getHeight() == 0) {
|
||||
return;
|
||||
}
|
||||
int previewWidth = mPreviewWidth;
|
||||
int previewHeight = mPreviewHeight;
|
||||
// Initialize mFocusArea.
|
||||
if (mFocusAreaSupported) {
|
||||
initializeFocusAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
|
||||
}
|
||||
// Initialize mMeteringArea.
|
||||
if (mMeteringAreaSupported) {
|
||||
initializeMeteringAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
|
||||
}
|
||||
|
||||
// Use margin to set the focus indicator to the touched area.
|
||||
mPieRenderer.setFocus(x, y);
|
||||
|
||||
// Set the focus area and metering area.
|
||||
mListener.setFocusParameters();
|
||||
if (mFocusAreaSupported) {
|
||||
autoFocus();
|
||||
} else { // Just show the indicator in all other cases.
|
||||
updateFocusUI();
|
||||
// Reset the metering area in 3 seconds.
|
||||
mHandler.removeMessages(RESET_TOUCH_FOCUS);
|
||||
mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void onPreviewStarted() {
|
||||
mState = STATE_IDLE;
|
||||
}
|
||||
|
||||
public void onPreviewStopped() {
|
||||
// If auto focus was in progress, it would have been stopped.
|
||||
mState = STATE_IDLE;
|
||||
resetTouchFocus();
|
||||
updateFocusUI();
|
||||
}
|
||||
|
||||
public void onCameraReleased() {
|
||||
onPreviewStopped();
|
||||
}
|
||||
|
||||
private void autoFocus() {
|
||||
LogUtil.v(TAG, "Start autofocus.");
|
||||
mListener.autoFocus();
|
||||
mState = STATE_FOCUSING;
|
||||
updateFocusUI();
|
||||
mHandler.removeMessages(RESET_TOUCH_FOCUS);
|
||||
}
|
||||
|
||||
private void cancelAutoFocus() {
|
||||
LogUtil.v(TAG, "Cancel autofocus.");
|
||||
|
||||
// Reset the tap area before calling mListener.cancelAutofocus.
|
||||
// Otherwise, focus mode stays at auto and the tap area passed to the
|
||||
// driver is not reset.
|
||||
resetTouchFocus();
|
||||
mListener.cancelAutoFocus();
|
||||
mState = STATE_IDLE;
|
||||
updateFocusUI();
|
||||
mHandler.removeMessages(RESET_TOUCH_FOCUS);
|
||||
}
|
||||
|
||||
private void capture() {
|
||||
if (mListener.capture()) {
|
||||
mState = STATE_IDLE;
|
||||
mHandler.removeMessages(RESET_TOUCH_FOCUS);
|
||||
}
|
||||
}
|
||||
|
||||
public String getFocusMode() {
|
||||
if (mOverrideFocusMode != null) {
|
||||
return mOverrideFocusMode;
|
||||
}
|
||||
List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
|
||||
|
||||
if (mFocusAreaSupported && mFocusArea != null) {
|
||||
// Always use autofocus in tap-to-focus.
|
||||
mFocusMode = Parameters.FOCUS_MODE_AUTO;
|
||||
} else {
|
||||
mFocusMode = Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
|
||||
}
|
||||
|
||||
if (!isSupported(mFocusMode, supportedFocusModes)) {
|
||||
// For some reasons, the driver does not support the current
|
||||
// focus mode. Fall back to auto.
|
||||
if (isSupported(Parameters.FOCUS_MODE_AUTO,
|
||||
mParameters.getSupportedFocusModes())) {
|
||||
mFocusMode = Parameters.FOCUS_MODE_AUTO;
|
||||
} else {
|
||||
mFocusMode = mParameters.getFocusMode();
|
||||
}
|
||||
}
|
||||
return mFocusMode;
|
||||
}
|
||||
|
||||
public List getFocusAreas() {
|
||||
return mFocusArea;
|
||||
}
|
||||
|
||||
public List getMeteringAreas() {
|
||||
return mMeteringArea;
|
||||
}
|
||||
|
||||
public void updateFocusUI() {
|
||||
if (!mInitialized) {
|
||||
return;
|
||||
}
|
||||
FocusIndicator focusIndicator = mPieRenderer;
|
||||
|
||||
if (mState == STATE_IDLE) {
|
||||
if (mFocusArea == null) {
|
||||
focusIndicator.clear();
|
||||
} else {
|
||||
// Users touch on the preview and the indicator represents the
|
||||
// metering area. Either focus area is not supported or
|
||||
// autoFocus call is not required.
|
||||
focusIndicator.showStart();
|
||||
}
|
||||
} else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
|
||||
focusIndicator.showStart();
|
||||
} else {
|
||||
if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
|
||||
// TODO: check HAL behavior and decide if this can be removed.
|
||||
focusIndicator.showSuccess(false);
|
||||
} else if (mState == STATE_SUCCESS) {
|
||||
focusIndicator.showSuccess(false);
|
||||
} else if (mState == STATE_FAIL) {
|
||||
focusIndicator.showFail(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void resetTouchFocus() {
|
||||
if (!mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Put focus indicator to the center. clear reset position
|
||||
mPieRenderer.clear();
|
||||
|
||||
mFocusArea = null;
|
||||
mMeteringArea = null;
|
||||
}
|
||||
|
||||
private void calculateTapArea(int focusWidth, int focusHeight, float areaMultiple,
|
||||
int x, int y, int previewWidth, int previewHeight, Rect rect) {
|
||||
int areaWidth = (int) (focusWidth * areaMultiple);
|
||||
int areaHeight = (int) (focusHeight * areaMultiple);
|
||||
int left = clamp(x - areaWidth / 2, 0, previewWidth - areaWidth);
|
||||
int top = clamp(y - areaHeight / 2, 0, previewHeight - areaHeight);
|
||||
|
||||
RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight);
|
||||
mMatrix.mapRect(rectF);
|
||||
rectFToRect(rectF, rect);
|
||||
}
|
||||
|
||||
/* package */ int getFocusState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
public boolean isFocusCompleted() {
|
||||
return mState == STATE_SUCCESS || mState == STATE_FAIL;
|
||||
}
|
||||
|
||||
public boolean isFocusingSnapOnFinish() {
|
||||
return mState == STATE_FOCUSING_SNAP_ON_FINISH;
|
||||
}
|
||||
|
||||
public void removeMessages() {
|
||||
mHandler.removeMessages(RESET_TOUCH_FOCUS);
|
||||
}
|
||||
|
||||
public void overrideFocusMode(String focusMode) {
|
||||
mOverrideFocusMode = focusMode;
|
||||
}
|
||||
|
||||
public void setAeAwbLock(boolean lock) {
|
||||
mAeAwbLock = lock;
|
||||
}
|
||||
|
||||
public boolean getAeAwbLock() {
|
||||
return mAeAwbLock;
|
||||
}
|
||||
|
||||
private boolean needAutoFocusCall() {
|
||||
String focusMode = getFocusMode();
|
||||
return !(focusMode.equals(Parameters.FOCUS_MODE_INFINITY)
|
||||
|| focusMode.equals(Parameters.FOCUS_MODE_FIXED)
|
||||
|| focusMode.equals(Parameters.FOCUS_MODE_EDOF));
|
||||
}
|
||||
|
||||
public static boolean isAutoExposureLockSupported(Parameters params) {
|
||||
return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED));
|
||||
}
|
||||
|
||||
public static boolean isAutoWhiteBalanceLockSupported(Parameters params) {
|
||||
return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED));
|
||||
}
|
||||
|
||||
public static boolean isSupported(String value, List<String> supported) {
|
||||
return supported != null && supported.indexOf(value) >= 0;
|
||||
}
|
||||
|
||||
public static boolean isMeteringAreaSupported(Parameters params) {
|
||||
return params.getMaxNumMeteringAreas() > 0;
|
||||
}
|
||||
|
||||
public static boolean isFocusAreaSupported(Parameters params) {
|
||||
return (params.getMaxNumFocusAreas() > 0
|
||||
&& isSupported(Parameters.FOCUS_MODE_AUTO,
|
||||
params.getSupportedFocusModes()));
|
||||
}
|
||||
|
||||
public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation,
|
||||
int viewWidth, int viewHeight) {
|
||||
// Need mirror for front camera.
|
||||
matrix.setScale(mirror ? -1 : 1, 1);
|
||||
// This is the value for android.hardware.Camera.setDisplayOrientation.
|
||||
matrix.postRotate(displayOrientation);
|
||||
// Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
|
||||
// UI coordinates range from (0, 0) to (width, height).
|
||||
matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);
|
||||
matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
|
||||
}
|
||||
|
||||
public static int clamp(int x, int min, int max) {
|
||||
Assert.isTrue(max >= min);
|
||||
if (x > max) {
|
||||
return max;
|
||||
}
|
||||
if (x < min) {
|
||||
return min;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
public static void rectFToRect(RectF rectF, Rect rect) {
|
||||
rect.left = Math.round(rectF.left);
|
||||
rect.top = Math.round(rectF.top);
|
||||
rect.right = Math.round(rectF.right);
|
||||
rect.bottom = Math.round(rectF.bottom);
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker.camerafocus;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
public abstract class OverlayRenderer implements RenderOverlay.Renderer {
|
||||
|
||||
private static final String TAG = "CAM OverlayRenderer";
|
||||
protected RenderOverlay mOverlay;
|
||||
|
||||
protected int mLeft, mTop, mRight, mBottom;
|
||||
|
||||
protected boolean mVisible;
|
||||
|
||||
public void setVisible(boolean vis) {
|
||||
mVisible = vis;
|
||||
update();
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return mVisible;
|
||||
}
|
||||
|
||||
// default does not handle touch
|
||||
@Override
|
||||
public boolean handlesTouch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent evt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void onDraw(Canvas canvas);
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
if (mVisible) {
|
||||
onDraw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOverlay(RenderOverlay overlay) {
|
||||
mOverlay = overlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void layout(int left, int top, int right, int bottom) {
|
||||
mLeft = left;
|
||||
mRight = right;
|
||||
mTop = top;
|
||||
mBottom = bottom;
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
if (mOverlay != null) {
|
||||
return mOverlay.getContext();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return mRight - mLeft;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return mBottom - mTop;
|
||||
}
|
||||
|
||||
protected void update() {
|
||||
if (mOverlay != null) {
|
||||
mOverlay.update();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker.camerafocus;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Pie menu item
|
||||
*/
|
||||
public class PieItem {
|
||||
|
||||
public static interface OnClickListener {
|
||||
void onClick(PieItem item);
|
||||
}
|
||||
|
||||
private Drawable mDrawable;
|
||||
private int level;
|
||||
private float mCenter;
|
||||
private float start;
|
||||
private float sweep;
|
||||
private float animate;
|
||||
private int inner;
|
||||
private int outer;
|
||||
private boolean mSelected;
|
||||
private boolean mEnabled;
|
||||
private List<PieItem> mItems;
|
||||
private Path mPath;
|
||||
private OnClickListener mOnClickListener;
|
||||
private float mAlpha;
|
||||
|
||||
// Gray out the view when disabled
|
||||
private static final float ENABLED_ALPHA = 1;
|
||||
private static final float DISABLED_ALPHA = (float) 0.3;
|
||||
private boolean mChangeAlphaWhenDisabled = true;
|
||||
|
||||
public PieItem(Drawable drawable, int level) {
|
||||
mDrawable = drawable;
|
||||
this.level = level;
|
||||
setAlpha(1f);
|
||||
mEnabled = true;
|
||||
setAnimationAngle(getAnimationAngle());
|
||||
start = -1;
|
||||
mCenter = -1;
|
||||
}
|
||||
|
||||
public boolean hasItems() {
|
||||
return mItems != null;
|
||||
}
|
||||
|
||||
public List<PieItem> getItems() {
|
||||
return mItems;
|
||||
}
|
||||
|
||||
public void addItem(PieItem item) {
|
||||
if (mItems == null) {
|
||||
mItems = new ArrayList<PieItem>();
|
||||
}
|
||||
mItems.add(item);
|
||||
}
|
||||
|
||||
public void setPath(Path p) {
|
||||
mPath = p;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public void setChangeAlphaWhenDisabled (boolean enable) {
|
||||
mChangeAlphaWhenDisabled = enable;
|
||||
}
|
||||
|
||||
public void setAlpha(float alpha) {
|
||||
mAlpha = alpha;
|
||||
mDrawable.setAlpha((int) (255 * alpha));
|
||||
}
|
||||
|
||||
public void setAnimationAngle(float a) {
|
||||
animate = a;
|
||||
}
|
||||
|
||||
public float getAnimationAngle() {
|
||||
return animate;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
mEnabled = enabled;
|
||||
if (mChangeAlphaWhenDisabled) {
|
||||
if (mEnabled) {
|
||||
setAlpha(ENABLED_ALPHA);
|
||||
} else {
|
||||
setAlpha(DISABLED_ALPHA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
public void setSelected(boolean s) {
|
||||
mSelected = s;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return mSelected;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setGeometry(float st, float sw, int inside, int outside) {
|
||||
start = st;
|
||||
sweep = sw;
|
||||
inner = inside;
|
||||
outer = outside;
|
||||
}
|
||||
|
||||
public void setFixedSlice(float center, float sweep) {
|
||||
mCenter = center;
|
||||
this.sweep = sweep;
|
||||
}
|
||||
|
||||
public float getCenter() {
|
||||
return mCenter;
|
||||
}
|
||||
|
||||
public float getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public float getStartAngle() {
|
||||
return start + animate;
|
||||
}
|
||||
|
||||
public float getSweep() {
|
||||
return sweep;
|
||||
}
|
||||
|
||||
public int getInnerRadius() {
|
||||
return inner;
|
||||
}
|
||||
|
||||
public int getOuterRadius() {
|
||||
return outer;
|
||||
}
|
||||
|
||||
public void setOnClickListener(OnClickListener listener) {
|
||||
mOnClickListener = listener;
|
||||
}
|
||||
|
||||
public void performClick() {
|
||||
if (mOnClickListener != null) {
|
||||
mOnClickListener.onClick(this);
|
||||
}
|
||||
}
|
||||
|
||||
public int getIntrinsicWidth() {
|
||||
return mDrawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
public int getIntrinsicHeight() {
|
||||
return mDrawable.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
public void setBounds(int left, int top, int right, int bottom) {
|
||||
mDrawable.setBounds(left, top, right, bottom);
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
mDrawable.draw(canvas);
|
||||
}
|
||||
|
||||
public void setImageResource(Context context, int resId) {
|
||||
Drawable d = context.getResources().getDrawable(resId).mutate();
|
||||
d.setBounds(mDrawable.getBounds());
|
||||
mDrawable = d;
|
||||
setAlpha(mAlpha);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,825 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker.camerafocus;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Animation.AnimationListener;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.view.animation.Transformation;
|
||||
import com.android.messaging.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PieRenderer extends OverlayRenderer
|
||||
implements FocusIndicator {
|
||||
// Sometimes continuous autofocus starts and stops several times quickly.
|
||||
// These states are used to make sure the animation is run for at least some
|
||||
// time.
|
||||
private volatile int mState;
|
||||
private ScaleAnimation mAnimation = new ScaleAnimation();
|
||||
private static final int STATE_IDLE = 0;
|
||||
private static final int STATE_FOCUSING = 1;
|
||||
private static final int STATE_FINISHING = 2;
|
||||
private static final int STATE_PIE = 8;
|
||||
|
||||
private Runnable mDisappear = new Disappear();
|
||||
private Animation.AnimationListener mEndAction = new EndAction();
|
||||
private static final int SCALING_UP_TIME = 600;
|
||||
private static final int SCALING_DOWN_TIME = 100;
|
||||
private static final int DISAPPEAR_TIMEOUT = 200;
|
||||
private static final int DIAL_HORIZONTAL = 157;
|
||||
|
||||
private static final long PIE_FADE_IN_DURATION = 200;
|
||||
private static final long PIE_XFADE_DURATION = 200;
|
||||
private static final long PIE_SELECT_FADE_DURATION = 300;
|
||||
|
||||
private static final int MSG_OPEN = 0;
|
||||
private static final int MSG_CLOSE = 1;
|
||||
private static final float PIE_SWEEP = (float) (Math.PI * 2 / 3);
|
||||
// geometry
|
||||
private Point mCenter;
|
||||
private int mRadius;
|
||||
private int mRadiusInc;
|
||||
|
||||
// the detection if touch is inside a slice is offset
|
||||
// inbounds by this amount to allow the selection to show before the
|
||||
// finger covers it
|
||||
private int mTouchOffset;
|
||||
|
||||
private List<PieItem> mItems;
|
||||
|
||||
private PieItem mOpenItem;
|
||||
|
||||
private Paint mSelectedPaint;
|
||||
private Paint mSubPaint;
|
||||
|
||||
// touch handling
|
||||
private PieItem mCurrentItem;
|
||||
|
||||
private Paint mFocusPaint;
|
||||
private int mSuccessColor;
|
||||
private int mFailColor;
|
||||
private int mCircleSize;
|
||||
private int mFocusX;
|
||||
private int mFocusY;
|
||||
private int mCenterX;
|
||||
private int mCenterY;
|
||||
|
||||
private int mDialAngle;
|
||||
private RectF mCircle;
|
||||
private RectF mDial;
|
||||
private Point mPoint1;
|
||||
private Point mPoint2;
|
||||
private int mStartAnimationAngle;
|
||||
private boolean mFocused;
|
||||
private int mInnerOffset;
|
||||
private int mOuterStroke;
|
||||
private int mInnerStroke;
|
||||
private boolean mTapMode;
|
||||
private boolean mBlockFocus;
|
||||
private int mTouchSlopSquared;
|
||||
private Point mDown;
|
||||
private boolean mOpening;
|
||||
private LinearAnimation mXFade;
|
||||
private LinearAnimation mFadeIn;
|
||||
private volatile boolean mFocusCancelled;
|
||||
|
||||
private Handler mHandler = new Handler() {
|
||||
public void handleMessage(Message msg) {
|
||||
switch(msg.what) {
|
||||
case MSG_OPEN:
|
||||
if (mListener != null) {
|
||||
mListener.onPieOpened(mCenter.x, mCenter.y);
|
||||
}
|
||||
break;
|
||||
case MSG_CLOSE:
|
||||
if (mListener != null) {
|
||||
mListener.onPieClosed();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private PieListener mListener;
|
||||
|
||||
public static interface PieListener {
|
||||
public void onPieOpened(int centerX, int centerY);
|
||||
public void onPieClosed();
|
||||
}
|
||||
|
||||
public void setPieListener(PieListener pl) {
|
||||
mListener = pl;
|
||||
}
|
||||
|
||||
public PieRenderer(Context context) {
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context ctx) {
|
||||
setVisible(false);
|
||||
mItems = new ArrayList<PieItem>();
|
||||
Resources res = ctx.getResources();
|
||||
mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
|
||||
mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
|
||||
mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
|
||||
mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
|
||||
mCenter = new Point(0, 0);
|
||||
mSelectedPaint = new Paint();
|
||||
mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
|
||||
mSelectedPaint.setAntiAlias(true);
|
||||
mSubPaint = new Paint();
|
||||
mSubPaint.setAntiAlias(true);
|
||||
mSubPaint.setColor(Color.argb(200, 250, 230, 128));
|
||||
mFocusPaint = new Paint();
|
||||
mFocusPaint.setAntiAlias(true);
|
||||
mFocusPaint.setColor(Color.WHITE);
|
||||
mFocusPaint.setStyle(Paint.Style.STROKE);
|
||||
mSuccessColor = Color.GREEN;
|
||||
mFailColor = Color.RED;
|
||||
mCircle = new RectF();
|
||||
mDial = new RectF();
|
||||
mPoint1 = new Point();
|
||||
mPoint2 = new Point();
|
||||
mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
|
||||
mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
|
||||
mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
|
||||
mState = STATE_IDLE;
|
||||
mBlockFocus = false;
|
||||
mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
|
||||
mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
|
||||
mDown = new Point();
|
||||
}
|
||||
|
||||
public boolean showsItems() {
|
||||
return mTapMode;
|
||||
}
|
||||
|
||||
public void addItem(PieItem item) {
|
||||
// add the item to the pie itself
|
||||
mItems.add(item);
|
||||
}
|
||||
|
||||
public void removeItem(PieItem item) {
|
||||
mItems.remove(item);
|
||||
}
|
||||
|
||||
public void clearItems() {
|
||||
mItems.clear();
|
||||
}
|
||||
|
||||
public void showInCenter() {
|
||||
if ((mState == STATE_PIE) && isVisible()) {
|
||||
mTapMode = false;
|
||||
show(false);
|
||||
} else {
|
||||
if (mState != STATE_IDLE) {
|
||||
cancelFocus();
|
||||
}
|
||||
mState = STATE_PIE;
|
||||
setCenter(mCenterX, mCenterY);
|
||||
mTapMode = true;
|
||||
show(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
show(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* guaranteed has center set
|
||||
* @param show
|
||||
*/
|
||||
private void show(boolean show) {
|
||||
if (show) {
|
||||
mState = STATE_PIE;
|
||||
// ensure clean state
|
||||
mCurrentItem = null;
|
||||
mOpenItem = null;
|
||||
for (PieItem item : mItems) {
|
||||
item.setSelected(false);
|
||||
}
|
||||
layoutPie();
|
||||
fadeIn();
|
||||
} else {
|
||||
mState = STATE_IDLE;
|
||||
mTapMode = false;
|
||||
if (mXFade != null) {
|
||||
mXFade.cancel();
|
||||
}
|
||||
}
|
||||
setVisible(show);
|
||||
mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
|
||||
}
|
||||
|
||||
private void fadeIn() {
|
||||
mFadeIn = new LinearAnimation(0, 1);
|
||||
mFadeIn.setDuration(PIE_FADE_IN_DURATION);
|
||||
mFadeIn.setAnimationListener(new AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
mFadeIn = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
});
|
||||
mFadeIn.startNow();
|
||||
mOverlay.startAnimation(mFadeIn);
|
||||
}
|
||||
|
||||
public void setCenter(int x, int y) {
|
||||
mCenter.x = x;
|
||||
mCenter.y = y;
|
||||
// when using the pie menu, align the focus ring
|
||||
alignFocus(x, y);
|
||||
}
|
||||
|
||||
private void layoutPie() {
|
||||
int rgap = 2;
|
||||
int inner = mRadius + rgap;
|
||||
int outer = mRadius + mRadiusInc - rgap;
|
||||
int gap = 1;
|
||||
layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
|
||||
}
|
||||
|
||||
private void layoutItems(List<PieItem> items, float centerAngle, int inner,
|
||||
int outer, int gap) {
|
||||
float emptyangle = PIE_SWEEP / 16;
|
||||
float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size();
|
||||
float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
|
||||
// check if we have custom geometry
|
||||
// first item we find triggers custom sweep for all
|
||||
// this allows us to re-use the path
|
||||
for (PieItem item : items) {
|
||||
if (item.getCenter() >= 0) {
|
||||
sweep = item.getSweep();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap,
|
||||
outer, inner, mCenter);
|
||||
for (PieItem item : items) {
|
||||
// shared between items
|
||||
item.setPath(path);
|
||||
if (item.getCenter() >= 0) {
|
||||
angle = item.getCenter();
|
||||
}
|
||||
int w = item.getIntrinsicWidth();
|
||||
int h = item.getIntrinsicHeight();
|
||||
// move views to outer border
|
||||
int r = inner + (outer - inner) * 2 / 3;
|
||||
int x = (int) (r * Math.cos(angle));
|
||||
int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
|
||||
x = mCenter.x + x - w / 2;
|
||||
item.setBounds(x, y, x + w, y + h);
|
||||
float itemstart = angle - sweep / 2;
|
||||
item.setGeometry(itemstart, sweep, inner, outer);
|
||||
if (item.hasItems()) {
|
||||
layoutItems(item.getItems(), angle, inner,
|
||||
outer + mRadiusInc / 2, gap);
|
||||
}
|
||||
angle += sweep;
|
||||
}
|
||||
}
|
||||
|
||||
private Path makeSlice(float start, float end, int outer, int inner, Point center) {
|
||||
RectF bb =
|
||||
new RectF(center.x - outer, center.y - outer, center.x + outer,
|
||||
center.y + outer);
|
||||
RectF bbi =
|
||||
new RectF(center.x - inner, center.y - inner, center.x + inner,
|
||||
center.y + inner);
|
||||
Path path = new Path();
|
||||
path.arcTo(bb, start, end - start, true);
|
||||
path.arcTo(bbi, end, start - end);
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* converts a
|
||||
* @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
|
||||
* @return skia angle
|
||||
*/
|
||||
private float getDegrees(double angle) {
|
||||
return (float) (360 - 180 * angle / Math.PI);
|
||||
}
|
||||
|
||||
private void startFadeOut() {
|
||||
mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
deselect();
|
||||
show(false);
|
||||
mOverlay.setAlpha(1);
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).setDuration(PIE_SELECT_FADE_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
float alpha = 1;
|
||||
if (mXFade != null) {
|
||||
alpha = mXFade.getValue();
|
||||
} else if (mFadeIn != null) {
|
||||
alpha = mFadeIn.getValue();
|
||||
}
|
||||
int state = canvas.save();
|
||||
if (mFadeIn != null) {
|
||||
float sf = 0.9f + alpha * 0.1f;
|
||||
canvas.scale(sf, sf, mCenter.x, mCenter.y);
|
||||
}
|
||||
drawFocus(canvas);
|
||||
if (mState == STATE_FINISHING) {
|
||||
canvas.restoreToCount(state);
|
||||
return;
|
||||
}
|
||||
if ((mOpenItem == null) || (mXFade != null)) {
|
||||
// draw base menu
|
||||
for (PieItem item : mItems) {
|
||||
drawItem(canvas, item, alpha);
|
||||
}
|
||||
}
|
||||
if (mOpenItem != null) {
|
||||
for (PieItem inner : mOpenItem.getItems()) {
|
||||
drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
|
||||
}
|
||||
}
|
||||
canvas.restoreToCount(state);
|
||||
}
|
||||
|
||||
private void drawItem(Canvas canvas, PieItem item, float alpha) {
|
||||
if (mState == STATE_PIE) {
|
||||
if (item.getPath() != null) {
|
||||
if (item.isSelected()) {
|
||||
Paint p = mSelectedPaint;
|
||||
int state = canvas.save();
|
||||
float r = getDegrees(item.getStartAngle());
|
||||
canvas.rotate(r, mCenter.x, mCenter.y);
|
||||
canvas.drawPath(item.getPath(), p);
|
||||
canvas.restoreToCount(state);
|
||||
}
|
||||
alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
|
||||
// draw the item view
|
||||
item.setAlpha(alpha);
|
||||
item.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent evt) {
|
||||
float x = evt.getX();
|
||||
float y = evt.getY();
|
||||
int action = evt.getActionMasked();
|
||||
PointF polar = getPolar(x, y, !(mTapMode));
|
||||
if (MotionEvent.ACTION_DOWN == action) {
|
||||
mDown.x = (int) evt.getX();
|
||||
mDown.y = (int) evt.getY();
|
||||
mOpening = false;
|
||||
if (mTapMode) {
|
||||
PieItem item = findItem(polar);
|
||||
if ((item != null) && (mCurrentItem != item)) {
|
||||
mState = STATE_PIE;
|
||||
onEnter(item);
|
||||
}
|
||||
} else {
|
||||
setCenter((int) x, (int) y);
|
||||
show(true);
|
||||
}
|
||||
return true;
|
||||
} else if (MotionEvent.ACTION_UP == action) {
|
||||
if (isVisible()) {
|
||||
PieItem item = mCurrentItem;
|
||||
if (mTapMode) {
|
||||
item = findItem(polar);
|
||||
if (item != null && mOpening) {
|
||||
mOpening = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (item == null) {
|
||||
mTapMode = false;
|
||||
show(false);
|
||||
} else if (!mOpening
|
||||
&& !item.hasItems()) {
|
||||
item.performClick();
|
||||
startFadeOut();
|
||||
mTapMode = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else if (MotionEvent.ACTION_CANCEL == action) {
|
||||
if (isVisible() || mTapMode) {
|
||||
show(false);
|
||||
}
|
||||
deselect();
|
||||
return false;
|
||||
} else if (MotionEvent.ACTION_MOVE == action) {
|
||||
if (polar.y < mRadius) {
|
||||
if (mOpenItem != null) {
|
||||
mOpenItem = null;
|
||||
} else {
|
||||
deselect();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
PieItem item = findItem(polar);
|
||||
boolean moved = hasMoved(evt);
|
||||
if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
|
||||
// only select if we didn't just open or have moved past slop
|
||||
mOpening = false;
|
||||
if (moved) {
|
||||
// switch back to swipe mode
|
||||
mTapMode = false;
|
||||
}
|
||||
onEnter(item);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasMoved(MotionEvent e) {
|
||||
return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
|
||||
+ (e.getY() - mDown.y) * (e.getY() - mDown.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* enter a slice for a view
|
||||
* updates model only
|
||||
* @param item
|
||||
*/
|
||||
private void onEnter(PieItem item) {
|
||||
if (mCurrentItem != null) {
|
||||
mCurrentItem.setSelected(false);
|
||||
}
|
||||
if (item != null && item.isEnabled()) {
|
||||
item.setSelected(true);
|
||||
mCurrentItem = item;
|
||||
if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
|
||||
openCurrentItem();
|
||||
}
|
||||
} else {
|
||||
mCurrentItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
if (mCurrentItem != null) {
|
||||
mCurrentItem.setSelected(false);
|
||||
}
|
||||
if (mOpenItem != null) {
|
||||
mOpenItem = null;
|
||||
}
|
||||
mCurrentItem = null;
|
||||
}
|
||||
|
||||
private void openCurrentItem() {
|
||||
if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
|
||||
mCurrentItem.setSelected(false);
|
||||
mOpenItem = mCurrentItem;
|
||||
mOpening = true;
|
||||
mXFade = new LinearAnimation(1, 0);
|
||||
mXFade.setDuration(PIE_XFADE_DURATION);
|
||||
mXFade.setAnimationListener(new AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
mXFade = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
});
|
||||
mXFade.startNow();
|
||||
mOverlay.startAnimation(mXFade);
|
||||
}
|
||||
}
|
||||
|
||||
private PointF getPolar(float x, float y, boolean useOffset) {
|
||||
PointF res = new PointF();
|
||||
// get angle and radius from x/y
|
||||
res.x = (float) Math.PI / 2;
|
||||
x = x - mCenter.x;
|
||||
y = mCenter.y - y;
|
||||
res.y = (float) Math.sqrt(x * x + y * y);
|
||||
if (x != 0) {
|
||||
res.x = (float) Math.atan2(y, x);
|
||||
if (res.x < 0) {
|
||||
res.x = (float) (2 * Math.PI + res.x);
|
||||
}
|
||||
}
|
||||
res.y = res.y + (useOffset ? mTouchOffset : 0);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param polar x: angle, y: dist
|
||||
* @return the item at angle/dist or null
|
||||
*/
|
||||
private PieItem findItem(PointF polar) {
|
||||
// find the matching item:
|
||||
List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
|
||||
for (PieItem item : items) {
|
||||
if (inside(polar, item)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean inside(PointF polar, PieItem item) {
|
||||
return (item.getInnerRadius() < polar.y)
|
||||
&& (item.getStartAngle() < polar.x)
|
||||
&& (item.getStartAngle() + item.getSweep() > polar.x)
|
||||
&& (!mTapMode || (item.getOuterRadius() > polar.y));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handlesTouch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// focus specific code
|
||||
|
||||
public void setBlockFocus(boolean blocked) {
|
||||
mBlockFocus = blocked;
|
||||
if (blocked) {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFocus(int x, int y) {
|
||||
mFocusX = x;
|
||||
mFocusY = y;
|
||||
setCircle(mFocusX, mFocusY);
|
||||
}
|
||||
|
||||
public void alignFocus(int x, int y) {
|
||||
mOverlay.removeCallbacks(mDisappear);
|
||||
mAnimation.cancel();
|
||||
mAnimation.reset();
|
||||
mFocusX = x;
|
||||
mFocusY = y;
|
||||
mDialAngle = DIAL_HORIZONTAL;
|
||||
setCircle(x, y);
|
||||
mFocused = false;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return 2 * mCircleSize;
|
||||
}
|
||||
|
||||
private int getRandomRange() {
|
||||
return (int) (-60 + 120 * Math.random());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void layout(int l, int t, int r, int b) {
|
||||
super.layout(l, t, r, b);
|
||||
mCenterX = (r - l) / 2;
|
||||
mCenterY = (b - t) / 2;
|
||||
mFocusX = mCenterX;
|
||||
mFocusY = mCenterY;
|
||||
setCircle(mFocusX, mFocusY);
|
||||
if (isVisible() && mState == STATE_PIE) {
|
||||
setCenter(mCenterX, mCenterY);
|
||||
layoutPie();
|
||||
}
|
||||
}
|
||||
|
||||
private void setCircle(int cx, int cy) {
|
||||
mCircle.set(cx - mCircleSize, cy - mCircleSize,
|
||||
cx + mCircleSize, cy + mCircleSize);
|
||||
mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
|
||||
cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
|
||||
}
|
||||
|
||||
public void drawFocus(Canvas canvas) {
|
||||
if (mBlockFocus) {
|
||||
return;
|
||||
}
|
||||
mFocusPaint.setStrokeWidth(mOuterStroke);
|
||||
canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
|
||||
if (mState == STATE_PIE) {
|
||||
return;
|
||||
}
|
||||
int color = mFocusPaint.getColor();
|
||||
if (mState == STATE_FINISHING) {
|
||||
mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
|
||||
}
|
||||
mFocusPaint.setStrokeWidth(mInnerStroke);
|
||||
drawLine(canvas, mDialAngle, mFocusPaint);
|
||||
drawLine(canvas, mDialAngle + 45, mFocusPaint);
|
||||
drawLine(canvas, mDialAngle + 180, mFocusPaint);
|
||||
drawLine(canvas, mDialAngle + 225, mFocusPaint);
|
||||
canvas.save();
|
||||
// rotate the arc instead of its offset to better use framework's shape caching
|
||||
canvas.rotate(mDialAngle, mFocusX, mFocusY);
|
||||
canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
|
||||
canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
|
||||
canvas.restore();
|
||||
mFocusPaint.setColor(color);
|
||||
}
|
||||
|
||||
private void drawLine(Canvas canvas, int angle, Paint p) {
|
||||
convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
|
||||
convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
|
||||
canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
|
||||
mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
|
||||
}
|
||||
|
||||
private static void convertCart(int angle, int radius, Point out) {
|
||||
double a = 2 * Math.PI * (angle % 360) / 360;
|
||||
out.x = (int) (radius * Math.cos(a) + 0.5);
|
||||
out.y = (int) (radius * Math.sin(a) + 0.5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showStart() {
|
||||
if (mState == STATE_PIE) {
|
||||
return;
|
||||
}
|
||||
cancelFocus();
|
||||
mStartAnimationAngle = 67;
|
||||
int range = getRandomRange();
|
||||
startAnimation(SCALING_UP_TIME,
|
||||
false, mStartAnimationAngle, mStartAnimationAngle + range);
|
||||
mState = STATE_FOCUSING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSuccess(boolean timeout) {
|
||||
if (mState == STATE_FOCUSING) {
|
||||
startAnimation(SCALING_DOWN_TIME,
|
||||
timeout, mStartAnimationAngle);
|
||||
mState = STATE_FINISHING;
|
||||
mFocused = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFail(boolean timeout) {
|
||||
if (mState == STATE_FOCUSING) {
|
||||
startAnimation(SCALING_DOWN_TIME,
|
||||
timeout, mStartAnimationAngle);
|
||||
mState = STATE_FINISHING;
|
||||
mFocused = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelFocus() {
|
||||
mFocusCancelled = true;
|
||||
mOverlay.removeCallbacks(mDisappear);
|
||||
if (mAnimation != null) {
|
||||
mAnimation.cancel();
|
||||
}
|
||||
mFocusCancelled = false;
|
||||
mFocused = false;
|
||||
mState = STATE_IDLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
if (mState == STATE_PIE) {
|
||||
return;
|
||||
}
|
||||
cancelFocus();
|
||||
mOverlay.post(mDisappear);
|
||||
}
|
||||
|
||||
private void startAnimation(long duration, boolean timeout,
|
||||
float toScale) {
|
||||
startAnimation(duration, timeout, mDialAngle,
|
||||
toScale);
|
||||
}
|
||||
|
||||
private void startAnimation(long duration, boolean timeout,
|
||||
float fromScale, float toScale) {
|
||||
setVisible(true);
|
||||
mAnimation.reset();
|
||||
mAnimation.setDuration(duration);
|
||||
mAnimation.setScale(fromScale, toScale);
|
||||
mAnimation.setAnimationListener(timeout ? mEndAction : null);
|
||||
mOverlay.startAnimation(mAnimation);
|
||||
update();
|
||||
}
|
||||
|
||||
private class EndAction implements Animation.AnimationListener {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
// Keep the focus indicator for some time.
|
||||
if (!mFocusCancelled) {
|
||||
mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
}
|
||||
|
||||
private class Disappear implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mState == STATE_PIE) {
|
||||
return;
|
||||
}
|
||||
setVisible(false);
|
||||
mFocusX = mCenterX;
|
||||
mFocusY = mCenterY;
|
||||
mState = STATE_IDLE;
|
||||
setCircle(mFocusX, mFocusY);
|
||||
mFocused = false;
|
||||
}
|
||||
}
|
||||
|
||||
private class ScaleAnimation extends Animation {
|
||||
private float mFrom = 1f;
|
||||
private float mTo = 1f;
|
||||
|
||||
public ScaleAnimation() {
|
||||
setFillAfter(true);
|
||||
}
|
||||
|
||||
public void setScale(float from, float to) {
|
||||
mFrom = from;
|
||||
mTo = to;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
mDialAngle = (int) (mFrom + (mTo - mFrom) * interpolatedTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class LinearAnimation extends Animation {
|
||||
private float mFrom;
|
||||
private float mTo;
|
||||
private float mValue;
|
||||
|
||||
public LinearAnimation(float from, float to) {
|
||||
setFillAfter(true);
|
||||
setInterpolator(new LinearInterpolator());
|
||||
mFrom = from;
|
||||
mTo = to;
|
||||
}
|
||||
|
||||
public float getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
The files in this package were copied from the android-4.4.4_r1 branch of ASOP from the folders
|
||||
com/android/camera/ and com/android/camera/ui from files with the same name. Some modifications
|
||||
have been made to remove unneeded features and adjust to our needs.
|
||||
@@ -1,178 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.mediapicker.camerafocus;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class RenderOverlay extends FrameLayout {
|
||||
|
||||
interface Renderer {
|
||||
|
||||
public boolean handlesTouch();
|
||||
public boolean onTouchEvent(MotionEvent evt);
|
||||
public void setOverlay(RenderOverlay overlay);
|
||||
public void layout(int left, int top, int right, int bottom);
|
||||
public void draw(Canvas canvas);
|
||||
|
||||
}
|
||||
|
||||
private RenderView mRenderView;
|
||||
private List<Renderer> mClients;
|
||||
|
||||
// reverse list of touch clients
|
||||
private List<Renderer> mTouchClients;
|
||||
private int[] mPosition = new int[2];
|
||||
|
||||
public RenderOverlay(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mRenderView = new RenderView(context);
|
||||
addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT));
|
||||
mClients = new ArrayList<Renderer>(10);
|
||||
mTouchClients = new ArrayList<Renderer>(10);
|
||||
setWillNotDraw(false);
|
||||
|
||||
addRenderer(new PieRenderer(context));
|
||||
}
|
||||
|
||||
public PieRenderer getPieRenderer() {
|
||||
for (Renderer renderer : mClients) {
|
||||
if (renderer instanceof PieRenderer) {
|
||||
return (PieRenderer) renderer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addRenderer(Renderer renderer) {
|
||||
mClients.add(renderer);
|
||||
renderer.setOverlay(this);
|
||||
if (renderer.handlesTouch()) {
|
||||
mTouchClients.add(0, renderer);
|
||||
}
|
||||
renderer.layout(getLeft(), getTop(), getRight(), getBottom());
|
||||
}
|
||||
|
||||
public void addRenderer(int pos, Renderer renderer) {
|
||||
mClients.add(pos, renderer);
|
||||
renderer.setOverlay(this);
|
||||
renderer.layout(getLeft(), getTop(), getRight(), getBottom());
|
||||
}
|
||||
|
||||
public void remove(Renderer renderer) {
|
||||
mClients.remove(renderer);
|
||||
renderer.setOverlay(null);
|
||||
}
|
||||
|
||||
public int getClientSize() {
|
||||
return mClients.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent m) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean directDispatchTouch(MotionEvent m, Renderer target) {
|
||||
mRenderView.setTouchTarget(target);
|
||||
boolean res = super.dispatchTouchEvent(m);
|
||||
mRenderView.setTouchTarget(null);
|
||||
return res;
|
||||
}
|
||||
|
||||
private void adjustPosition() {
|
||||
getLocationInWindow(mPosition);
|
||||
}
|
||||
|
||||
public int getWindowPositionX() {
|
||||
return mPosition[0];
|
||||
}
|
||||
|
||||
public int getWindowPositionY() {
|
||||
return mPosition[1];
|
||||
}
|
||||
|
||||
public void update() {
|
||||
mRenderView.invalidate();
|
||||
}
|
||||
|
||||
private class RenderView extends View {
|
||||
|
||||
private Renderer mTouchTarget;
|
||||
|
||||
public RenderView(Context context) {
|
||||
super(context);
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
public void setTouchTarget(Renderer target) {
|
||||
mTouchTarget = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent evt) {
|
||||
if (mTouchTarget != null) {
|
||||
return mTouchTarget.onTouchEvent(evt);
|
||||
}
|
||||
if (mTouchClients != null) {
|
||||
boolean res = false;
|
||||
for (Renderer client : mTouchClients) {
|
||||
res |= client.onTouchEvent(evt);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
adjustPosition();
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
if (mClients == null) {
|
||||
return;
|
||||
}
|
||||
for (Renderer renderer : mClients) {
|
||||
renderer.layout(left, top, right, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
if (mClients == null) {
|
||||
return;
|
||||
}
|
||||
boolean redraw = false;
|
||||
for (Renderer renderer : mClients) {
|
||||
renderer.draw(canvas);
|
||||
redraw = redraw || ((OverlayRenderer) renderer).isVisible();
|
||||
}
|
||||
if (redraw) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user