1 /**
2  * Copyright (c) 2010, 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 android.content;
18 
19 import android.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SystemApi;
24 import android.annotation.SystemService;
25 import android.annotation.TestApi;
26 import android.compat.annotation.UnsupportedAppUsage;
27 import android.os.Handler;
28 import android.os.RemoteException;
29 import android.os.ServiceManager;
30 import android.os.ServiceManager.ServiceNotFoundException;
31 
32 import java.util.ArrayList;
33 import java.util.Objects;
34 
35 /**
36  * Interface to the clipboard service, for placing and retrieving text in
37  * the global clipboard.
38  *
39  * <p>
40  * The ClipboardManager API itself is very simple: it consists of methods
41  * to atomically get and set the current primary clipboard data.  That data
42  * is expressed as a {@link ClipData} object, which defines the protocol
43  * for data exchange between applications.
44  *
45  * <div class="special reference">
46  * <h3>Developer Guides</h3>
47  * <p>For more information about using the clipboard framework, read the
48  * <a href="{@docRoot}guide/topics/clipboard/copy-paste.html">Copy and Paste</a>
49  * developer guide.</p>
50  * </div>
51  */
52 @SystemService(Context.CLIPBOARD_SERVICE)
53 @android.ravenwood.annotation.RavenwoodKeepWholeClass
54 public class ClipboardManager extends android.text.ClipboardManager {
55 
56     /**
57      * DeviceConfig property, within the clipboard namespace, that determines whether notifications
58      * are shown when an app accesses clipboard. This may be overridden by a user-controlled
59      * setting.
60      *
61      * @hide
62      */
63     public static final String DEVICE_CONFIG_SHOW_ACCESS_NOTIFICATIONS =
64             "show_access_notifications";
65 
66     /**
67      * Default value for the DeviceConfig property that determines whether notifications are shown
68      * when an app accesses clipboard.
69      *
70      * @hide
71      */
72     public static final boolean DEVICE_CONFIG_DEFAULT_SHOW_ACCESS_NOTIFICATIONS = true;
73 
74     /**
75      * DeviceConfig property, within the clipboard namespace, that determines whether VirtualDevices
76      * are allowed to have siloed Clipboards for the apps running on them. If false, then clipboard
77      * access is blocked entirely for apps running on VirtualDevices.
78      *
79      * @hide
80      */
81     public static final String DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS =
82             "allow_virtualdevice_silos";
83 
84     /**
85      * Default value for the DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS property.
86      *
87      * @hide
88      */
89     public static final boolean DEVICE_CONFIG_DEFAULT_ALLOW_VIRTUALDEVICE_SILOS = true;
90 
91     private final Context mContext;
92     private final Handler mHandler;
93     private final IClipboard mService;
94 
95     private final ArrayList<OnPrimaryClipChangedListener> mPrimaryClipChangedListeners
96              = new ArrayList<OnPrimaryClipChangedListener>();
97 
98     private final IOnPrimaryClipChangedListener.Stub mPrimaryClipChangedServiceListener
99             = new IOnPrimaryClipChangedListener.Stub() {
100         @Override
101         public void dispatchPrimaryClipChanged() {
102             mHandler.post(() -> {
103                 reportPrimaryClipChanged();
104             });
105         }
106     };
107 
108     /**
109      * Defines a listener callback that is invoked when the primary clip on the clipboard changes.
110      * Objects that want to register a listener call
111      * {@link android.content.ClipboardManager#addPrimaryClipChangedListener(OnPrimaryClipChangedListener)
112      * addPrimaryClipChangedListener()} with an
113      * object that implements OnPrimaryClipChangedListener.
114      *
115      */
116     public interface OnPrimaryClipChangedListener {
117 
118         /**
119          * Callback that is invoked by {@link android.content.ClipboardManager} when the primary
120          * clip changes.
121          *
122          * <p>This is called when the result of {@link ClipDescription#getClassificationStatus()}
123          * changes, as well as when new clip data is set. So in cases where text classification is
124          * performed, this callback may be invoked multiple times for the same clip.
125          */
onPrimaryClipChanged()126         void onPrimaryClipChanged();
127     }
128 
129     /** {@hide} */
130     @UnsupportedAppUsage
ClipboardManager(Context context, Handler handler)131     public ClipboardManager(Context context, Handler handler) throws ServiceNotFoundException {
132         mContext = context;
133         mHandler = handler;
134         mService = IClipboard.Stub.asInterface(
135                 ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE));
136     }
137 
138     /**
139      * Determine if the Clipboard Access Notifications are enabled
140      *
141      * @return true if notifications are enabled, false otherwise.
142      *
143      * @hide
144      */
145     @SystemApi
146     @RequiresPermission(Manifest.permission.MANAGE_CLIPBOARD_ACCESS_NOTIFICATION)
147     @android.ravenwood.annotation.RavenwoodThrow
areClipboardAccessNotificationsEnabled()148     public boolean areClipboardAccessNotificationsEnabled() {
149         try {
150             return mService.areClipboardAccessNotificationsEnabledForUser(mContext.getUserId());
151         } catch (RemoteException e) {
152             throw e.rethrowFromSystemServer();
153         }
154     }
155 
156     /**
157      *
158      * Set the enable state of the Clipboard Access Notifications
159      * @param enable Whether to enable notifications
160      * @hide
161      */
162     @SystemApi
163     @RequiresPermission(Manifest.permission.MANAGE_CLIPBOARD_ACCESS_NOTIFICATION)
164     @android.ravenwood.annotation.RavenwoodThrow
setClipboardAccessNotificationsEnabled(boolean enable)165     public void setClipboardAccessNotificationsEnabled(boolean enable) {
166         try {
167             mService.setClipboardAccessNotificationsEnabledForUser(enable, mContext.getUserId());
168         } catch (RemoteException e) {
169             throw e.rethrowFromSystemServer();
170         }
171     }
172 
173     /**
174      * Sets the current primary clip on the clipboard.  This is the clip that
175      * is involved in normal cut and paste operations.
176      *
177      * @param clip The clipped data item to set.
178      * @see #getPrimaryClip()
179      * @see #clearPrimaryClip()
180      */
setPrimaryClip(@onNull ClipData clip)181     public void setPrimaryClip(@NonNull ClipData clip) {
182         try {
183             Objects.requireNonNull(clip);
184             clip.prepareToLeaveProcess(true);
185             mService.setPrimaryClip(
186                     clip,
187                     mContext.getOpPackageName(),
188                     mContext.getAttributionTag(),
189                     mContext.getUserId(),
190                     mContext.getDeviceId());
191         } catch (RemoteException e) {
192             throw e.rethrowFromSystemServer();
193         }
194     }
195 
196     /**
197      * Sets the current primary clip on the clipboard, attributed to the specified {@code
198      * sourcePackage}. The primary clip is the clip that is involved in normal cut and paste
199      * operations.
200      *
201      * @param clip The clipped data item to set.
202      * @param sourcePackage The package name of the app that is the source of the clip data.
203      * @throws IllegalArgumentException if the clip is null or contains no items.
204      *
205      * @hide
206      */
207     @SystemApi
208     @RequiresPermission(Manifest.permission.SET_CLIP_SOURCE)
setPrimaryClipAsPackage(@onNull ClipData clip, @NonNull String sourcePackage)209     public void setPrimaryClipAsPackage(@NonNull ClipData clip, @NonNull String sourcePackage) {
210         try {
211             Objects.requireNonNull(clip);
212             Objects.requireNonNull(sourcePackage);
213             clip.prepareToLeaveProcess(true);
214             mService.setPrimaryClipAsPackage(
215                     clip,
216                     mContext.getOpPackageName(),
217                     mContext.getAttributionTag(),
218                     mContext.getUserId(),
219                     mContext.getDeviceId(),
220                     sourcePackage);
221         } catch (RemoteException e) {
222             throw e.rethrowFromSystemServer();
223         }
224     }
225 
226     /**
227      * Clears any current primary clip on the clipboard.
228      *
229      * @see #setPrimaryClip(ClipData)
230      */
clearPrimaryClip()231     public void clearPrimaryClip() {
232         try {
233             mService.clearPrimaryClip(
234                     mContext.getOpPackageName(),
235                     mContext.getAttributionTag(),
236                     mContext.getUserId(),
237                     mContext.getDeviceId());
238         } catch (RemoteException e) {
239             throw e.rethrowFromSystemServer();
240         }
241     }
242 
243     /**
244      * Returns the current primary clip on the clipboard.
245      *
246      * <em>If the application is not the default IME or does not have input focus this return
247      * {@code null}.</em>
248      *
249      * @see #setPrimaryClip(ClipData)
250      */
getPrimaryClip()251     public @Nullable ClipData getPrimaryClip() {
252         try {
253             return mService.getPrimaryClip(
254                     mContext.getOpPackageName(),
255                     mContext.getAttributionTag(),
256                     mContext.getUserId(),
257                     mContext.getDeviceId());
258         } catch (RemoteException e) {
259             throw e.rethrowFromSystemServer();
260         }
261     }
262 
263     /**
264      * Returns a description of the current primary clip on the clipboard but not a copy of its
265      * data.
266      *
267      * <p><em>If the application is not the default IME or does not have input focus this return
268      * {@code null}.</em>
269      *
270      * @see #setPrimaryClip(ClipData)
271      */
getPrimaryClipDescription()272     public @Nullable ClipDescription getPrimaryClipDescription() {
273         try {
274             return mService.getPrimaryClipDescription(
275                     mContext.getOpPackageName(),
276                     mContext.getAttributionTag(),
277                     mContext.getUserId(),
278                     mContext.getDeviceId());
279         } catch (RemoteException e) {
280             throw e.rethrowFromSystemServer();
281         }
282     }
283 
284     /**
285      * Returns true if there is currently a primary clip on the clipboard.
286      *
287      * <em>If the application is not the default IME or the does not have input focus this will
288      * return {@code false}.</em>
289      */
hasPrimaryClip()290     public boolean hasPrimaryClip() {
291         try {
292             return mService.hasPrimaryClip(
293                     mContext.getOpPackageName(),
294                     mContext.getAttributionTag(),
295                     mContext.getUserId(),
296                     mContext.getDeviceId());
297         } catch (RemoteException e) {
298             throw e.rethrowFromSystemServer();
299         }
300     }
301 
addPrimaryClipChangedListener(OnPrimaryClipChangedListener what)302     public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener what) {
303         synchronized (mPrimaryClipChangedListeners) {
304             if (mPrimaryClipChangedListeners.isEmpty()) {
305                 try {
306                     mService.addPrimaryClipChangedListener(
307                             mPrimaryClipChangedServiceListener,
308                             mContext.getOpPackageName(),
309                             mContext.getAttributionTag(),
310                             mContext.getUserId(),
311                             mContext.getDeviceId());
312                 } catch (RemoteException e) {
313                     throw e.rethrowFromSystemServer();
314                 }
315             }
316             mPrimaryClipChangedListeners.add(what);
317         }
318     }
319 
removePrimaryClipChangedListener(OnPrimaryClipChangedListener what)320     public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener what) {
321         synchronized (mPrimaryClipChangedListeners) {
322             mPrimaryClipChangedListeners.remove(what);
323             if (mPrimaryClipChangedListeners.isEmpty()) {
324                 try {
325                     mService.removePrimaryClipChangedListener(
326                             mPrimaryClipChangedServiceListener,
327                             mContext.getOpPackageName(),
328                             mContext.getAttributionTag(),
329                             mContext.getUserId(),
330                             mContext.getDeviceId());
331                 } catch (RemoteException e) {
332                     throw e.rethrowFromSystemServer();
333                 }
334             }
335         }
336     }
337 
338     /**
339      * @deprecated Use {@link #getPrimaryClip()} instead.  This retrieves
340      * the primary clip and tries to coerce it to a string.
341      */
342     @Deprecated
getText()343     public CharSequence getText() {
344         ClipData clip = getPrimaryClip();
345         if (clip != null && clip.getItemCount() > 0) {
346             return clip.getItemAt(0).coerceToText(mContext);
347         }
348         return null;
349     }
350 
351     /**
352      * @deprecated Use {@link #setPrimaryClip(ClipData)} instead.  This
353      * creates a ClippedItem holding the given text and sets it as the
354      * primary clip.  It has no label or icon.
355      */
356     @Deprecated
setText(CharSequence text)357     public void setText(CharSequence text) {
358         setPrimaryClip(ClipData.newPlainText(null, text));
359     }
360 
361     /**
362      * @deprecated Use {@link #hasPrimaryClip()} instead.
363      */
364     @Deprecated
hasText()365     public boolean hasText() {
366         try {
367             return mService.hasClipboardText(
368                     mContext.getOpPackageName(),
369                     mContext.getAttributionTag(),
370                     mContext.getUserId(),
371                     mContext.getDeviceId());
372         } catch (RemoteException e) {
373             throw e.rethrowFromSystemServer();
374         }
375     }
376 
377     /**
378      * Returns the package name of the source of the current primary clip, or null if there is no
379      * primary clip or if a source is not available.
380      *
381      * @hide
382      */
383     @TestApi
384     @Nullable
385     @RequiresPermission(Manifest.permission.SET_CLIP_SOURCE)
getPrimaryClipSource()386     public String getPrimaryClipSource() {
387         try {
388             return mService.getPrimaryClipSource(
389                     mContext.getOpPackageName(),
390                     mContext.getAttributionTag(),
391                     mContext.getUserId(),
392                     mContext.getDeviceId());
393         } catch (RemoteException e) {
394             throw e.rethrowFromSystemServer();
395         }
396     }
397 
398     @UnsupportedAppUsage
reportPrimaryClipChanged()399     void reportPrimaryClipChanged() {
400         Object[] listeners;
401 
402         synchronized (mPrimaryClipChangedListeners) {
403             final int N = mPrimaryClipChangedListeners.size();
404             if (N <= 0) {
405                 return;
406             }
407             listeners = mPrimaryClipChangedListeners.toArray();
408         }
409 
410         for (int i=0; i<listeners.length; i++) {
411             ((OnPrimaryClipChangedListener)listeners[i]).onPrimaryClipChanged();
412         }
413     }
414 }
415