Messaging: Replace own camera handling with pickers
We do not need to handle camera on our own - there are apps out there that are made for that! Change-Id: Ifeb70e46b17bff5eb8d85c8775589efac4f2c284
This commit is contained in:
@@ -15,129 +15,80 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
<FrameLayout
|
||||||
<com.android.messaging.ui.mediapicker.CameraMediaChooserView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@android:color/black">
|
android:background="@android:color/white">
|
||||||
|
|
||||||
<FrameLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/mediapicker_enabled"
|
android:id="@+id/mediapicker_enabled"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" >
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<!-- Default to using the hardware rendered camera preview, we will fall back to
|
<TextView
|
||||||
SoftwareCameraPreview in CameraMediaChooserView if needed -->
|
android:id="@+id/textView"
|
||||||
<com.android.messaging.ui.mediapicker.HardwareCameraPreview
|
style="@style/ContactPickerHintText"
|
||||||
android:id="@+id/camera_preview"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center" />
|
|
||||||
|
|
||||||
<com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay
|
|
||||||
android:id="@+id/focus_visual"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/camera_shutter_visual"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@android:color/white"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<!-- Need a background on this view in order for the ripple effect to have a place to draw -->
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/camera_button_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@android:color/transparent"
|
android:layout_gravity="top|center"
|
||||||
android:paddingTop="20dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_gravity="bottom">
|
android:importantForAccessibility="no"
|
||||||
|
android:text="@string/mediapicker_cameraChooserDescription"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/mediapicker_enabled"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/mediapicker_enabled" />
|
||||||
|
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/take_picture"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_gravity="start|center_vertical"
|
android:layout_height="0dp"
|
||||||
style="@style/CameraChooserFrameStyle">
|
android:layout_weight="0.5"
|
||||||
|
android:gravity="end"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/take_video"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<ImageButton
|
<ImageView
|
||||||
android:id="@+id/camera_swap_mode_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minWidth="48dp"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:background="@drawable/transparent_button_background"
|
|
||||||
android:src="@drawable/ic_mp_video_small_light"
|
|
||||||
android:contentDescription="@string/camera_switch_to_video_mode"/>
|
|
||||||
|
|
||||||
<Chronometer
|
|
||||||
android:id="@+id/camera_video_counter"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/camera_capture_button"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:minWidth="96dp"
|
android:layout_marginEnd="32dp"
|
||||||
android:minHeight="96dp"
|
android:contentDescription="@null"
|
||||||
android:background="@drawable/transparent_button_background"
|
android:importantForAccessibility="no"
|
||||||
android:src="@drawable/ic_mp_video_small_light"
|
android:scaleX="1.33"
|
||||||
android:scaleX="2.5"
|
android:scaleY="1.33"
|
||||||
android:scaleY="2.5"
|
android:src="@drawable/ic_camera_light"
|
||||||
android:contentDescription="@string/camera_take_picture"/>
|
app:tint="@color/primary_color" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/take_video"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="0.5"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/take_picture"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="center"
|
||||||
android:paddingEnd="16dp">
|
android:layout_marginStart="32dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:scaleX="1.33"
|
||||||
|
android:scaleY="1.33"
|
||||||
|
android:src="@drawable/ic_mp_video_small_light"
|
||||||
|
app:tint="@color/primary_color" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageButton
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/camera_fullScreen_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minWidth="48dp"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:background="@drawable/transparent_button_background"
|
|
||||||
android:src="@drawable/ic_mp_full_screen_light"
|
|
||||||
android:contentDescription="@string/camera_switch_full_screen" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/camera_swapCamera_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minWidth="48dp"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:background="@drawable/transparent_button_background"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:contentDescription="@string/camera_switch_camera_facing"/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/camera_cancel_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minWidth="48dp"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:background="@drawable/transparent_button_background"
|
|
||||||
android:src="@drawable/ic_cancel_small_light"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:contentDescription="@string/camera_cancel_recording" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- This view will hide all other views if the required permission is not granted -->
|
<!-- This view will hide all other views if the required permission is not granted -->
|
||||||
<TextView
|
<TextView
|
||||||
@@ -149,5 +100,4 @@
|
|||||||
android:background="@android:color/white"
|
android:background="@android:color/white"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
</FrameLayout>
|
||||||
</com.android.messaging.ui.mediapicker.CameraMediaChooserView>
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,55 +17,82 @@
|
|||||||
|
|
||||||
package com.android.messaging.ui.mediapicker;
|
package com.android.messaging.ui.mediapicker;
|
||||||
|
|
||||||
|
import static com.android.messaging.util.ContentType.VIDEO_UNSPECIFIED;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.content.Context;
|
import android.app.Activity;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.hardware.Camera;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.animation.AlphaAnimation;
|
|
||||||
import android.view.animation.Animation;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import android.view.animation.AnimationSet;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import android.widget.Chronometer;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
|
|
||||||
import com.android.messaging.R;
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||||
|
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||||
import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
|
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.OsUtil;
|
||||||
import com.android.messaging.util.UiUtils;
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chooser which allows the user to take pictures or video without leaving the current app/activity
|
* Chooser which allows the user to take pictures or video without leaving the current app/activity
|
||||||
*/
|
*/
|
||||||
class CameraMediaChooser extends MediaChooser implements
|
class CameraMediaChooser extends MediaChooser {
|
||||||
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 mEnabledView;
|
||||||
private View mMissingPermissionView;
|
private View mMissingPermissionView;
|
||||||
|
private final ActivityResultLauncher<Uri> mPictureLauncher;
|
||||||
|
private final ActivityResultLauncher<Uri> mVideoLauncher;
|
||||||
|
private final DocumentImagePicker mDocumentImagePicker;
|
||||||
|
private Uri mOutputUri;
|
||||||
|
|
||||||
CameraMediaChooser(final MediaPicker mediaPicker) {
|
CameraMediaChooser(final MediaPicker mediaPicker) {
|
||||||
super(mediaPicker);
|
super(mediaPicker);
|
||||||
|
mDocumentImagePicker = new DocumentImagePicker(mMediaPicker, data -> {
|
||||||
|
if (mBindingRef.isBound()) {
|
||||||
|
mMediaPicker.dispatchPendingItemAdded(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mPictureLauncher = mediaPicker.registerForActivityResult(
|
||||||
|
new ActivityResultContracts.TakePicture(), result -> {
|
||||||
|
if (result) {
|
||||||
|
List<Uri> list = new ArrayList<>();
|
||||||
|
list.add(mOutputUri);
|
||||||
|
mDocumentImagePicker.onDocumentsPicked(list);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mVideoLauncher = mediaPicker.registerForActivityResult(
|
||||||
|
new ActivityResultContracts.CaptureVideo(), result -> {
|
||||||
|
if (result) {
|
||||||
|
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, VIDEO_UNSPECIFIED,
|
||||||
|
mOutputUri, MessagingContentProvider.UNSPECIFIED_SIZE,
|
||||||
|
MessagingContentProvider.UNSPECIFIED_SIZE),
|
||||||
|
true /* dismissMediaPicker */);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSupportedMediaTypes() {
|
public int getSupportedMediaTypes() {
|
||||||
if (CameraManager.get().hasAnyCamera()) {
|
Activity activity = mMediaPicker.getActivity();
|
||||||
|
if (activity == null) {
|
||||||
|
return MediaPicker.MEDIA_TYPE_NONE;
|
||||||
|
}
|
||||||
|
PackageManager pm = activity.getPackageManager();
|
||||||
|
if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
|
||||||
return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO;
|
return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO;
|
||||||
} else {
|
} else {
|
||||||
return MediaPicker.MEDIA_TYPE_NONE;
|
return MediaPicker.MEDIA_TYPE_NONE;
|
||||||
@@ -74,129 +101,33 @@ class CameraMediaChooser extends MediaChooser implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View destroyView() {
|
public View destroyView() {
|
||||||
CameraManager.get().closeCamera();
|
|
||||||
CameraManager.get().setListener(null);
|
|
||||||
CameraManager.get().setSubscriptionDataProvider(null);
|
|
||||||
return super.destroyView();
|
return super.destroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected View createView(final ViewGroup container) {
|
protected View createView(final ViewGroup container) {
|
||||||
CameraManager.get().setListener(this);
|
|
||||||
CameraManager.get().setSubscriptionDataProvider(this);
|
|
||||||
CameraManager.get().setVideoMode(false);
|
|
||||||
final LayoutInflater inflater = getLayoutInflater();
|
final LayoutInflater inflater = getLayoutInflater();
|
||||||
final CameraMediaChooserView view = (CameraMediaChooserView) inflater.inflate(
|
final View view = inflater.inflate(
|
||||||
R.layout.mediapicker_camera_chooser,
|
R.layout.mediapicker_camera_chooser,
|
||||||
container /* root */,
|
container /* root */,
|
||||||
false /* attachToRoot */);
|
false /* attachToRoot */);
|
||||||
mCameraPreviewHost = (CameraPreview.CameraPreviewHost) view.findViewById(
|
|
||||||
R.id.camera_preview);
|
|
||||||
mCameraPreviewHost.getView().setOnTouchListener((view1, 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(view12 -> mMediaPicker.setFullScreen(true));
|
|
||||||
|
|
||||||
mSwapCameraButton = (ImageButton) view.findViewById(R.id.camera_swapCamera_button);
|
|
||||||
mSwapCameraButton.setOnClickListener(view13 -> CameraManager.get().swapCamera());
|
|
||||||
|
|
||||||
mCaptureButton = (ImageButton) view.findViewById(R.id.camera_capture_button);
|
|
||||||
mCaptureButton.setOnClickListener(v -> {
|
|
||||||
final float heightPercent = Math.min(mMediaPicker.getViewPager().getHeight() /
|
|
||||||
(float) mCameraPreviewHost.getView().getHeight(), 1);
|
|
||||||
|
|
||||||
if (CameraManager.get().isRecording()) {
|
|
||||||
CameraManager.get().stopVideo();
|
|
||||||
} else {
|
|
||||||
final MediaCallback callback = new 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(view14 -> {
|
|
||||||
final boolean isSwitchingToVideo = !CameraManager.get().isVideoMode();
|
|
||||||
if (isSwitchingToVideo && !OsUtil.hasRecordAudioPermission()) {
|
|
||||||
requestRecordAudioPermission();
|
|
||||||
} else {
|
|
||||||
onSwapMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mCancelVideoButton = (ImageButton) view.findViewById(R.id.camera_cancel_button);
|
|
||||||
mCancelVideoButton.setOnClickListener(view15 -> {
|
|
||||||
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);
|
mEnabledView = view.findViewById(R.id.mediapicker_enabled);
|
||||||
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
|
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
|
||||||
|
View takePictureView = view.findViewById(R.id.take_picture);
|
||||||
// Must set mView before calling updateViewState because it operates on mView
|
takePictureView.setOnClickListener(v -> {
|
||||||
mView = view;
|
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
|
||||||
updateViewState();
|
mPictureLauncher.launch(mOutputUri);
|
||||||
updateForPermissionState(CameraManager.hasCameraPermission());
|
});
|
||||||
|
View takeVideoView = view.findViewById(R.id.take_video);
|
||||||
|
takeVideoView.setOnClickListener(v -> {
|
||||||
|
if (!OsUtil.hasRecordAudioPermission()) {
|
||||||
|
requestRecordAudioPermission();
|
||||||
|
} else {
|
||||||
|
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
|
||||||
|
mVideoLauncher.launch(mOutputUri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateForPermissionState(hasCameraPermission());
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,36 +141,11 @@ class CameraMediaChooser extends MediaChooser implements
|
|||||||
return R.string.mediapicker_cameraChooserDescription;
|
return R.string.mediapicker_cameraChooserDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the view when entering or leaving full-screen camera mode
|
|
||||||
*/
|
|
||||||
@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
|
@Override
|
||||||
protected void setSelected(final boolean selected) {
|
protected void setSelected(final boolean selected) {
|
||||||
super.setSelected(selected);
|
super.setSelected(selected);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
if (CameraManager.hasCameraPermission()) {
|
if (!hasCameraPermission()) {
|
||||||
// If an error occurred before the chooser was selected, show it now
|
|
||||||
showErrorToastIfNeeded();
|
|
||||||
} else {
|
|
||||||
requestCameraPermission();
|
requestCameraPermission();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,17 +167,6 @@ class CameraMediaChooser extends MediaChooser implements
|
|||||||
if (requestCode == MediaPicker.CAMERA_PERMISSION_REQUEST_CODE) {
|
if (requestCode == MediaPicker.CAMERA_PERMISSION_REQUEST_CODE) {
|
||||||
final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||||
updateForPermissionState(permissionGranted);
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,174 +180,12 @@ class CameraMediaChooser extends MediaChooser implements
|
|||||||
mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE);
|
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_camera_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_light);
|
|
||||||
mCaptureButton.setContentDescription(context.getString(
|
|
||||||
R.string.camera_stop_recording));
|
|
||||||
} else if (videoMode) {
|
|
||||||
mCaptureButton.setImageResource(R.drawable.ic_mp_video_small_light);
|
|
||||||
mCaptureButton.setContentDescription(context.getString(
|
|
||||||
R.string.camera_start_recording));
|
|
||||||
} else {
|
|
||||||
mCaptureButton.setImageResource(R.drawable.ic_checkmark_light);
|
|
||||||
mCaptureButton.setContentDescription(context.getString(
|
|
||||||
R.string.camera_take_picture));
|
|
||||||
}
|
|
||||||
mCaptureButton.setEnabled(isCameraAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
int getActionBarTitleResId() {
|
int getActionBarTitleResId() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static boolean hasCameraPermission() {
|
||||||
* Returns if the camera is currently ready camera is loaded and not taking a picture.
|
return OsUtil.hasPermission(Manifest.permission.CAMERA);
|
||||||
* otherwise we should avoid taking another picture, swapping camera or recording video.
|
|
||||||
*/
|
|
||||||
private boolean isCameraAvailable() {
|
|
||||||
return CameraManager.get().isCameraAvailable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
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(@NonNull 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(() -> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,7 @@ import androidx.activity.result.contract.ActivityResultContracts;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.android.messaging.Factory;
|
import com.android.messaging.Factory;
|
||||||
|
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||||
import com.android.messaging.datamodel.data.PendingAttachmentData;
|
import com.android.messaging.datamodel.data.PendingAttachmentData;
|
||||||
import com.android.messaging.ui.UIIntents;
|
import com.android.messaging.ui.UIIntents;
|
||||||
import com.android.messaging.util.BugleGservicesKeys;
|
import com.android.messaging.util.BugleGservicesKeys;
|
||||||
@@ -110,7 +111,8 @@ public class DocumentImagePicker {
|
|||||||
new SafeAsyncTask<Void, Void, String>() {
|
new SafeAsyncTask<Void, Void, String>() {
|
||||||
@Override
|
@Override
|
||||||
protected String doInBackgroundTimed(final Void... params) {
|
protected String doInBackgroundTimed(final Void... params) {
|
||||||
if (FileUtil.isInPrivateDir(documentUri)) {
|
if (FileUtil.isInPrivateDir(documentUri) &&
|
||||||
|
!MediaScratchFileProvider.isMediaScratchSpaceUri(documentUri)) {
|
||||||
// hacker sending private app data. Bail out
|
// hacker sending private app data. Bail out
|
||||||
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.ERROR)) {
|
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.ERROR)) {
|
||||||
LogUtil.e(LogUtil.BUGLE_TAG, "Aborting attach of private app data ("
|
LogUtil.e(LogUtil.BUGLE_TAG, "Aborting attach of private app data ("
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
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 final CameraPreview mPreview;
|
|
||||||
|
|
||||||
public HardwareCameraPreview(final Context context, final AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
mPreview = new CameraPreview(this);
|
|
||||||
setSurfaceTextureListener(new SurfaceTextureListener() {
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureAvailable(@NonNull final SurfaceTexture surfaceTexture,
|
|
||||||
final int i, final int i2) {
|
|
||||||
CameraManager.get().setSurface(mPreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureSizeChanged(@NonNull final SurfaceTexture surfaceTexture,
|
|
||||||
final int i, final int i2) {
|
|
||||||
CameraManager.get().setSurface(mPreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSurfaceTextureDestroyed(@NonNull final SurfaceTexture surfaceTexture) {
|
|
||||||
CameraManager.get().setSurface(null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureUpdated(@NonNull 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,173 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 = androidx.exifinterface.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,6 +30,7 @@ import android.widget.ImageButton;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
@@ -179,7 +180,6 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
};
|
};
|
||||||
|
|
||||||
mOpen = false;
|
mOpen = false;
|
||||||
setSupportedMediaTypes(MEDIA_TYPE_ALL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean mIsAttached;
|
private boolean mIsAttached;
|
||||||
@@ -190,6 +190,7 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
public void onAttach(@NonNull final Context context) {
|
public void onAttach(@NonNull final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
mIsAttached = true;
|
mIsAttached = true;
|
||||||
|
setSupportedMediaTypes(MEDIA_TYPE_ALL);
|
||||||
if (mStartingMediaTypeOnAttach != MEDA_TYPE_INVALID) {
|
if (mStartingMediaTypeOnAttach != MEDA_TYPE_INVALID) {
|
||||||
// open() was previously called. Do the pending open now.
|
// open() was previously called. Do the pending open now.
|
||||||
doOpen(mStartingMediaTypeOnAttach, mAnimateOnAttach);
|
doOpen(mStartingMediaTypeOnAttach, mAnimateOnAttach);
|
||||||
@@ -215,11 +216,9 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
mTabStrip.setBackgroundColor(mThemeColor);
|
mTabStrip.setBackgroundColor(mThemeColor);
|
||||||
for (final MediaChooser chooser : mChoosers) {
|
for (final MediaChooser chooser : mChoosers) {
|
||||||
chooser.onCreateTabButton(inflater, mTabStrip);
|
chooser.onCreateTabButton(inflater, mTabStrip);
|
||||||
final boolean enabled = (chooser.getSupportedMediaTypes() & mSupportedMediaTypes) !=
|
|
||||||
MEDIA_TYPE_NONE;
|
|
||||||
final ImageButton tabButton = chooser.getTabButton();
|
final ImageButton tabButton = chooser.getTabButton();
|
||||||
if (tabButton != null) {
|
if (tabButton != null) {
|
||||||
tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
tabButton.setVisibility(View.GONE); // we make them visible in onViewCreated
|
||||||
mTabStrip.addView(tabButton);
|
mTabStrip.addView(tabButton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,10 +259,23 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
return mMediaPickerPanel;
|
return mMediaPickerPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
for (final MediaChooser chooser : mChoosers) {
|
||||||
|
final boolean enabled = (chooser.getSupportedMediaTypes() & mSupportedMediaTypes) !=
|
||||||
|
MEDIA_TYPE_NONE;
|
||||||
|
final ImageButton tabButton = chooser.getTabButton();
|
||||||
|
if (tabButton != null) {
|
||||||
|
tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
CameraManager.get().onPause();
|
|
||||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||||
chooser.onPause();
|
chooser.onPause();
|
||||||
}
|
}
|
||||||
@@ -272,7 +284,6 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
CameraManager.get().onResume();
|
|
||||||
|
|
||||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||||
chooser.onResume();
|
chooser.onResume();
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
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(@NonNull final SurfaceHolder surfaceHolder) {
|
|
||||||
CameraManager.get().setSurface(mPreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceChanged(@NonNull final SurfaceHolder surfaceHolder, final int format,
|
|
||||||
final int width, final int height) {
|
|
||||||
CameraManager.get().setSurface(mPreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceDestroyed(@NonNull final SurfaceHolder surfaceHolder) {
|
|
||||||
CameraManager.get().setSurface(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onVisibilityChanged(@NonNull 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,25 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 {
|
|
||||||
void showStart();
|
|
||||||
void showSuccess(boolean timeout);
|
|
||||||
void showFail(boolean timeout);
|
|
||||||
void clear();
|
|
||||||
}
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 final 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 final Handler mHandler;
|
|
||||||
private final Listener mListener;
|
|
||||||
|
|
||||||
public interface Listener {
|
|
||||||
void autoFocus();
|
|
||||||
void cancelAutoFocus();
|
|
||||||
boolean capture();
|
|
||||||
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<>();
|
|
||||||
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<>();
|
|
||||||
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.contains(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,183 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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 interface OnClickListener {
|
|
||||||
void onClick(PieItem item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Drawable mDrawable;
|
|
||||||
private final 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;
|
|
||||||
|
|
||||||
// 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<>();
|
|
||||||
}
|
|
||||||
mItems.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPath(Path p) {
|
|
||||||
mPath = p;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path getPath() {
|
|
||||||
return mPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAlpha(float 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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,825 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Looper;
|
|
||||||
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 final 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 final Runnable mDisappear = new Disappear();
|
|
||||||
private final 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 final Handler mHandler = new Handler(Looper.getMainLooper()) {
|
|
||||||
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 interface PieListener {
|
|
||||||
void onPieOpened(int centerX, int centerY);
|
|
||||||
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<>();
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
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 final float mFrom;
|
|
||||||
private final 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,181 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
* Copyright (C) 2024 The LineageOS Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class RenderOverlay extends FrameLayout {
|
|
||||||
|
|
||||||
interface Renderer {
|
|
||||||
|
|
||||||
boolean handlesTouch();
|
|
||||||
boolean onTouchEvent(MotionEvent evt);
|
|
||||||
void setOverlay(RenderOverlay overlay);
|
|
||||||
void layout(int left, int top, int right, int bottom);
|
|
||||||
void draw(Canvas canvas);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private final RenderView mRenderView;
|
|
||||||
private final List<Renderer> mClients;
|
|
||||||
|
|
||||||
// reverse list of touch clients
|
|
||||||
private final List<Renderer> mTouchClients;
|
|
||||||
private final 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<>(10);
|
|
||||||
mTouchClients = new ArrayList<>(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(@NonNull 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