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.Flags.clipboardNoninteractiveOnLockscreen;
22 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
23 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
24 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
25 
26 import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
27 
28 import android.app.KeyguardManager;
29 import android.content.ClipData;
30 import android.content.ClipboardManager;
31 import android.content.Context;
32 import android.os.Build;
33 import android.provider.Settings;
34 import android.util.Log;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.internal.logging.UiEventLogger;
38 import com.android.systemui.CoreStartable;
39 import com.android.systemui.dagger.SysUISingleton;
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 KeyguardManager mKeyguardManager;
63     private final UiEventLogger mUiEventLogger;
64     private ClipboardOverlay mClipboardOverlay;
65 
66     @Inject
ClipboardListener(Context context, Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, ClipboardToast clipboardToast, ClipboardManager clipboardManager, KeyguardManager keyguardManager, UiEventLogger uiEventLogger)67     public ClipboardListener(Context context,
68             Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
69             ClipboardToast clipboardToast,
70             ClipboardManager clipboardManager,
71             KeyguardManager keyguardManager,
72             UiEventLogger uiEventLogger) {
73         mContext = context;
74         mOverlayProvider = clipboardOverlayControllerProvider;
75         mClipboardToast = clipboardToast;
76         mClipboardManager = clipboardManager;
77         mKeyguardManager = keyguardManager;
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, Build.IS_EMULATOR)) {
96             Log.i(TAG, "Clipboard overlay suppressed.");
97             return;
98         }
99 
100         // user should not access intents before setup or while device is locked
101         if ((clipboardNoninteractiveOnLockscreen() && mKeyguardManager.isDeviceLocked())
102                 || !isUserSetupComplete()
103                 || clipData == null // shouldn't happen, but just in case
104                 || clipData.getItemCount() == 0) {
105             if (shouldShowToast(clipData)) {
106                 mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource);
107                 mClipboardToast.showCopiedToast();
108             }
109             return;
110         }
111 
112         if (mClipboardOverlay == null) {
113             mClipboardOverlay = mOverlayProvider.get();
114             mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
115         } else {
116             mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
117         }
118         mClipboardOverlay.setClipData(clipData, clipSource);
119         mClipboardOverlay.setOnSessionCompleteListener(() -> {
120             // Session is complete, free memory until it's needed again.
121             mClipboardOverlay = null;
122         });
123     }
124 
125     // The overlay is suppressed if EXTRA_SUPPRESS_OVERLAY is true and the device is an emulator or
126     // the source package is SHELL_PACKAGE. This is meant to suppress the overlay when the emulator
127     // or a mirrored device is syncing the clipboard.
128     @VisibleForTesting
shouldSuppressOverlay(ClipData clipData, String clipSource, boolean isEmulator)129     static boolean shouldSuppressOverlay(ClipData clipData, String clipSource,
130             boolean isEmulator) {
131         if (!(isEmulator || SHELL_PACKAGE.equals(clipSource))) {
132             return false;
133         }
134         if (clipData == null || clipData.getDescription().getExtras() == null) {
135             return false;
136         }
137         return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false);
138     }
139 
shouldShowToast(ClipData clipData)140     boolean shouldShowToast(ClipData clipData) {
141         if (clipData == null) {
142             return false;
143         } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) {
144             // only show for classification complete if we aren't already showing a toast, to ignore
145             // the duplicate ClipData with classification
146             return !mClipboardToast.isShowing();
147         }
148         return true;
149     }
150 
isUserSetupComplete()151     private boolean isUserSetupComplete() {
152         return Settings.Secure.getInt(mContext.getContentResolver(),
153                 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
154     }
155 
156     interface ClipboardOverlay {
setClipData(ClipData clipData, String clipSource)157         void setClipData(ClipData clipData, String clipSource);
158 
setOnSessionCompleteListener(Runnable runnable)159         void setOnSessionCompleteListener(Runnable runnable);
160     }
161 }
162