Add clipboard overlay prototype
Adds a floating window/overlay (similar to screenshots) when something is copied to the clipboard. Opens Markup or a lightweight test editing activity (as appropriate) when edit is tapped. Bug: 195554988 Test: manual (tested copying text/images, and interactions with screenshots) Change-Id: I02aff237a1245ed2fed130d5e43112472d320a9c
This commit is contained in:
@@ -523,6 +523,12 @@ public final class SystemUiDeviceConfigFlags {
|
||||
*/
|
||||
public static final String TASK_MANAGER_ENABLED = "task_manager_enabled";
|
||||
|
||||
|
||||
/**
|
||||
* (boolean) Whether the clipboard overlay is enabled.
|
||||
*/
|
||||
public static final String CLIPBOARD_OVERLAY_ENABLED = "clipboard_overlay_enabled";
|
||||
|
||||
private SystemUiDeviceConfigFlags() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +299,9 @@
|
||||
|
||||
<uses-permission android:name="android.permission.BIND_APPWIDGET" />
|
||||
|
||||
<!-- For clipboard overlay -->
|
||||
<uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND" />
|
||||
|
||||
<protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
|
||||
<protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
|
||||
<protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
|
||||
@@ -348,6 +351,7 @@
|
||||
<!-- started from PhoneWindowManager
|
||||
TODO: Should have an android:permission attribute -->
|
||||
<service android:name=".screenshot.TakeScreenshotService"
|
||||
android:permission="com.android.systemui.permission.SELF"
|
||||
android:process=":screenshot"
|
||||
android:exported="false" />
|
||||
|
||||
@@ -760,6 +764,12 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".clipboardoverlay.EditTextActivity"
|
||||
android:theme="@style/EditTextActivity"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
/>
|
||||
|
||||
<activity android:name=".controls.management.ControlsProviderSelectorActivity"
|
||||
android:label="@string/controls_providers_title"
|
||||
android:theme="@style/Theme.ControlsManagement"
|
||||
|
||||
27
packages/SystemUI/res/drawable/ic_baseline_devices_24.xml
Normal file
27
packages/SystemUI/res/drawable/ic_baseline_devices_24.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
Copyright (C) 2020 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
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,6h18L22,4L4,4c-1.1,0 -2,0.9 -2,2v11L0,17v3h14v-3L4,17L4,6zM23,8h-6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1L24,9c0,-0.55 -0.45,-1 -1,-1zM22,17h-4v-7h4v7z"/>
|
||||
</vector>
|
||||
|
||||
60
packages/SystemUI/res/layout/clipboard_content_preview.xml
Normal file
60
packages/SystemUI/res/layout/clipboard_content_preview.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2021 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.
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/preview_border"
|
||||
android:elevation="9dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/screenshot_offset_x"
|
||||
android:layout_marginBottom="@dimen/screenshot_offset_y"
|
||||
android:layout_gravity="bottom|start"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:clipToPadding="false"
|
||||
android:clipChildren="false"
|
||||
android:padding="4dp"
|
||||
android:background="@drawable/screenshot_border"
|
||||
>
|
||||
<FrameLayout
|
||||
android:elevation="0dp"
|
||||
android:background="@drawable/screenshot_preview_background"
|
||||
android:clipChildren="true"
|
||||
android:clipToOutline="true"
|
||||
android:clipToPadding="true"
|
||||
android:layout_width="@dimen/screenshot_x_scale"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView android:id="@+id/text_preview"
|
||||
android:textFontWeight="500"
|
||||
android:padding="8dp"
|
||||
android:gravity="center|start"
|
||||
android:ellipsize="end"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:autoSizeMinTextSize="10sp"
|
||||
android:autoSizeMaxTextSize="200sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:layout_width="@dimen/screenshot_x_scale"
|
||||
android:layout_height="@dimen/screenshot_x_scale"/>
|
||||
<ImageView
|
||||
android:id="@+id/image_preview"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
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_height="match_parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/copy_button"
|
||||
style="@android:style/Widget.DeviceDefault.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/clipboard_edit_text_copy"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attribution"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="@+id/copy_button"
|
||||
app:layout_constraintTop_toBottomOf="@+id/copy_button" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/share"
|
||||
style="@android:style/Widget.Material.Button.Borderless"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:padding="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@*android:string/share"
|
||||
android:tooltipText="@*android:string/share"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/copy_button"
|
||||
android:src="@drawable/ic_screenshot_share" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/copy_button"
|
||||
app:layout_constraintTop_toBottomOf="@+id/attribution">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|top"
|
||||
android:textSize="24sp" />
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
61
packages/SystemUI/res/layout/clipboard_overlay.xml
Normal file
61
packages/SystemUI/res/layout/clipboard_overlay.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2021 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.
|
||||
-->
|
||||
<com.android.systemui.clipboardoverlay.DraggableConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
android:id="@+id/actions_container_background"
|
||||
android:visibility="gone"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="0dp"
|
||||
android:elevation="1dp"
|
||||
android:background="@drawable/action_chip_container_background"
|
||||
android:layout_marginStart="@dimen/screenshot_action_container_margin_horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/actions_container"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/actions_container"
|
||||
app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/actions_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/screenshot_action_container_margin_horizontal"
|
||||
android:paddingEnd="@dimen/screenshot_action_container_padding_right"
|
||||
android:paddingVertical="@dimen/screenshot_action_container_padding_vertical"
|
||||
android:elevation="1dp"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintWidth_percent="1.0"
|
||||
app:layout_constraintWidth_max="wrap"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/preview_border"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/actions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<include layout="@layout/screenshot_action_chip"
|
||||
android:id="@+id/remote_copy_chip"/>
|
||||
<include layout="@layout/screenshot_action_chip"
|
||||
android:id="@+id/edit_chip"/>
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
<include layout="@layout/clipboard_content_preview" />
|
||||
</com.android.systemui.clipboardoverlay.DraggableConstraintLayout>
|
||||
@@ -38,6 +38,15 @@
|
||||
<item name="android:textColorSecondary">?android:attr/textColorPrimaryInverse</item>
|
||||
</style>
|
||||
|
||||
<!-- Clipboard overlay's edit text activity. -->
|
||||
<style name="EditTextActivity" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:navigationBarColor">?android:attr/colorBackgroundFloating</item>
|
||||
<item name="android:textColorSecondary">?android:attr/textColorPrimaryInverse</item>
|
||||
</style>
|
||||
|
||||
<style name="Screenshot" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||
<item name="android:textColorPrimary">?android:attr/textColorPrimaryInverse</item>
|
||||
</style>
|
||||
|
||||
@@ -315,6 +315,7 @@
|
||||
<item>com.android.systemui.accessibility.SystemActions</item>
|
||||
<item>com.android.systemui.toast.ToastUI</item>
|
||||
<item>com.android.systemui.wmshell.WMShell</item>
|
||||
<item>com.android.systemui.clipboardoverlay.ClipboardListener</item>
|
||||
</string-array>
|
||||
|
||||
<!-- SystemUI Services: The classes of the additional stuff to start. Services here are
|
||||
|
||||
@@ -2349,4 +2349,9 @@
|
||||
<string name="fgs_manager_dialog_title">Apps running in the background</string>
|
||||
<!-- Label of the button to stop the app from running in the background [CHAR LIMIT=12]-->
|
||||
<string name="fgs_manager_app_item_stop_button_label">Stop</string>
|
||||
|
||||
<!-- Label for button to copy edited text back to the clipboard [CHAR LIMIT=20] -->
|
||||
<string name="clipboard_edit_text_copy">Copy</string>
|
||||
<!-- Text informing user that content has been copied to the system clipboard [CHAR LIMIT=NONE] -->
|
||||
<string name="clipboard_overlay_text_copied">Copied</string>
|
||||
</resources>
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
|
||||
<style name="Screenshot" parent="@android:style/Theme.DeviceDefault.DayNight"/>
|
||||
|
||||
<!-- Clipboard overlay's edit text activity. -->
|
||||
<style name="EditTextActivity" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
<item name="android:navigationBarColor">?android:attr/colorBackgroundFloating</item>
|
||||
</style>
|
||||
|
||||
<!-- Privacy dialog -->
|
||||
<style name="PrivacyDialog" parent="Theme.SystemUI.QuickSettings.Dialog">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.provider.DeviceConfig;
|
||||
|
||||
import com.android.systemui.CoreStartable;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.screenshot.TimeoutHandler;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
|
||||
*/
|
||||
@SysUISingleton
|
||||
public class ClipboardListener extends CoreStartable
|
||||
implements ClipboardManager.OnPrimaryClipChangedListener {
|
||||
|
||||
private ClipboardOverlayController mClipboardOverlayController;
|
||||
private ClipboardManager mClipboardManager;
|
||||
|
||||
@Inject
|
||||
public ClipboardListener(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (DeviceConfig.getBoolean(
|
||||
DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, false)) {
|
||||
mClipboardManager = requireNonNull(mContext.getSystemService(ClipboardManager.class));
|
||||
mClipboardManager.addPrimaryClipChangedListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrimaryClipChanged() {
|
||||
if (!mClipboardManager.hasPrimaryClip()) {
|
||||
return;
|
||||
}
|
||||
if (mClipboardOverlayController == null) {
|
||||
mClipboardOverlayController = new ClipboardOverlayController(mContext,
|
||||
new TimeoutHandler(mContext));
|
||||
}
|
||||
mClipboardOverlayController.setClipData(mClipboardManager.getPrimaryClip());
|
||||
mClipboardOverlayController.setOnSessionCompleteListener(() -> {
|
||||
// Session is complete, free memory until it's needed again.
|
||||
mClipboardOverlayController = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
|
||||
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.MainThread;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ClipData;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.Display;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputEvent;
|
||||
import android.view.InputEventReceiver;
|
||||
import android.view.InputMonitor;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.policy.PhoneWindow;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.screenshot.FloatingWindowUtil;
|
||||
import com.android.systemui.screenshot.ScreenshotActionChip;
|
||||
import com.android.systemui.screenshot.TimeoutHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Controls state and UI for the overlay that appears when something is added to the clipboard
|
||||
*/
|
||||
public class ClipboardOverlayController {
|
||||
private static final String TAG = "ClipboardOverlayCtrlr";
|
||||
private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY";
|
||||
|
||||
/** Constants for screenshot/copy deconflicting */
|
||||
public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT";
|
||||
public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF";
|
||||
public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
|
||||
|
||||
private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
|
||||
|
||||
private final Context mContext;
|
||||
private final DisplayManager mDisplayManager;
|
||||
private final WindowManager mWindowManager;
|
||||
private final WindowManager.LayoutParams mWindowLayoutParams;
|
||||
private final PhoneWindow mWindow;
|
||||
private final TimeoutHandler mTimeoutHandler;
|
||||
|
||||
private final DraggableConstraintLayout mView;
|
||||
private final ImageView mImagePreview;
|
||||
private final TextView mTextPreview;
|
||||
private final ScreenshotActionChip mEditChip;
|
||||
private final ScreenshotActionChip mRemoteCopyChip;
|
||||
private final View mActionContainerBackground;
|
||||
|
||||
private Runnable mOnSessionCompleteListener;
|
||||
|
||||
private InputEventReceiver mInputEventReceiver;
|
||||
|
||||
private BroadcastReceiver mCloseDialogsReceiver;
|
||||
private BroadcastReceiver mScreenshotReceiver;
|
||||
|
||||
private boolean mBlockAttach = false;
|
||||
|
||||
public ClipboardOverlayController(Context context, TimeoutHandler timeoutHandler) {
|
||||
mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
|
||||
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
|
||||
mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
|
||||
|
||||
mWindowManager = mContext.getSystemService(WindowManager.class);
|
||||
|
||||
mTimeoutHandler = timeoutHandler;
|
||||
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
|
||||
|
||||
// Setup the window that we are going to use
|
||||
mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
|
||||
mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||
mWindowLayoutParams.height = WRAP_CONTENT;
|
||||
mWindowLayoutParams.gravity = Gravity.BOTTOM;
|
||||
mWindowLayoutParams.setTitle("ClipboardOverlay");
|
||||
mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
|
||||
mWindow.setWindowManager(mWindowManager, null, null);
|
||||
|
||||
mView = (DraggableConstraintLayout)
|
||||
LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay, null);
|
||||
mActionContainerBackground = requireNonNull(
|
||||
mView.findViewById(R.id.actions_container_background));
|
||||
mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
|
||||
mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
|
||||
mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
|
||||
mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
|
||||
|
||||
mView.setOnDismissCallback(this::hideImmediate);
|
||||
mView.setOnInteractionCallback(() -> mTimeoutHandler.resetTimeout());
|
||||
|
||||
mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
|
||||
mRemoteCopyChip.setIcon(
|
||||
Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
|
||||
|
||||
// Only show remote copy if it's available.
|
||||
PackageManager packageManager = mContext.getPackageManager();
|
||||
if (packageManager.resolveActivity(getRemoteCopyIntent(), 0) != null) {
|
||||
mRemoteCopyChip.setOnClickListener((v) -> {
|
||||
showNearby();
|
||||
});
|
||||
mRemoteCopyChip.setAlpha(1f);
|
||||
} else {
|
||||
mRemoteCopyChip.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
attachWindow();
|
||||
withWindowAttached(() -> {
|
||||
mWindow.setContentView(mView);
|
||||
updateInsets(mWindowManager.getCurrentWindowMetrics().getWindowInsets());
|
||||
getEnterAnimation().start();
|
||||
});
|
||||
|
||||
mTimeoutHandler.setOnTimeoutRunnable(() -> animateOut());
|
||||
|
||||
mCloseDialogsReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
|
||||
animateOut();
|
||||
}
|
||||
}
|
||||
};
|
||||
mContext.registerReceiver(mCloseDialogsReceiver,
|
||||
new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
|
||||
mScreenshotReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (SCREENSHOT_ACTION.equals(intent.getAction())) {
|
||||
animateOut();
|
||||
}
|
||||
}
|
||||
};
|
||||
mContext.registerReceiver(mScreenshotReceiver, new IntentFilter(SCREENSHOT_ACTION),
|
||||
SELF_PERMISSION, null);
|
||||
monitorOutsideTouches();
|
||||
|
||||
mContext.sendBroadcast(new Intent(COPY_OVERLAY_ACTION), SELF_PERMISSION);
|
||||
}
|
||||
|
||||
void setClipData(ClipData clipData) {
|
||||
reset();
|
||||
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
showTextPreview(
|
||||
mContext.getResources().getString(R.string.clipboard_overlay_text_copied));
|
||||
} else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
|
||||
showEditableText(clipData.getItemAt(0).getText());
|
||||
} else if (clipData.getItemAt(0).getUri() != null) {
|
||||
// How to handle non-image URIs?
|
||||
showEditableImage(clipData.getItemAt(0).getUri());
|
||||
} else {
|
||||
showTextPreview(
|
||||
mContext.getResources().getString(R.string.clipboard_overlay_text_copied));
|
||||
}
|
||||
|
||||
mTimeoutHandler.resetTimeout();
|
||||
}
|
||||
|
||||
void setOnSessionCompleteListener(Runnable runnable) {
|
||||
mOnSessionCompleteListener = runnable;
|
||||
}
|
||||
|
||||
private void monitorOutsideTouches() {
|
||||
InputManager inputManager = mContext.getSystemService(InputManager.class);
|
||||
InputMonitor monitor = inputManager.monitorGestureInput("clipboard overlay", 0);
|
||||
mInputEventReceiver = new InputEventReceiver(monitor.getInputChannel(),
|
||||
Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void onInputEvent(InputEvent event) {
|
||||
if (event instanceof MotionEvent) {
|
||||
MotionEvent motionEvent = (MotionEvent) event;
|
||||
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
int[] pt = new int[2];
|
||||
mView.getLocationOnScreen(pt);
|
||||
Rect rect = new Rect(pt[0], pt[1], pt[0] + mView.getWidth(),
|
||||
pt[1] + mView.getHeight());
|
||||
if (!rect.contains((int) motionEvent.getRawX(),
|
||||
(int) motionEvent.getRawY())) {
|
||||
animateOut();
|
||||
}
|
||||
}
|
||||
}
|
||||
finishInputEvent(event, true /* handled */);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void editImage(Uri uri) {
|
||||
String editorPackage = mContext.getString(R.string.config_screenshotEditor);
|
||||
Intent editIntent = new Intent(Intent.ACTION_EDIT);
|
||||
if (!TextUtils.isEmpty(editorPackage)) {
|
||||
editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
|
||||
}
|
||||
editIntent.setDataAndType(uri, "image/*");
|
||||
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
mContext.startActivity(editIntent);
|
||||
animateOut();
|
||||
}
|
||||
|
||||
private void editText() {
|
||||
Intent editIntent = new Intent(mContext, EditTextActivity.class);
|
||||
editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
mContext.startActivity(editIntent);
|
||||
animateOut();
|
||||
}
|
||||
|
||||
private void showNearby() {
|
||||
mContext.startActivity(getRemoteCopyIntent());
|
||||
animateOut();
|
||||
}
|
||||
|
||||
private void showTextPreview(CharSequence text) {
|
||||
mTextPreview.setVisibility(View.VISIBLE);
|
||||
mImagePreview.setVisibility(View.GONE);
|
||||
mTextPreview.setText(text);
|
||||
mEditChip.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void showEditableText(CharSequence text) {
|
||||
showTextPreview(text);
|
||||
mEditChip.setVisibility(View.VISIBLE);
|
||||
mEditChip.setAlpha(1f);
|
||||
View.OnClickListener listener = v -> editText();
|
||||
mEditChip.setOnClickListener(listener);
|
||||
mTextPreview.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
private void showEditableImage(Uri uri) {
|
||||
mTextPreview.setVisibility(View.GONE);
|
||||
mImagePreview.setVisibility(View.VISIBLE);
|
||||
mEditChip.setAlpha(1f);
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
try {
|
||||
int size = mContext.getResources().getDimensionPixelSize(R.dimen.screenshot_x_scale);
|
||||
// The width of the view is capped, height maintains aspect ratio, so allow it to be
|
||||
// taller if needed.
|
||||
Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
|
||||
mImagePreview.setImageBitmap(thumbnail);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Thumbnail loading failed", e);
|
||||
}
|
||||
View.OnClickListener listener = v -> editImage(uri);
|
||||
mEditChip.setOnClickListener(listener);
|
||||
mImagePreview.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
private Intent getRemoteCopyIntent() {
|
||||
Intent nearbyIntent = new Intent(REMOTE_COPY_ACTION);
|
||||
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
return nearbyIntent;
|
||||
}
|
||||
|
||||
private void animateOut() {
|
||||
getExitAnimation().start();
|
||||
}
|
||||
|
||||
private ValueAnimator getEnterAnimation() {
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
||||
|
||||
mView.setAlpha(0);
|
||||
final View previewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
|
||||
final View actionBackground = requireNonNull(
|
||||
mView.findViewById(R.id.actions_container_background));
|
||||
mImagePreview.setVisibility(View.VISIBLE);
|
||||
mActionContainerBackground.setVisibility(View.VISIBLE);
|
||||
|
||||
anim.addUpdateListener(animation -> {
|
||||
mView.setAlpha(animation.getAnimatedFraction());
|
||||
float scale = 0.6f + 0.4f * animation.getAnimatedFraction();
|
||||
mView.setPivotY(mView.getHeight() - previewBorder.getHeight() / 2f);
|
||||
mView.setPivotX(actionBackground.getWidth() / 2f);
|
||||
mView.setScaleX(scale);
|
||||
mView.setScaleY(scale);
|
||||
});
|
||||
anim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
mView.setAlpha(1);
|
||||
mTimeoutHandler.resetTimeout();
|
||||
}
|
||||
});
|
||||
return anim;
|
||||
}
|
||||
|
||||
private ValueAnimator getExitAnimation() {
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
||||
|
||||
anim.addUpdateListener(animation -> {
|
||||
mView.setAlpha(1 - animation.getAnimatedFraction());
|
||||
final View actionBackground = requireNonNull(
|
||||
mView.findViewById(R.id.actions_container_background));
|
||||
mView.setTranslationX(
|
||||
-animation.getAnimatedFraction() * actionBackground.getWidth() / 2);
|
||||
});
|
||||
|
||||
anim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
hideImmediate();
|
||||
}
|
||||
});
|
||||
|
||||
return anim;
|
||||
}
|
||||
|
||||
private void hideImmediate() {
|
||||
// Note this may be called multiple times if multiple dismissal events happen at the same
|
||||
// time.
|
||||
mTimeoutHandler.cancelTimeout();
|
||||
final View decorView = mWindow.peekDecorView();
|
||||
if (decorView != null && decorView.isAttachedToWindow()) {
|
||||
mWindowManager.removeViewImmediate(decorView);
|
||||
}
|
||||
if (mCloseDialogsReceiver != null) {
|
||||
mContext.unregisterReceiver(mCloseDialogsReceiver);
|
||||
mCloseDialogsReceiver = null;
|
||||
}
|
||||
if (mScreenshotReceiver != null) {
|
||||
mContext.unregisterReceiver(mScreenshotReceiver);
|
||||
mScreenshotReceiver = null;
|
||||
}
|
||||
if (mInputEventReceiver != null) {
|
||||
mInputEventReceiver.dispose();
|
||||
mInputEventReceiver = null;
|
||||
}
|
||||
if (mOnSessionCompleteListener != null) {
|
||||
mOnSessionCompleteListener.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
mView.setTranslationX(0);
|
||||
mView.setAlpha(1);
|
||||
mTimeoutHandler.cancelTimeout();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void attachWindow() {
|
||||
View decorView = mWindow.getDecorView();
|
||||
if (decorView.isAttachedToWindow() || mBlockAttach) {
|
||||
return;
|
||||
}
|
||||
mBlockAttach = true;
|
||||
mWindowManager.addView(decorView, mWindowLayoutParams);
|
||||
decorView.requestApplyInsets();
|
||||
mView.requestApplyInsets();
|
||||
decorView.getViewTreeObserver().addOnWindowAttachListener(
|
||||
new ViewTreeObserver.OnWindowAttachListener() {
|
||||
@Override
|
||||
public void onWindowAttached() {
|
||||
mBlockAttach = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowDetached() {
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void withWindowAttached(Runnable action) {
|
||||
View decorView = mWindow.getDecorView();
|
||||
if (decorView.isAttachedToWindow()) {
|
||||
action.run();
|
||||
} else {
|
||||
decorView.getViewTreeObserver().addOnWindowAttachListener(
|
||||
new ViewTreeObserver.OnWindowAttachListener() {
|
||||
@Override
|
||||
public void onWindowAttached() {
|
||||
mBlockAttach = false;
|
||||
decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
|
||||
action.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowDetached() {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateInsets(WindowInsets insets) {
|
||||
int orientation = mContext.getResources().getConfiguration().orientation;
|
||||
FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams();
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
|
||||
if (cutout == null) {
|
||||
p.setMargins(0, 0, 0, navBarInsets.bottom);
|
||||
} else {
|
||||
Insets waterfall = cutout.getWaterfallInsets();
|
||||
if (orientation == ORIENTATION_PORTRAIT) {
|
||||
p.setMargins(
|
||||
waterfall.left,
|
||||
Math.max(cutout.getSafeInsetTop(), waterfall.top),
|
||||
waterfall.right,
|
||||
Math.max(cutout.getSafeInsetBottom(),
|
||||
Math.max(navBarInsets.bottom, waterfall.bottom)));
|
||||
} else {
|
||||
p.setMargins(
|
||||
Math.max(cutout.getSafeInsetLeft(), waterfall.left),
|
||||
waterfall.top,
|
||||
Math.max(cutout.getSafeInsetRight(), waterfall.right),
|
||||
Math.max(navBarInsets.bottom, waterfall.bottom));
|
||||
}
|
||||
}
|
||||
mView.setLayoutParams(p);
|
||||
mView.requestLayout();
|
||||
}
|
||||
|
||||
private Display getDefaultDisplay() {
|
||||
return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.screenshot.SwipeDismissHandler;
|
||||
|
||||
/**
|
||||
* ConstraintLayout that is draggable when touched in a specific region
|
||||
*/
|
||||
public class DraggableConstraintLayout extends ConstraintLayout {
|
||||
private final SwipeDismissHandler mSwipeDismissHandler;
|
||||
private final GestureDetector mSwipeDetector;
|
||||
private Runnable mOnDismiss;
|
||||
private Runnable mOnInteraction;
|
||||
|
||||
public DraggableConstraintLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public DraggableConstraintLayout(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public DraggableConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
mSwipeDismissHandler = new SwipeDismissHandler(mContext, this,
|
||||
new SwipeDismissHandler.SwipeDismissCallbacks() {
|
||||
@Override
|
||||
public void onInteraction() {
|
||||
if (mOnInteraction != null) {
|
||||
mOnInteraction.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
if (mOnDismiss != null) {
|
||||
mOnDismiss.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
setOnTouchListener(mSwipeDismissHandler);
|
||||
|
||||
mSwipeDetector = new GestureDetector(mContext,
|
||||
new GestureDetector.SimpleOnGestureListener() {
|
||||
final Rect mActionsRect = new Rect();
|
||||
|
||||
@Override
|
||||
public boolean onScroll(
|
||||
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
|
||||
View actionsContainer = findViewById(R.id.actions_container);
|
||||
actionsContainer.getBoundsOnScreen(mActionsRect);
|
||||
// return true if we aren't in the actions bar, or if we are but it isn't
|
||||
// scrollable in the direction of movement
|
||||
return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY())
|
||||
|| !actionsContainer.canScrollHorizontally((int) distanceX);
|
||||
}
|
||||
});
|
||||
mSwipeDetector.setIsLongpressEnabled(false);
|
||||
}
|
||||
|
||||
@Override // View
|
||||
protected void onFinishInflate() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
mSwipeDismissHandler.onTouch(this, ev);
|
||||
}
|
||||
|
||||
return mSwipeDetector.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
public void setOnDismissCallback(Runnable callback) {
|
||||
mOnDismiss = callback;
|
||||
}
|
||||
|
||||
public void setOnInteractionCallback(Runnable callback) {
|
||||
mOnInteraction = callback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* Lightweight activity for editing text clipboard contents
|
||||
*/
|
||||
public class EditTextActivity extends Activity {
|
||||
private EditText mEditText;
|
||||
private ClipboardManager mClipboardManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.clipboard_edit_text_activity);
|
||||
findViewById(R.id.copy_button).setOnClickListener((v) -> saveToClipboard());
|
||||
findViewById(R.id.share).setOnClickListener((v) -> share());
|
||||
mEditText = findViewById(R.id.edit_text);
|
||||
mClipboardManager = requireNonNull(getSystemService(ClipboardManager.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
ClipData clip = mClipboardManager.getPrimaryClip();
|
||||
if (clip == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// TODO: put clip attribution in R.id.attribution TextView
|
||||
mEditText.setText(clip.getItemAt(0).getText());
|
||||
mEditText.requestFocus();
|
||||
}
|
||||
|
||||
private void saveToClipboard() {
|
||||
ClipData clip = ClipData.newPlainText("text", mEditText.getText());
|
||||
mClipboardManager.setPrimaryClip(clip);
|
||||
hideImeAndFinish();
|
||||
}
|
||||
|
||||
private void share() {
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, mEditText.getText());
|
||||
sendIntent.setType("text/plain");
|
||||
|
||||
Intent shareIntent = Intent.createChooser(sendIntent, null);
|
||||
startActivity(shareIntent);
|
||||
}
|
||||
|
||||
private void hideImeAndFinish() {
|
||||
InputMethodManager imm = getSystemService(InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import com.android.systemui.SliceBroadcastRelayHandler;
|
||||
import com.android.systemui.accessibility.SystemActions;
|
||||
import com.android.systemui.accessibility.WindowMagnification;
|
||||
import com.android.systemui.biometrics.AuthController;
|
||||
import com.android.systemui.clipboardoverlay.ClipboardListener;
|
||||
import com.android.systemui.communal.CommunalManagerUpdater;
|
||||
import com.android.systemui.dreams.DreamOverlayRegistrant;
|
||||
import com.android.systemui.dreams.appwidgets.ComplicationPrimer;
|
||||
@@ -72,6 +73,12 @@ public abstract class SystemUIBinder {
|
||||
@ClassKey(GarbageMonitor.Service.class)
|
||||
public abstract CoreStartable bindGarbageMonitorService(GarbageMonitor.Service sysui);
|
||||
|
||||
/** Inject into ClipboardListener. */
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ClipboardListener.class)
|
||||
public abstract CoreStartable bindClipboardListener(ClipboardListener sysui);
|
||||
|
||||
/** Inject into GlobalActionsComponent. */
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.screenshot;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.android.internal.policy.PhoneWindow;
|
||||
|
||||
/**
|
||||
* Utility methods for setting up a floating window
|
||||
*/
|
||||
public class FloatingWindowUtil {
|
||||
|
||||
/**
|
||||
* Convert input dp to pixels given DisplayMetrics
|
||||
*/
|
||||
public static float dpToPx(DisplayMetrics metrics, float dp) {
|
||||
return dp * metrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up window params for a floating window
|
||||
*/
|
||||
public static WindowManager.LayoutParams getFloatingWindowParams() {
|
||||
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||
MATCH_PARENT, MATCH_PARENT, /* xpos */ 0, /* ypos */ 0, TYPE_SCREENSHOT,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
params.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
params.setFitInsetsTypes(0);
|
||||
// This is needed to let touches pass through outside the touchable areas
|
||||
params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a transparent floating window
|
||||
*/
|
||||
public static PhoneWindow getFloatingWindow(Context context) {
|
||||
PhoneWindow window = new PhoneWindow(context);
|
||||
window.requestFeature(Window.FEATURE_NO_TITLE);
|
||||
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
|
||||
window.setBackgroundDrawableResource(android.R.color.transparent);
|
||||
return window;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -70,19 +70,28 @@ public class ScreenshotActionChip extends FrameLayout {
|
||||
super.setPressed(mIsPending || pressed);
|
||||
}
|
||||
|
||||
void setIcon(Icon icon, boolean tint) {
|
||||
/**
|
||||
* Set chip icon and whether to tint with theme color
|
||||
*/
|
||||
public void setIcon(Icon icon, boolean tint) {
|
||||
mIconView.setImageIcon(icon);
|
||||
if (!tint) {
|
||||
mIconView.setImageTintList(null);
|
||||
}
|
||||
}
|
||||
|
||||
void setText(CharSequence text) {
|
||||
/**
|
||||
* Set chip text
|
||||
*/
|
||||
public void setText(CharSequence text) {
|
||||
mTextView.setText(text);
|
||||
updatePadding(text.length() > 0);
|
||||
}
|
||||
|
||||
void setPendingIntent(PendingIntent intent, Runnable finisher) {
|
||||
/**
|
||||
* Set PendingIntent to be sent and Runnable to be run, when chip is clicked
|
||||
*/
|
||||
public void setPendingIntent(PendingIntent intent, Runnable finisher) {
|
||||
setOnClickListener(v -> {
|
||||
try {
|
||||
intent.send();
|
||||
@@ -93,7 +102,10 @@ public class ScreenshotActionChip extends FrameLayout {
|
||||
});
|
||||
}
|
||||
|
||||
void setIsPending(boolean isPending) {
|
||||
/**
|
||||
* Set pressed state of chip (to be used when chip is clicked before underlying intent is ready)
|
||||
*/
|
||||
public void setIsPending(boolean isPending) {
|
||||
mIsPending = isPending;
|
||||
setPressed(mIsPending);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package com.android.systemui.screenshot;
|
||||
|
||||
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
|
||||
|
||||
import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
|
||||
@@ -41,23 +40,21 @@ import android.app.ExitTransitionCoordinator;
|
||||
import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks;
|
||||
import android.app.ICompatCameraControlCallback;
|
||||
import android.app.Notification;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.media.MediaActionSound;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -76,7 +73,6 @@ import android.view.SurfaceControl;
|
||||
import android.view.View;
|
||||
import android.view.ViewRootImpl;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManagerGlobal;
|
||||
@@ -90,6 +86,7 @@ import com.android.internal.logging.UiEventLogger;
|
||||
import com.android.internal.policy.PhoneWindow;
|
||||
import com.android.settingslib.applications.InterestingConfigChanges;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
|
||||
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
|
||||
@@ -235,13 +232,11 @@ public class ScreenshotController {
|
||||
static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
|
||||
static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
|
||||
|
||||
|
||||
private static final int MESSAGE_CORNER_TIMEOUT = 2;
|
||||
private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
|
||||
|
||||
// From WizardManagerHelper.java
|
||||
private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
|
||||
|
||||
private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
|
||||
|
||||
private final WindowContext mContext;
|
||||
private final ScreenshotNotificationsController mNotificationsController;
|
||||
private final ScreenshotSmartActions mScreenshotSmartActions;
|
||||
@@ -260,6 +255,7 @@ public class ScreenshotController {
|
||||
private final ScrollCaptureController mScrollCaptureController;
|
||||
private final LongScreenshotData mLongScreenshotHolder;
|
||||
private final boolean mIsLowRamDevice;
|
||||
private final TimeoutHandler mScreenshotHandler;
|
||||
|
||||
private ScreenshotView mScreenshotView;
|
||||
private Bitmap mScreenBitmap;
|
||||
@@ -270,24 +266,8 @@ public class ScreenshotController {
|
||||
private Animator mScreenshotAnimation;
|
||||
private RequestCallback mCurrentRequestCallback;
|
||||
private String mPackageName = "";
|
||||
private BroadcastReceiver mCopyBroadcastReceiver;
|
||||
|
||||
private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MESSAGE_CORNER_TIMEOUT:
|
||||
if (DEBUG_UI) {
|
||||
Log.d(TAG, "Corner timeout hit");
|
||||
}
|
||||
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT, 0,
|
||||
mPackageName);
|
||||
ScreenshotController.this.dismissScreenshot(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Tracks config changes that require re-creating UI */
|
||||
private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
|
||||
@@ -309,7 +289,8 @@ public class ScreenshotController {
|
||||
@Main Executor mainExecutor,
|
||||
ScrollCaptureController scrollCaptureController,
|
||||
LongScreenshotData longScreenshotHolder,
|
||||
ActivityManager activityManager) {
|
||||
ActivityManager activityManager,
|
||||
TimeoutHandler timeoutHandler) {
|
||||
mScreenshotSmartActions = screenshotSmartActions;
|
||||
mNotificationsController = screenshotNotificationsController;
|
||||
mScrollCaptureClient = scrollCaptureClient;
|
||||
@@ -321,6 +302,17 @@ public class ScreenshotController {
|
||||
mIsLowRamDevice = activityManager.isLowRamDevice();
|
||||
mBgExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
mScreenshotHandler = timeoutHandler;
|
||||
mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
|
||||
mScreenshotHandler.setOnTimeoutRunnable(() -> {
|
||||
if (DEBUG_UI) {
|
||||
Log.d(TAG, "Corner timeout hit");
|
||||
}
|
||||
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT, 0,
|
||||
mPackageName);
|
||||
ScreenshotController.this.dismissScreenshot(false);
|
||||
});
|
||||
|
||||
mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
|
||||
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
|
||||
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
|
||||
@@ -329,27 +321,11 @@ public class ScreenshotController {
|
||||
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
|
||||
|
||||
// Setup the window that we are going to use
|
||||
mWindowLayoutParams = new WindowManager.LayoutParams(
|
||||
MATCH_PARENT, MATCH_PARENT, /* xpos */ 0, /* ypos */ 0, TYPE_SCREENSHOT,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
|
||||
mWindowLayoutParams.setTitle("ScreenshotAnimation");
|
||||
mWindowLayoutParams.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
mWindowLayoutParams.setFitInsetsTypes(0);
|
||||
// This is needed to let touches pass through outside the touchable areas
|
||||
mWindowLayoutParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
|
||||
|
||||
mWindow = new PhoneWindow(mContext);
|
||||
mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
|
||||
mWindow.setWindowManager(mWindowManager, null, null);
|
||||
mWindow.requestFeature(Window.FEATURE_NO_TITLE);
|
||||
mWindow.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
|
||||
mWindow.setBackgroundDrawableResource(android.R.color.transparent);
|
||||
|
||||
mConfigChanges.applyNewConfig(context.getResources());
|
||||
reloadAssets();
|
||||
@@ -357,6 +333,18 @@ public class ScreenshotController {
|
||||
// Setup the Camera shutter sound
|
||||
mCameraSound = new MediaActionSound();
|
||||
mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
|
||||
|
||||
mCopyBroadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) {
|
||||
dismissScreenshot(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
mContext.registerReceiver(mCopyBroadcastReceiver, new IntentFilter(
|
||||
ClipboardOverlayController.COPY_OVERLAY_ACTION),
|
||||
ClipboardOverlayController.SELF_PERMISSION, null);
|
||||
}
|
||||
|
||||
void takeScreenshotFullscreen(ComponentName topComponent, Consumer<Uri> finisher,
|
||||
@@ -424,7 +412,7 @@ public class ScreenshotController {
|
||||
}
|
||||
return;
|
||||
}
|
||||
cancelTimeout();
|
||||
mScreenshotHandler.cancelTimeout();
|
||||
if (immediate) {
|
||||
finishDismiss();
|
||||
} else {
|
||||
@@ -440,6 +428,7 @@ public class ScreenshotController {
|
||||
* Release the constructed window context.
|
||||
*/
|
||||
void releaseContext() {
|
||||
mContext.unregisterReceiver(mCopyBroadcastReceiver);
|
||||
mContext.release();
|
||||
mCameraSound.release();
|
||||
mBgExecutor.shutdownNow();
|
||||
@@ -462,7 +451,7 @@ public class ScreenshotController {
|
||||
if (DEBUG_INPUT) {
|
||||
Log.d(TAG, "onUserInteraction");
|
||||
}
|
||||
resetTimeout();
|
||||
mScreenshotHandler.resetTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -517,6 +506,9 @@ public class ScreenshotController {
|
||||
}
|
||||
|
||||
saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
|
||||
|
||||
mContext.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
|
||||
ClipboardOverlayController.SELF_PERMISSION);
|
||||
}
|
||||
|
||||
private Bitmap captureScreenshot(Rect crop) {
|
||||
@@ -651,7 +643,7 @@ public class ScreenshotController {
|
||||
// ignore system bar insets for the purpose of window layout
|
||||
mWindow.getDecorView().setOnApplyWindowInsetsListener(
|
||||
(v, insets) -> WindowInsets.CONSUMED);
|
||||
cancelTimeout(); // restarted after animation
|
||||
mScreenshotHandler.cancelTimeout(); // restarted after animation
|
||||
}
|
||||
|
||||
private void requestScrollCapture() {
|
||||
@@ -878,7 +870,7 @@ public class ScreenshotController {
|
||||
}
|
||||
mScreenshotView.reset();
|
||||
removeWindow();
|
||||
cancelTimeout();
|
||||
mScreenshotHandler.cancelTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -905,30 +897,6 @@ public class ScreenshotController {
|
||||
mSaveInBgTask.execute();
|
||||
}
|
||||
|
||||
private void cancelTimeout() {
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(TAG, "cancel timeout");
|
||||
}
|
||||
mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
|
||||
}
|
||||
|
||||
private void resetTimeout() {
|
||||
cancelTimeout();
|
||||
|
||||
AccessibilityManager accessibilityManager = (AccessibilityManager)
|
||||
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
|
||||
SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS,
|
||||
AccessibilityManager.FLAG_CONTENT_CONTROLS);
|
||||
|
||||
mScreenshotHandler.sendMessageDelayed(
|
||||
mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
|
||||
timeoutMs);
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(TAG, "dismiss timeout: " + timeoutMs + " ms");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the action shade and its entrance animation, once we get the screenshot URI.
|
||||
@@ -939,7 +907,7 @@ public class ScreenshotController {
|
||||
Log.d(TAG, "Showing UI actions");
|
||||
}
|
||||
|
||||
resetTimeout();
|
||||
mScreenshotHandler.resetTimeout();
|
||||
|
||||
if (imageData.uri != null) {
|
||||
mScreenshotHandler.post(() -> {
|
||||
|
||||
@@ -222,7 +222,6 @@ public class ScreenshotView extends FrameLayout implements
|
||||
}
|
||||
});
|
||||
mSwipeDetector.setIsLongpressEnabled(false);
|
||||
mSwipeDismissHandler = new SwipeDismissHandler();
|
||||
addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
@@ -244,7 +243,7 @@ public class ScreenshotView extends FrameLayout implements
|
||||
* Called to display the scroll action chip when support is detected.
|
||||
*
|
||||
* @param packageName the owning package of the window to be captured
|
||||
* @param onClick the action to take when the chip is clicked.
|
||||
* @param onClick the action to take when the chip is clicked.
|
||||
*/
|
||||
public void showScrollChip(String packageName, Runnable onClick) {
|
||||
if (DEBUG_SCROLL) {
|
||||
@@ -273,10 +272,12 @@ public class ScreenshotView extends FrameLayout implements
|
||||
|
||||
final Rect tmpRect = new Rect();
|
||||
mScreenshotPreview.getBoundsOnScreen(tmpRect);
|
||||
tmpRect.inset((int) dpToPx(-SWIPE_PADDING_DP), (int) dpToPx(-SWIPE_PADDING_DP));
|
||||
tmpRect.inset((int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
|
||||
(int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
|
||||
touchRegion.op(tmpRect, Region.Op.UNION);
|
||||
mActionsContainerBackground.getBoundsOnScreen(tmpRect);
|
||||
tmpRect.inset((int) dpToPx(-SWIPE_PADDING_DP), (int) dpToPx(-SWIPE_PADDING_DP));
|
||||
tmpRect.inset((int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
|
||||
(int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
|
||||
touchRegion.op(tmpRect, Region.Op.UNION);
|
||||
mDismissButton.getBoundsOnScreen(tmpRect);
|
||||
touchRegion.op(tmpRect, Region.Op.UNION);
|
||||
@@ -365,7 +366,7 @@ public class ScreenshotView extends FrameLayout implements
|
||||
mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip));
|
||||
mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip));
|
||||
|
||||
int swipePaddingPx = (int) dpToPx(SWIPE_PADDING_DP);
|
||||
int swipePaddingPx = (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, SWIPE_PADDING_DP);
|
||||
TouchDelegate previewDelegate = new TouchDelegate(
|
||||
new Rect(swipePaddingPx, swipePaddingPx, swipePaddingPx, swipePaddingPx),
|
||||
mScreenshotPreview);
|
||||
@@ -390,6 +391,24 @@ public class ScreenshotView extends FrameLayout implements
|
||||
// Get focus so that the key events go to the layout.
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
|
||||
mSwipeDismissHandler = new SwipeDismissHandler(mContext, mScreenshotStatic,
|
||||
new SwipeDismissHandler.SwipeDismissCallbacks() {
|
||||
@Override
|
||||
public void onInteraction() {
|
||||
mCallbacks.onUserInteraction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(ScreenshotView.TAG, "dismiss triggered via swipe gesture");
|
||||
}
|
||||
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0,
|
||||
mPackageName);
|
||||
mCallbacks.onDismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
View getScreenshotPreview() {
|
||||
@@ -859,8 +878,8 @@ public class ScreenshotView extends FrameLayout implements
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
mCallbacks.onDismiss();
|
||||
}
|
||||
mCallbacks.onDismiss();
|
||||
}
|
||||
});
|
||||
animSet.start();
|
||||
}
|
||||
@@ -934,38 +953,7 @@ public class ScreenshotView extends FrameLayout implements
|
||||
}
|
||||
|
||||
void animateDismissal() {
|
||||
animateDismissal(createScreenshotTranslateDismissAnimation());
|
||||
}
|
||||
|
||||
private void animateDismissal(Animator dismissAnimation) {
|
||||
mDismissAnimation = dismissAnimation;
|
||||
mDismissAnimation.addListener(new AnimatorListenerAdapter() {
|
||||
private boolean mCancelled = false;
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
super.onAnimationCancel(animation);
|
||||
if (DEBUG_ANIM) {
|
||||
Log.d(TAG, "Cancelled dismiss animation");
|
||||
}
|
||||
mCancelled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
if (!mCancelled) {
|
||||
if (DEBUG_ANIM) {
|
||||
Log.d(TAG, "after dismiss animation, calling onDismissRunnable.run()");
|
||||
}
|
||||
mCallbacks.onDismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (DEBUG_ANIM) {
|
||||
Log.d(TAG, "Starting dismiss animation");
|
||||
}
|
||||
mDismissAnimation.start();
|
||||
mSwipeDismissHandler.dismiss();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
@@ -979,6 +967,7 @@ public class ScreenshotView extends FrameLayout implements
|
||||
}
|
||||
mDismissAnimation.cancel();
|
||||
}
|
||||
mSwipeDismissHandler.cancel();
|
||||
if (DEBUG_WINDOW) {
|
||||
Log.d(TAG, "removing OnComputeInternalInsetsListener");
|
||||
}
|
||||
@@ -1042,8 +1031,8 @@ public class ScreenshotView extends FrameLayout implements
|
||||
xAnim.setInterpolator(mAccelerateInterpolator);
|
||||
xAnim.setDuration(SCREENSHOT_DISMISS_X_DURATION_MS);
|
||||
float deltaX = mDirectionLTR
|
||||
? -1 * (mScreenshotPreviewBorder.getX() + mScreenshotPreviewBorder.getWidth())
|
||||
: (mDisplayMetrics.widthPixels - mScreenshotPreviewBorder.getX());
|
||||
? -1 * (mScreenshotPreviewBorder.getX() + mScreenshotPreviewBorder.getWidth())
|
||||
: (mDisplayMetrics.widthPixels - mScreenshotPreviewBorder.getX());
|
||||
xAnim.addUpdateListener(animation -> {
|
||||
float currXDelta = MathUtils.lerp(0, deltaX, animation.getAnimatedFraction());
|
||||
mScreenshotStatic.setTranslationX(currXDelta);
|
||||
@@ -1100,130 +1089,4 @@ public class ScreenshotView extends FrameLayout implements
|
||||
return insetDrawable;
|
||||
}
|
||||
}
|
||||
|
||||
private float dpToPx(float dp) {
|
||||
return dp * mDisplayMetrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;
|
||||
}
|
||||
|
||||
class SwipeDismissHandler implements OnTouchListener {
|
||||
// distance needed to register a dismissal
|
||||
private static final float DISMISS_DISTANCE_THRESHOLD_DP = 20;
|
||||
|
||||
private final GestureDetector mGestureDetector;
|
||||
|
||||
private float mStartX;
|
||||
// Keeps track of the most recent direction (between the last two move events).
|
||||
// -1 for left; +1 for right.
|
||||
private int mDirectionX;
|
||||
private float mPreviousX;
|
||||
|
||||
SwipeDismissHandler() {
|
||||
GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
|
||||
mGestureDetector = new GestureDetector(mContext, gestureListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent event) {
|
||||
boolean gestureResult = mGestureDetector.onTouchEvent(event);
|
||||
mCallbacks.onUserInteraction();
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
mStartX = event.getRawX();
|
||||
mPreviousX = mStartX;
|
||||
return true;
|
||||
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
|
||||
if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
if (isPastDismissThreshold()) {
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(TAG, "dismiss triggered via swipe gesture");
|
||||
}
|
||||
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0, mPackageName);
|
||||
animateDismissal(createSwipeDismissAnimation());
|
||||
} else {
|
||||
// if we've moved, but not past the threshold, start the return animation
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(TAG, "swipe gesture abandoned");
|
||||
}
|
||||
createSwipeReturnAnimation().start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return gestureResult;
|
||||
}
|
||||
|
||||
class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
@Override
|
||||
public boolean onScroll(
|
||||
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
|
||||
mScreenshotStatic.setTranslationX(ev2.getRawX() - mStartX);
|
||||
mDirectionX = (ev2.getRawX() < mPreviousX) ? -1 : 1;
|
||||
mPreviousX = ev2.getRawX();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
|
||||
float velocityY) {
|
||||
if (mScreenshotStatic.getTranslationX() * velocityX > 0
|
||||
&& (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
|
||||
animateDismissal(createSwipeDismissAnimation(velocityX / (float) 1000));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPastDismissThreshold() {
|
||||
float translationX = mScreenshotStatic.getTranslationX();
|
||||
// Determines whether the absolute translation from the start is in the same direction
|
||||
// as the current movement. For example, if the user moves most of the way to the right,
|
||||
// but then starts dragging back left, we do not dismiss even though the absolute
|
||||
// distance is greater than the threshold.
|
||||
if (translationX * mDirectionX > 0) {
|
||||
return Math.abs(translationX) >= dpToPx(DISMISS_DISTANCE_THRESHOLD_DP);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ValueAnimator createSwipeDismissAnimation() {
|
||||
return createSwipeDismissAnimation(1);
|
||||
}
|
||||
|
||||
private ValueAnimator createSwipeDismissAnimation(float velocity) {
|
||||
// velocity is measured in pixels per millisecond
|
||||
velocity = Math.min(3, Math.max(1, velocity));
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
||||
float startX = mScreenshotStatic.getTranslationX();
|
||||
// make sure the UI gets all the way off the screen in the direction of movement
|
||||
// (the actions container background is guaranteed to be both the leftmost and
|
||||
// rightmost UI element in LTR and RTL)
|
||||
float finalX = startX < 0
|
||||
? -1 * mActionsContainerBackground.getRight()
|
||||
: mDisplayMetrics.widthPixels;
|
||||
float distance = Math.abs(finalX - startX);
|
||||
|
||||
anim.addUpdateListener(animation -> {
|
||||
float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction());
|
||||
mScreenshotStatic.setTranslationX(translation);
|
||||
setAlpha(1 - animation.getAnimatedFraction());
|
||||
});
|
||||
anim.setDuration((long) (distance / Math.abs(velocity)));
|
||||
return anim;
|
||||
}
|
||||
|
||||
private ValueAnimator createSwipeReturnAnimation() {
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
||||
float startX = mScreenshotStatic.getTranslationX();
|
||||
float finalX = 0;
|
||||
|
||||
anim.addUpdateListener(animation -> {
|
||||
float translation = MathUtils.lerp(
|
||||
startX, finalX, animation.getAnimatedFraction());
|
||||
mScreenshotStatic.setTranslationX(translation);
|
||||
});
|
||||
|
||||
return anim;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.screenshot;
|
||||
|
||||
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.MathUtils;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Allows a view to be swipe-dismissed, or returned to its location if distance threshold is not met
|
||||
*/
|
||||
public class SwipeDismissHandler implements View.OnTouchListener {
|
||||
private static final String TAG = "SwipeDismissHandler";
|
||||
|
||||
// distance needed to register a dismissal
|
||||
private static final float DISMISS_DISTANCE_THRESHOLD_DP = 20;
|
||||
|
||||
/**
|
||||
* Stores the callbacks when the view is interacted with or dismissed.
|
||||
*/
|
||||
public interface SwipeDismissCallbacks {
|
||||
/**
|
||||
* Run when the view is interacted with (touched)
|
||||
*/
|
||||
void onInteraction();
|
||||
|
||||
/**
|
||||
* Run when the view is dismissed (the distance threshold is met), post-dismissal animation
|
||||
*/
|
||||
void onDismiss();
|
||||
}
|
||||
|
||||
private final View mView;
|
||||
private final SwipeDismissCallbacks mCallbacks;
|
||||
private final GestureDetector mGestureDetector;
|
||||
private DisplayMetrics mDisplayMetrics;
|
||||
private ValueAnimator mDismissAnimation;
|
||||
|
||||
|
||||
private float mStartX;
|
||||
// Keeps track of the most recent direction (between the last two move events).
|
||||
// -1 for left; +1 for right.
|
||||
private int mDirectionX;
|
||||
private float mPreviousX;
|
||||
|
||||
public SwipeDismissHandler(Context context, View view, SwipeDismissCallbacks callbacks) {
|
||||
mView = view;
|
||||
mCallbacks = callbacks;
|
||||
GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
|
||||
mGestureDetector = new GestureDetector(context, gestureListener);
|
||||
mDisplayMetrics = new DisplayMetrics();
|
||||
context.getDisplay().getRealMetrics(mDisplayMetrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent event) {
|
||||
boolean gestureResult = mGestureDetector.onTouchEvent(event);
|
||||
mCallbacks.onInteraction();
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
mStartX = event.getRawX();
|
||||
mPreviousX = mStartX;
|
||||
return true;
|
||||
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
|
||||
if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
if (isPastDismissThreshold()) {
|
||||
dismiss();
|
||||
} else {
|
||||
// if we've moved, but not past the threshold, start the return animation
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(TAG, "swipe gesture abandoned");
|
||||
}
|
||||
createSwipeReturnAnimation().start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return gestureResult;
|
||||
}
|
||||
|
||||
class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
@Override
|
||||
public boolean onScroll(
|
||||
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
|
||||
mView.setTranslationX(ev2.getRawX() - mStartX);
|
||||
mDirectionX = (ev2.getRawX() < mPreviousX) ? -1 : 1;
|
||||
mPreviousX = ev2.getRawX();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
|
||||
float velocityY) {
|
||||
if (mView.getTranslationX() * velocityX > 0
|
||||
&& (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
|
||||
dismiss(velocityX / (float) 1000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPastDismissThreshold() {
|
||||
float translationX = mView.getTranslationX();
|
||||
// Determines whether the absolute translation from the start is in the same direction
|
||||
// as the current movement. For example, if the user moves most of the way to the right,
|
||||
// but then starts dragging back left, we do not dismiss even though the absolute
|
||||
// distance is greater than the threshold.
|
||||
if (translationX * mDirectionX > 0) {
|
||||
return Math.abs(translationX) >= FloatingWindowUtil.dpToPx(mDisplayMetrics,
|
||||
DISMISS_DISTANCE_THRESHOLD_DP);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the currently-running dismissal animation, if any.
|
||||
*/
|
||||
public void cancel() {
|
||||
if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
|
||||
mDismissAnimation.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start dismissal animation (will run onDismiss callback when animation complete)
|
||||
*/
|
||||
public void dismiss() {
|
||||
dismiss(1);
|
||||
}
|
||||
|
||||
private void dismiss(float velocity) {
|
||||
mDismissAnimation = createSwipeDismissAnimation(velocity);
|
||||
mDismissAnimation.addListener(new AnimatorListenerAdapter() {
|
||||
private boolean mCancelled;
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
super.onAnimationCancel(animation);
|
||||
mCancelled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
if (!mCancelled) {
|
||||
mCallbacks.onDismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
mDismissAnimation.start();
|
||||
}
|
||||
|
||||
private ValueAnimator createSwipeDismissAnimation(float velocity) {
|
||||
// velocity is measured in pixels per millisecond
|
||||
velocity = Math.min(3, Math.max(1, velocity));
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
||||
float startX = mView.getTranslationX();
|
||||
// make sure the UI gets all the way off the screen in the direction of movement
|
||||
// (the actions container background is guaranteed to be both the leftmost and
|
||||
// rightmost UI element in LTR and RTL)
|
||||
float finalX = startX <= 0 ? -1 * mView.getRight() : mDisplayMetrics.widthPixels;
|
||||
float distance = Math.abs(finalX - startX);
|
||||
|
||||
anim.addUpdateListener(animation -> {
|
||||
float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction());
|
||||
mView.setTranslationX(translation);
|
||||
mView.setAlpha(1 - animation.getAnimatedFraction());
|
||||
});
|
||||
anim.setDuration((long) (distance / Math.abs(velocity)));
|
||||
return anim;
|
||||
}
|
||||
|
||||
private ValueAnimator createSwipeReturnAnimation() {
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
||||
float startX = mView.getTranslationX();
|
||||
float finalX = 0;
|
||||
|
||||
anim.addUpdateListener(animation -> {
|
||||
float translation = MathUtils.lerp(
|
||||
startX, finalX, animation.getAnimatedFraction());
|
||||
mView.setTranslationX(translation);
|
||||
});
|
||||
|
||||
return anim;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public class TakeScreenshotService extends Service {
|
||||
|
||||
/** Informs about coarse grained state of the Controller. */
|
||||
interface RequestCallback {
|
||||
/** Respond to the current request indicating the screenshot request failed.*/
|
||||
/** Respond to the current request indicating the screenshot request failed. */
|
||||
void reportError();
|
||||
|
||||
/** The controller has completed handling this request UI has been removed */
|
||||
@@ -230,7 +230,7 @@ public class TakeScreenshotService extends Service {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
private static void sendComplete(Messenger target) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.screenshot;
|
||||
|
||||
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Starts a configurable runnable on timeout. Can be cancelled. Used for automatically dismissing
|
||||
* floating overlays.
|
||||
*/
|
||||
public class TimeoutHandler extends Handler {
|
||||
private static final String TAG = "TimeoutHandler";
|
||||
|
||||
private static final int MESSAGE_CORNER_TIMEOUT = 2;
|
||||
private static final int DEFAULT_TIMEOUT_MILLIS = 6000;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private Runnable mOnTimeout;
|
||||
private int mDefaultTimeout = DEFAULT_TIMEOUT_MILLIS;
|
||||
|
||||
@Inject
|
||||
public TimeoutHandler(Context context) {
|
||||
super(Looper.getMainLooper());
|
||||
mContext = context;
|
||||
mOnTimeout = () -> {
|
||||
};
|
||||
}
|
||||
|
||||
public void setOnTimeoutRunnable(Runnable onTimeout) {
|
||||
mOnTimeout = onTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MESSAGE_CORNER_TIMEOUT:
|
||||
mOnTimeout.run();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default timeout (if not overridden by accessibility)
|
||||
*/
|
||||
public void setDefaultTimeoutMillis(int timeout) {
|
||||
mDefaultTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current timeout, if any. To reset the delayed runnable use resetTimeout instead.
|
||||
*/
|
||||
public void cancelTimeout() {
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(TAG, "cancel timeout");
|
||||
}
|
||||
removeMessages(MESSAGE_CORNER_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the timeout.
|
||||
*/
|
||||
public void resetTimeout() {
|
||||
cancelTimeout();
|
||||
|
||||
AccessibilityManager accessibilityManager = (AccessibilityManager)
|
||||
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
|
||||
mDefaultTimeout,
|
||||
AccessibilityManager.FLAG_CONTENT_CONTROLS);
|
||||
|
||||
sendMessageDelayed(obtainMessage(MESSAGE_CORNER_TIMEOUT), timeoutMs);
|
||||
if (DEBUG_DISMISS) {
|
||||
Log.d(TAG, "dismiss timeout: " + timeoutMs + " ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user