Merge changes Ie6015f71,Ib14d60c0 into udc-dev
* changes: Delay clipboard entrance animation until image loaded Add flag for clipboard image timeout
This commit is contained in:
committed by
Android (Google) Code Review
commit
2cdde680a1
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.systemui.clipboardoverlay
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import java.io.IOException
|
||||
import java.util.function.Consumer
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class ClipboardImageLoader
|
||||
@Inject
|
||||
constructor(
|
||||
private val context: Context,
|
||||
@Background private val bgDispatcher: CoroutineDispatcher,
|
||||
@Application private val mainScope: CoroutineScope
|
||||
) {
|
||||
private val TAG: String = "ClipboardImageLoader"
|
||||
|
||||
suspend fun load(uri: Uri, timeoutMs: Long = 300) =
|
||||
withTimeoutOrNull(timeoutMs) {
|
||||
withContext(bgDispatcher) {
|
||||
try {
|
||||
val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale)
|
||||
context.contentResolver.loadThumbnail(uri, Size(size, size * 4), null)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Thumbnail loading failed!", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAsync(uri: Uri, callback: Consumer<Bitmap?>) {
|
||||
mainScope.launch { callback.accept(load(uri)) }
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO
|
||||
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
|
||||
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
|
||||
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
|
||||
import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
@@ -90,6 +91,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
private final ClipboardOverlayUtils mClipboardUtils;
|
||||
private final FeatureFlags mFeatureFlags;
|
||||
private final Executor mBgExecutor;
|
||||
private final ClipboardImageLoader mClipboardImageLoader;
|
||||
|
||||
private final ClipboardOverlayView mView;
|
||||
|
||||
@@ -109,6 +111,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
|
||||
private Runnable mOnUiUpdate;
|
||||
|
||||
private boolean mShowingUi;
|
||||
private boolean mIsMinimized;
|
||||
private ClipboardModel mClipboardModel;
|
||||
|
||||
@@ -175,9 +178,11 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
FeatureFlags featureFlags,
|
||||
ClipboardOverlayUtils clipboardUtils,
|
||||
@Background Executor bgExecutor,
|
||||
ClipboardImageLoader clipboardImageLoader,
|
||||
UiEventLogger uiEventLogger) {
|
||||
mContext = context;
|
||||
mBroadcastDispatcher = broadcastDispatcher;
|
||||
mClipboardImageLoader = clipboardImageLoader;
|
||||
|
||||
mClipboardLogger = new ClipboardLogger(uiEventLogger);
|
||||
|
||||
@@ -260,21 +265,42 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting;
|
||||
mClipboardModel = model;
|
||||
mClipboardLogger.setClipSource(mClipboardModel.getSource());
|
||||
if (shouldAnimate) {
|
||||
reset();
|
||||
mClipboardLogger.setClipSource(mClipboardModel.getSource());
|
||||
if (shouldShowMinimized(mWindow.getWindowInsets())) {
|
||||
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
|
||||
mIsMinimized = true;
|
||||
mView.setMinimized(true);
|
||||
} else {
|
||||
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
|
||||
if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
|
||||
if (shouldAnimate) {
|
||||
reset();
|
||||
mClipboardLogger.setClipSource(mClipboardModel.getSource());
|
||||
if (shouldShowMinimized(mWindow.getWindowInsets())) {
|
||||
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
|
||||
mIsMinimized = true;
|
||||
mView.setMinimized(true);
|
||||
} else {
|
||||
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
|
||||
setExpandedView(this::animateIn);
|
||||
}
|
||||
mView.announceForAccessibility(
|
||||
getAccessibilityAnnouncement(mClipboardModel.getType()));
|
||||
} else if (!mIsMinimized) {
|
||||
setExpandedView(() -> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (shouldAnimate) {
|
||||
reset();
|
||||
mClipboardLogger.setClipSource(mClipboardModel.getSource());
|
||||
if (shouldShowMinimized(mWindow.getWindowInsets())) {
|
||||
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
|
||||
mIsMinimized = true;
|
||||
mView.setMinimized(true);
|
||||
} else {
|
||||
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
|
||||
setExpandedView();
|
||||
animateIn();
|
||||
}
|
||||
mView.announceForAccessibility(
|
||||
getAccessibilityAnnouncement(mClipboardModel.getType()));
|
||||
} else if (!mIsMinimized) {
|
||||
setExpandedView();
|
||||
}
|
||||
animateIn();
|
||||
mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType()));
|
||||
} else if (!mIsMinimized) {
|
||||
setExpandedView();
|
||||
}
|
||||
if (mClipboardModel.isRemote()) {
|
||||
mTimeoutHandler.cancelTimeout();
|
||||
@@ -285,6 +311,58 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpandedView(Runnable onViewReady) {
|
||||
final ClipboardModel model = mClipboardModel;
|
||||
mView.setMinimized(false);
|
||||
switch (model.getType()) {
|
||||
case TEXT:
|
||||
if (model.isRemote() || DeviceConfig.getBoolean(
|
||||
DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
|
||||
if (model.getTextLinks() != null) {
|
||||
classifyText(model);
|
||||
}
|
||||
}
|
||||
if (model.isSensitive()) {
|
||||
mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
|
||||
} else {
|
||||
mView.showTextPreview(model.getText().toString(), false);
|
||||
}
|
||||
mView.setEditAccessibilityAction(true);
|
||||
mOnPreviewTapped = this::editText;
|
||||
onViewReady.run();
|
||||
break;
|
||||
case IMAGE:
|
||||
mView.setEditAccessibilityAction(true);
|
||||
mOnPreviewTapped = () -> editImage(model.getUri());
|
||||
if (model.isSensitive()) {
|
||||
mView.showImagePreview(null);
|
||||
onViewReady.run();
|
||||
} else {
|
||||
mClipboardImageLoader.loadAsync(model.getUri(), (bitmap) -> mView.post(() -> {
|
||||
if (bitmap == null) {
|
||||
mView.showDefaultTextPreview();
|
||||
} else {
|
||||
mView.showImagePreview(bitmap);
|
||||
}
|
||||
onViewReady.run();
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case URI:
|
||||
case OTHER:
|
||||
mView.showDefaultTextPreview();
|
||||
onViewReady.run();
|
||||
break;
|
||||
}
|
||||
if (!model.isRemote()) {
|
||||
maybeShowRemoteCopy(model.getClipData());
|
||||
}
|
||||
if (model.getType() != ClipboardModel.Type.OTHER) {
|
||||
mOnShareTapped = () -> shareContent(model.getClipData());
|
||||
mView.showShareChip();
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpandedView() {
|
||||
final ClipboardModel model = mClipboardModel;
|
||||
mView.setMinimized(false);
|
||||
@@ -350,8 +428,12 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED);
|
||||
mIsMinimized = false;
|
||||
}
|
||||
setExpandedView();
|
||||
animateIn();
|
||||
if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
|
||||
setExpandedView(() -> animateIn());
|
||||
} else {
|
||||
setExpandedView();
|
||||
animateIn();
|
||||
}
|
||||
}
|
||||
});
|
||||
mEnterAnimator.start();
|
||||
@@ -412,7 +494,8 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void onInputEvent(InputEvent event) {
|
||||
if (event instanceof MotionEvent) {
|
||||
if ((!mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT) || mShowingUi)
|
||||
&& event instanceof MotionEvent) {
|
||||
MotionEvent motionEvent = (MotionEvent) event;
|
||||
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
if (!mView.isInTouchRegion(
|
||||
@@ -451,6 +534,12 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
}
|
||||
mEnterAnimator = mView.getEnterAnimation();
|
||||
mEnterAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
super.onAnimationStart(animation);
|
||||
mShowingUi = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
@@ -518,6 +607,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
|
||||
mOnRemoteCopyTapped = null;
|
||||
mOnShareTapped = null;
|
||||
mOnPreviewTapped = null;
|
||||
mShowingUi = false;
|
||||
mView.reset();
|
||||
mTimeoutHandler.cancelTimeout();
|
||||
mClipboardLogger.reset();
|
||||
|
||||
@@ -600,6 +600,8 @@ object Flags {
|
||||
|
||||
// 1700 - clipboard
|
||||
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
|
||||
// TODO(b/278714186) Tracking Bug
|
||||
@JvmField val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout")
|
||||
|
||||
// 1800 - shade container
|
||||
// TODO(b/265944639): Tracking Bug
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.systemui.clipboardoverlay
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ClipboardImageLoaderTest : SysuiTestCase() {
|
||||
@Mock private lateinit var mockContext: Context
|
||||
|
||||
@Mock private lateinit var mockContentResolver: ContentResolver
|
||||
|
||||
private lateinit var clipboardImageLoader: ClipboardImageLoader
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun test_imageLoadSuccess() = runTest {
|
||||
val testDispatcher = StandardTestDispatcher(this.testScheduler)
|
||||
clipboardImageLoader =
|
||||
ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher))
|
||||
val testUri = Uri.parse("testUri")
|
||||
whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
|
||||
whenever(mockContext.resources).thenReturn(context.resources)
|
||||
|
||||
clipboardImageLoader.load(testUri)
|
||||
|
||||
verify(mockContentResolver).loadThumbnail(eq(testUri), any(), any())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun test_imageLoadFailure() = runTest {
|
||||
val testDispatcher = StandardTestDispatcher(this.testScheduler)
|
||||
clipboardImageLoader =
|
||||
ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher))
|
||||
val testUri = Uri.parse("testUri")
|
||||
whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
|
||||
whenever(mockContext.resources).thenReturn(context.resources)
|
||||
|
||||
val res = clipboardImageLoader.load(testUri)
|
||||
|
||||
verify(mockContentResolver).loadThumbnail(eq(testUri), any(), any())
|
||||
assertNull(res)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO
|
||||
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_EXPANDED;
|
||||
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED;
|
||||
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
|
||||
import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
@@ -90,6 +91,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
|
||||
@Mock
|
||||
private ClipboardOverlayUtils mClipboardUtils;
|
||||
@Mock
|
||||
private ClipboardImageLoader mClipboardImageLoader;
|
||||
@Mock
|
||||
private UiEventLogger mUiEventLogger;
|
||||
private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
|
||||
private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
|
||||
@@ -120,6 +123,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
|
||||
mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
|
||||
new ClipData.Item("Test Item"));
|
||||
|
||||
mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, true); // turned off for legacy tests
|
||||
|
||||
mOverlayController = new ClipboardOverlayController(
|
||||
mContext,
|
||||
@@ -131,6 +135,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
|
||||
mFeatureFlags,
|
||||
mClipboardUtils,
|
||||
mExecutor,
|
||||
mClipboardImageLoader,
|
||||
mUiEventLogger);
|
||||
verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
|
||||
mCallbacks = mOverlayCallbacksCaptor.getValue();
|
||||
@@ -141,6 +146,69 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
|
||||
mOverlayController.hideImmediate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setClipData_invalidImageData_legacy() {
|
||||
ClipData clipData = new ClipData("", new String[]{"image/png"},
|
||||
new ClipData.Item(Uri.parse("")));
|
||||
mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
|
||||
|
||||
mOverlayController.setClipData(clipData, "");
|
||||
|
||||
verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
|
||||
verify(mClipboardOverlayView, times(1)).showShareChip();
|
||||
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setClipData_nonImageUri_legacy() {
|
||||
ClipData clipData = new ClipData("", new String[]{"resource/png"},
|
||||
new ClipData.Item(Uri.parse("")));
|
||||
mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
|
||||
|
||||
mOverlayController.setClipData(clipData, "");
|
||||
|
||||
verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
|
||||
verify(mClipboardOverlayView, times(1)).showShareChip();
|
||||
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setClipData_textData_legacy() {
|
||||
mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
|
||||
mOverlayController.setClipData(mSampleClipData, "abc");
|
||||
|
||||
verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
|
||||
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "abc");
|
||||
verify(mClipboardOverlayView, times(1)).showShareChip();
|
||||
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setClipData_sensitiveTextData_legacy() {
|
||||
mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
|
||||
ClipDescription description = mSampleClipData.getDescription();
|
||||
PersistableBundle b = new PersistableBundle();
|
||||
b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
|
||||
description.setExtras(b);
|
||||
ClipData data = new ClipData(description, mSampleClipData.getItemAt(0));
|
||||
mOverlayController.setClipData(data, "");
|
||||
|
||||
verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true);
|
||||
verify(mClipboardOverlayView, times(1)).showShareChip();
|
||||
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setClipData_repeatedCalls_legacy() {
|
||||
when(mAnimator.isRunning()).thenReturn(true);
|
||||
mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
|
||||
|
||||
mOverlayController.setClipData(mSampleClipData, "");
|
||||
mOverlayController.setClipData(mSampleClipData, "");
|
||||
|
||||
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setClipData_invalidImageData() {
|
||||
ClipData clipData = new ClipData("", new String[]{"image/png"},
|
||||
|
||||
Reference in New Issue
Block a user