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 
25 import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
26 
27 import android.content.ClipData;
28 import android.content.ClipboardManager;
29 import android.content.Context;
30 import android.os.SystemProperties;
31 import android.provider.Settings;
32 import android.util.Log;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.logging.UiEventLogger;
36 import com.android.systemui.CoreStartable;
37 import com.android.systemui.dagger.SysUISingleton;
38 
39 import javax.inject.Inject;
40 import javax.inject.Provider;
41 
42 /**
43  * ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
44  */
45 @SysUISingleton
46 public class ClipboardListener implements
47         CoreStartable, ClipboardManager.OnPrimaryClipChangedListener {
48     private static final String TAG = "ClipboardListener";
49 
50     @VisibleForTesting
51     static final String SHELL_PACKAGE = "com.android.shell";
52     @VisibleForTesting
53     static final String EXTRA_SUPPRESS_OVERLAY =
54             "com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY";
55 
56     private final Context mContext;
57     private final Provider<ClipboardOverlayController> mOverlayProvider;
58     private final ClipboardToast mClipboardToast;
59     private final ClipboardManager mClipboardManager;
60     private final UiEventLogger mUiEventLogger;
61     private ClipboardOverlay mClipboardOverlay;
62 
63     @Inject
ClipboardListener(Context context, Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, ClipboardToast clipboardToast, ClipboardManager clipboardManager, UiEventLogger uiEventLogger)64     public ClipboardListener(Context context,
65             Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
66             ClipboardToast clipboardToast,
67             ClipboardManager clipboardManager,
68             UiEventLogger uiEventLogger) {
69         mContext = context;
70         mOverlayProvider = clipboardOverlayControllerProvider;
71         mClipboardToast = clipboardToast;
72         mClipboardManager = clipboardManager;
73         mUiEventLogger = uiEventLogger;
74     }
75 
76     @Override
start()77     public void start() {
78         mClipboardManager.addPrimaryClipChangedListener(this);
79     }
80 
81     @Override
onPrimaryClipChanged()82     public void onPrimaryClipChanged() {
83         if (!mClipboardManager.hasPrimaryClip()) {
84             return;
85         }
86 
87         String clipSource = mClipboardManager.getPrimaryClipSource();
88         ClipData clipData = mClipboardManager.getPrimaryClip();
89 
90         if (shouldSuppressOverlay(clipData, clipSource, isEmulator())) {
91             Log.i(TAG, "Clipboard overlay suppressed.");
92             return;
93         }
94 
95         if (!isUserSetupComplete() // user should not access intents from this state
96                 || clipData == null // shouldn't happen, but just in case
97                 || clipData.getItemCount() == 0) {
98             if (shouldShowToast(clipData)) {
99                 mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource);
100                 mClipboardToast.showCopiedToast();
101             }
102             return;
103         }
104 
105         if (mClipboardOverlay == null) {
106             mClipboardOverlay = mOverlayProvider.get();
107             mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
108         } else {
109             mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
110         }
111         mClipboardOverlay.setClipData(clipData, clipSource);
112         mClipboardOverlay.setOnSessionCompleteListener(() -> {
113             // Session is complete, free memory until it's needed again.
114             mClipboardOverlay = null;
115         });
116     }
117 
118     // The overlay is suppressed if EXTRA_SUPPRESS_OVERLAY is true and the device is an emulator or
119     // the source package is SHELL_PACKAGE. This is meant to suppress the overlay when the emulator
120     // or a mirrored device is syncing the clipboard.
121     @VisibleForTesting
shouldSuppressOverlay(ClipData clipData, String clipSource, boolean isEmulator)122     static boolean shouldSuppressOverlay(ClipData clipData, String clipSource,
123             boolean isEmulator) {
124         if (!(isEmulator || SHELL_PACKAGE.equals(clipSource))) {
125             return false;
126         }
127         if (clipData == null || clipData.getDescription().getExtras() == null) {
128             return false;
129         }
130         return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false);
131     }
132 
shouldShowToast(ClipData clipData)133     boolean shouldShowToast(ClipData clipData) {
134         if (clipData == null) {
135             return false;
136         } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) {
137             // only show for classification complete if we aren't already showing a toast, to ignore
138             // the duplicate ClipData with classification
139             return !mClipboardToast.isShowing();
140         }
141         return true;
142     }
143 
isEmulator()144     private static boolean isEmulator() {
145         return SystemProperties.getBoolean("ro.boot.qemu", false);
146     }
147 
isUserSetupComplete()148     private boolean isUserSetupComplete() {
149         return Settings.Secure.getInt(mContext.getContentResolver(),
150                 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
151     }
152 
153     interface ClipboardOverlay {
setClipData(ClipData clipData, String clipSource)154         void setClipData(ClipData clipData, String clipSource);
155 
setOnSessionCompleteListener(Runnable runnable)156         void setOnSessionCompleteListener(Runnable runnable);
157     }
158 }
159