• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.clipboardoverlay;
18 
19 import static android.content.ClipDescription.CLASSIFICATION_COMPLETE;
20 
21 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
22 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
23 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
24 import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
25 
26 import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
27 
28 import android.content.ClipData;
29 import android.content.ClipboardManager;
30 import android.content.Context;
31 import android.os.SystemProperties;
32 import android.provider.Settings;
33 import android.util.Log;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.logging.UiEventLogger;
37 import com.android.systemui.CoreStartable;
38 import com.android.systemui.dagger.SysUISingleton;
39 import com.android.systemui.flags.FeatureFlags;
40 
41 import javax.inject.Inject;
42 import javax.inject.Provider;
43 
44 /**
45  * ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
46  */
47 @SysUISingleton
48 public class ClipboardListener implements
49         CoreStartable, ClipboardManager.OnPrimaryClipChangedListener {
50     private static final String TAG = "ClipboardListener";
51 
52     @VisibleForTesting
53     static final String SHELL_PACKAGE = "com.android.shell";
54     @VisibleForTesting
55     static final String EXTRA_SUPPRESS_OVERLAY =
56             "com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY";
57 
58     private final Context mContext;
59     private final Provider<ClipboardOverlayController> mOverlayProvider;
60     private final ClipboardToast mClipboardToast;
61     private final ClipboardManager mClipboardManager;
62     private final FeatureFlags mFeatureFlags;
63     private final UiEventLogger mUiEventLogger;
64     private ClipboardOverlay mClipboardOverlay;
65 
66     @Inject
ClipboardListener(Context context, Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, ClipboardToast clipboardToast, ClipboardManager clipboardManager, FeatureFlags featureFlags, UiEventLogger uiEventLogger)67     public ClipboardListener(Context context,
68             Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
69             ClipboardToast clipboardToast,
70             ClipboardManager clipboardManager,
71             FeatureFlags featureFlags,
72             UiEventLogger uiEventLogger) {
73         mContext = context;
74         mOverlayProvider = clipboardOverlayControllerProvider;
75         mClipboardToast = clipboardToast;
76         mClipboardManager = clipboardManager;
77         mFeatureFlags = featureFlags;
78         mUiEventLogger = uiEventLogger;
79     }
80 
81     @Override
start()82     public void start() {
83         mClipboardManager.addPrimaryClipChangedListener(this);
84     }
85 
86     @Override
onPrimaryClipChanged()87     public void onPrimaryClipChanged() {
88         if (!mClipboardManager.hasPrimaryClip()) {
89             return;
90         }
91 
92         String clipSource = mClipboardManager.getPrimaryClipSource();
93         ClipData clipData = mClipboardManager.getPrimaryClip();
94 
95         if (shouldSuppressOverlay(clipData, clipSource, isEmulator())) {
96             Log.i(TAG, "Clipboard overlay suppressed.");
97             return;
98         }
99 
100         if (!isUserSetupComplete() // user should not access intents from this state
101                 || clipData == null // shouldn't happen, but just in case
102                 || clipData.getItemCount() == 0) {
103             if (shouldShowToast(clipData)) {
104                 mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource);
105                 mClipboardToast.showCopiedToast();
106             }
107             return;
108         }
109 
110         if (mClipboardOverlay == null) {
111             mClipboardOverlay = mOverlayProvider.get();
112             mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
113         } else {
114             mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
115         }
116         if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
117             mClipboardOverlay.setClipData(clipData, clipSource);
118         } else {
119             mClipboardOverlay.setClipDataLegacy(clipData, clipSource);
120         }
121         mClipboardOverlay.setOnSessionCompleteListener(() -> {
122             // Session is complete, free memory until it's needed again.
123             mClipboardOverlay = null;
124         });
125     }
126 
127     // The overlay is suppressed if EXTRA_SUPPRESS_OVERLAY is true and the device is an emulator or
128     // the source package is SHELL_PACKAGE. This is meant to suppress the overlay when the emulator
129     // or a mirrored device is syncing the clipboard.
130     @VisibleForTesting
shouldSuppressOverlay(ClipData clipData, String clipSource, boolean isEmulator)131     static boolean shouldSuppressOverlay(ClipData clipData, String clipSource,
132             boolean isEmulator) {
133         if (!(isEmulator || SHELL_PACKAGE.equals(clipSource))) {
134             return false;
135         }
136         if (clipData == null || clipData.getDescription().getExtras() == null) {
137             return false;
138         }
139         return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false);
140     }
141 
shouldShowToast(ClipData clipData)142     boolean shouldShowToast(ClipData clipData) {
143         if (clipData == null) {
144             return false;
145         } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) {
146             // only show for classification complete if we aren't already showing a toast, to ignore
147             // the duplicate ClipData with classification
148             return !mClipboardToast.isShowing();
149         }
150         return true;
151     }
152 
isEmulator()153     private static boolean isEmulator() {
154         return SystemProperties.getBoolean("ro.boot.qemu", false);
155     }
156 
isUserSetupComplete()157     private boolean isUserSetupComplete() {
158         return Settings.Secure.getInt(mContext.getContentResolver(),
159                 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
160     }
161 
162     interface ClipboardOverlay {
setClipDataLegacy(ClipData clipData, String clipSource)163         void setClipDataLegacy(ClipData clipData, String clipSource);
164 
setClipData(ClipData clipData, String clipSource)165         void setClipData(ClipData clipData, String clipSource);
166 
setOnSessionCompleteListener(Runnable runnable)167         void setOnSessionCompleteListener(Runnable runnable);
168     }
169 }
170