17 | * A basic {@link Lens} implementation that composes an email with the provided addresses and 18 | * subject (optional). 19 | *
20 | * 21 | *The {@link #getBody()} method can be overridden to pre-populate the body of the email.
22 | */ 23 | public class EmailLens extends Lens { 24 | private final Context context; 25 | private final String subject; 26 | private final String[] addresses; 27 | 28 | /** @deprecated Use {@link #EmailLens(Context, String, String...)}. */ 29 | @Deprecated 30 | public EmailLens(Context context, String[] addresses, String subject) { 31 | this(context, subject, addresses); 32 | } 33 | 34 | public EmailLens(Context context, String subject, String... addresses) { 35 | this.context = context; 36 | this.addresses = addresses == null ? null : addresses.clone(); 37 | this.subject = subject; 38 | } 39 | 40 | /** Create the email body. */ 41 | @WorkerThread protected String getBody() { 42 | return null; 43 | } 44 | 45 | @Override public void onCapture(File screenshot) { 46 | new CreateIntentTask(context, screenshot).execute(); 47 | } 48 | 49 | @WorkerThread protected Setcontent://
{@link Uri} for a file
53 | * instead of a file:///
{@link Uri}.
54 | * 55 | * A content URI allows you to grant read and write access using 56 | * temporary access permissions. When you create an {@link Intent} containing 57 | * a content URI, in order to send the content URI 58 | * to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add 59 | * permissions. These permissions are available to the client app for as long as the stack for 60 | * a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a 61 | * {@link android.app.Service}, the permissions are available as long as the 62 | * {@link android.app.Service} is running. 63 | *
64 | * In comparison, to control access to a file:///
{@link Uri} you have to modify the
65 | * file system permissions of the underlying file. The permissions you provide become available to
66 | * any app, and remain in effect until you change them. This level of access is
67 | * fundamentally insecure.
68 | *
69 | * The increased level of file access security offered by a content URI 70 | * makes FileProvider a key part of Android's security infrastructure. 71 | *
72 | * This overview of FileProvider includes the following topics: 73 | *
74 | *
83 | * Since the default functionality of FileProvider includes content URI generation for files, you
84 | * don't need to define a subclass in code. Instead, you can include a FileProvider in your app
85 | * by specifying it entirely in XML. To specify the FileProvider component itself, add a
86 | * <provider>
87 | * element to your app manifest. Set the android:name
attribute to
88 | * android.support.v4.content.FileProvider
. Set the android:authorities
89 | * attribute to a URI authority based on a domain you control; for example, if you control the
90 | * domain mydomain.com
you should use the authority
91 | * com.mydomain.fileprovider
. Set the android:exported
attribute to
92 | * false
; the FileProvider does not need to be public. Set the
93 | * android:grantUriPermissions attribute to true
, to allow you
95 | * to grant temporary access to files. For example:
96 | *
97 | * <manifest> 98 | * ... 99 | * <application> 100 | * ... 101 | * <provider 102 | * android:name="android.support.v4.content.FileProvider" 103 | * android:authorities="com.mydomain.fileprovider" 104 | * android:exported="false" 105 | * android:grantUriPermissions="true"> 106 | * ... 107 | * </provider> 108 | * ... 109 | * </application> 110 | * </manifest>111 | *
112 | * If you want to override any of the default behavior of FileProvider methods, extend
113 | * the FileProvider class and use the fully-qualified class name in the android:name
114 | * attribute of the <provider>
element.
115 | *
<paths>
element.
119 | * For example, the following paths
element tells FileProvider that you intend to
120 | * request content URIs for the images/
subdirectory of your private file area.
121 | * 122 | * <paths xmlns:android="http://schemas.android.com/apk/res/android"> 123 | * <files-path name="my_images" path="images/"/> 124 | * ... 125 | * </paths> 126 | *127 | *
128 | * The <paths>
element must contain one or more of the following child elements:
129 | *
133 | * <files-path name="name" path="path" /> 134 | *135 | *
files/
subdirectory of your app's internal storage
138 | * area. This subdirectory is the same as the value returned by {@link Context#getFilesDir()
139 | * Context.getFilesDir()}.
140 | * 142 | * <external-path name="name" path="path" /> 143 | *144 | *
files/
subdirectory of this this root.
149 | * 152 | * <cache-path name="name" path="path" /> 153 | *154 | *
162 | * These child elements all use the same attributes: 163 | *
164 | *name="name"
167 | * path
attribute.
172 | * path="path"
175 | * name
attribute is a URI path
178 | * segment, the path
value is an actual subdirectory name. Notice that the
179 | * value refers to a subdirectory, not an individual file or files. You can't
180 | * share a single file by its file name, nor can you specify a subset of files using
181 | * wildcards.
182 | *
185 | * You must specify a child element of <paths>
for each directory that contains
186 | * files for which you want content URIs. For example, these XML elements specify two directories:
187 | *
188 | * <paths xmlns:android="http://schemas.android.com/apk/res/android"> 189 | * <files-path name="my_images" path="images/"/> 190 | * <files-path name="my_docs" path="docs/"/> 191 | * </paths> 192 | *193 | *
194 | * Put the <paths>
element and its children in an XML file in your project.
195 | * For example, you can add them to a new file called res/xml/file_paths.xml
.
196 | * To link this file to the FileProvider, add a
197 | * <meta-data> element
198 | * as a child of the <provider>
element that defines the FileProvider. Set the
199 | * <meta-data>
element's "android:name" attribute to
200 | * android.support.FILE_PROVIDER_PATHS
. Set the element's "android:resource" attribute
201 | * to @xml/file_paths
(notice that you don't specify the .xml
202 | * extension). For example:
203 | *
204 | * <provider 205 | * android:name="android.support.v4.content.FileProvider" 206 | * android:authorities="com.mydomain.fileprovider" 207 | * android:exported="false" 208 | * android:grantUriPermissions="true"> 209 | * <meta-data 210 | * android:name="android.support.FILE_PROVIDER_PATHS" 211 | * android:resource="@xml/file_paths" /> 212 | * </provider> 213 | *214 | *
216 | * To share a file with another app using a content URI, your app has to generate the content URI. 217 | * To generate the content URI, create a new {@link File} for the file, then pass the {@link File} 218 | * to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI 219 | * returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an 220 | * {@link android.content.Intent}. The client app that receives the content URI can open the file 221 | * and access its contents by calling 222 | * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) 223 | * ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}. 224 | *
225 | * For example, suppose your app is offering files to other apps with a FileProvider that has the
226 | * authority com.mydomain.fileprovider
. To get a content URI for the file
227 | * default_image.jpg
in the images/
subdirectory of your internal storage
228 | * add the following code:
229 | *
230 | * File imagePath = new File(Context.getFilesDir(), "images"); 231 | * File newFile = new File(imagePath, "default_image.jpg"); 232 | * Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile); 233 | *234 | * As a result of the previous snippet, 235 | * {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI 236 | *
content://com.mydomain.fileprovider/my_images/default_image.jpg
.
237 | * content://
245 | * {@link Uri}, using the desired mode flags. This grants temporary access permission for the
246 | * content URI to the specified package, according to the value of the
247 | * the mode_flags
parameter, which you can set to
248 | * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}
249 | * or both. The permission remains in effect until you revoke it by calling
250 | * {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device
251 | * reboots.
252 | * 266 | * Permissions granted in an {@link Intent} remain in effect while the stack of the receiving 267 | * {@link android.app.Activity} is active. When the stack finishes, the permissions are 268 | * automatically removed. Permissions granted to one {@link android.app.Activity} in a client 269 | * app are automatically extended to other components of that app. 270 | *
271 | *275 | * There are a variety of ways to serve the content URI for a file to a client app. One common way 276 | * is for the client app to start your app by calling 277 | * {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()}, 278 | * which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app. 279 | * In response, your app can immediately return a content URI to the client app or present a user 280 | * interface that allows the user to pick a file. In the latter case, once the user picks the file 281 | * your app can return its content URI. In both cases, your app returns the content URI in an 282 | * {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}. 283 | *
284 | *285 | * You can also put the content URI in a {@link android.content.ClipData} object and then add the 286 | * object to an {@link Intent} you send to a client app. To do this, call 287 | * {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can 288 | * add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own 289 | * content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent} 290 | * to set temporary access permissions, the same permissions are applied to all of the content 291 | * URIs. 292 | *
293 | *294 | * Note: The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is 295 | * only available in platform version 16 (Android 4.1) and later. If you want to maintain 296 | * compatibility with previous versions, you should send one content URI at a time in the 297 | * {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling 298 | * {@link Intent#setData setData()}. 299 | *
300 | *302 | * To learn more about FileProvider, see the Android training class 303 | * Sharing Files Securely with 304 | * URIs. 305 | *
306 | */ 307 | class FileProvider extends ContentProvider { 308 | private static final String[] COLUMNS = { 309 | OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE 310 | }; 311 | 312 | private static final String META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"; 313 | 314 | private static final String TAG_ROOT_PATH = "root-path"; 315 | private static final String TAG_FILES_PATH = "files-path"; 316 | private static final String TAG_CACHE_PATH = "cache-path"; 317 | private static final String TAG_EXTERNAL = "external-path"; 318 | private static final String TAG_EXTERNAL_APP = "external-app-path"; 319 | 320 | private static final String ATTR_NAME = "name"; 321 | private static final String ATTR_PATH = "path"; 322 | 323 | private static final File DEVICE_ROOT = new File("/"); 324 | 325 | // @GuardedBy("sCache") 326 | private static HashMapcontent
{@link Uri} for file paths defined in their <paths>
368 | * meta-data element. See the Class Overview for more information.
369 | *
370 | * @param context A {@link Context} for the current component.
371 | * @param authority The authority of a {@link FileProvider} defined in a
372 | * {@code <provider>} element in your app's manifest.
373 | * @param file A {@link File} pointing to the filename for which you want a
374 | * content
{@link Uri}.
375 | * @return A content URI for the file.
376 | * @throws IllegalArgumentException When the given {@link File} is outside
377 | * the paths supported by the provider.
378 | */
379 | protected static Uri getUriForFile(Context context, String authority, File file) {
380 | final PathStrategy strategy = getPathStrategy(context, authority);
381 | return strategy.getUriForFile(file);
382 | }
383 |
384 | /**
385 | * Use a content URI returned by
386 | * {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
387 | * managed by the FileProvider.
388 | * FileProvider reports the column names defined in {@link android.provider.OpenableColumns}:
389 | * application/octet-stream
.
449 | */
450 | @Override public String getType(Uri uri) {
451 | // ContentProvider has already checked granted permissions
452 | final File file = mStrategy.getFileForUri(uri);
453 |
454 | final int lastDot = file.getName().lastIndexOf('.');
455 | if (lastDot >= 0) {
456 | final String extension = file.getName().substring(lastDot + 1);
457 | final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
458 | if (mime != null) {
459 | return mime;
460 | }
461 | }
462 |
463 | return "application/octet-stream";
464 | }
465 |
466 | /**
467 | * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must
468 | * subclass FileProvider if you want to provide different functionality.
469 | */
470 | @Override public Uri insert(Uri uri, ContentValues values) {
471 | throw new UnsupportedOperationException("No external inserts");
472 | }
473 |
474 | /**
475 | * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must
476 | * subclass FileProvider if you want to provide different functionality.
477 | */
478 | @Override public int update(Uri uri, ContentValues values, String selection,
479 | String[] selectionArgs) {
480 | throw new UnsupportedOperationException("No external updates");
481 | }
482 |
483 | /**
484 | * Deletes the file associated with the specified content URI, as
485 | * returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
486 | * method does not throw an {@link java.io.IOException}; you must check its return value.
487 | *
488 | * @param uri A content URI for a file, as returned by
489 | * {@link #getUriForFile(Context, String, File) getUriForFile()}.
490 | * @param selection Ignored. Set to {@code null}.
491 | * @param selectionArgs Ignored. Set to {@code null}.
492 | * @return 1 if the delete succeeds; otherwise, 0.
493 | */
494 | @Override public int delete(Uri uri, String selection, String[] selectionArgs) {
495 | // ContentProvider has already checked granted permissions
496 | final File file = mStrategy.getFileForUri(uri);
497 | return file.delete() ? 1 : 0;
498 | }
499 |
500 | /**
501 | * By default, FileProvider automatically returns the
502 | * {@link ParcelFileDescriptor} for a file associated with a content://
503 | * {@link Uri}. To get the {@link ParcelFileDescriptor}, call
504 | * {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
505 | * ContentResolver.openFileDescriptor}.
506 | *
507 | * To override this method, you must provide your own subclass of FileProvider.
508 | *
509 | * @param uri A content URI associated with a file, as returned by
510 | * {@link #getUriForFile(Context, String, File) getUriForFile()}.
511 | * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
512 | * write access, or "rwt" for read and write access that truncates any existing file.
513 | * @return A new {@link ParcelFileDescriptor} with which you can access the file.
514 | */
515 | @Override public ParcelFileDescriptor openFile(Uri uri, String mode)
516 | throws FileNotFoundException {
517 | // ContentProvider has already checked granted permissions
518 | final File file = mStrategy.getFileForUri(uri);
519 | final int fileMode = modeToMode(mode);
520 | return ParcelFileDescriptor.open(file, fileMode);
521 | }
522 |
523 | /**
524 | * Return {@link PathStrategy} for given authority, either by parsing or
525 | * returning from cache.
526 | */
527 | private static PathStrategy getPathStrategy(Context context, String authority) {
528 | PathStrategy strat;
529 | synchronized (sCache) {
530 | strat = sCache.get(authority);
531 | if (strat == null) {
532 | try {
533 | strat = parsePathStrategy(context, authority);
534 | } catch (IOException e) {
535 | throw new IllegalArgumentException(
536 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
537 | } catch (XmlPullParserException e) {
538 | throw new IllegalArgumentException(
539 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
540 | }
541 | sCache.put(authority, strat);
542 | }
543 | }
544 | return strat;
545 | }
546 |
547 | /**
548 | * Parse and return {@link PathStrategy} for given authority as defined in
549 | * {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
550 | *
551 | * @see #getPathStrategy(Context, String)
552 | */
553 | private static PathStrategy parsePathStrategy(Context context, String authority)
554 | throws IOException, XmlPullParserException {
555 | final SimplePathStrategy strat = new SimplePathStrategy(authority);
556 |
557 | final ProviderInfo info =
558 | context.getPackageManager().resolveContentProvider(authority, PackageManager.GET_META_DATA);
559 | final XmlResourceParser in =
560 | info.loadXmlMetaData(context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
561 | if (in == null) {
562 | throw new IllegalArgumentException("Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
563 | }
564 |
565 | int type;
566 | while ((type = in.next()) != END_DOCUMENT) {
567 | if (type == START_TAG) {
568 | final String tag = in.getName();
569 |
570 | final String name = in.getAttributeValue(null, ATTR_NAME);
571 | String path = in.getAttributeValue(null, ATTR_PATH);
572 |
573 | File target = null;
574 | if (TAG_ROOT_PATH.equals(tag)) {
575 | target = buildPath(DEVICE_ROOT, path);
576 | } else if (TAG_FILES_PATH.equals(tag)) {
577 | target = buildPath(context.getFilesDir(), path);
578 | } else if (TAG_CACHE_PATH.equals(tag)) {
579 | target = buildPath(context.getCacheDir(), path);
580 | } else if (TAG_EXTERNAL.equals(tag)) {
581 | target = buildPath(Environment.getExternalStorageDirectory(), path);
582 | } else if (TAG_EXTERNAL_APP.equals(tag)) {
583 | target = buildPath(context.getExternalFilesDir(null), path);
584 | }
585 |
586 | if (target != null) {
587 | strat.addRoot(name, target);
588 | }
589 | }
590 | }
591 |
592 | return strat;
593 | }
594 |
595 | /**
596 | * Strategy for mapping between {@link File} and {@link Uri}.
597 | * 598 | * Strategies must be symmetric so that mapping a {@link File} to a 599 | * {@link Uri} and then back to a {@link File} points at the original 600 | * target. 601 | *
602 | * Strategies must remain consistent across app launches, and not rely on 603 | * dynamic state. This ensures that any generated {@link Uri} can still be 604 | * resolved if your process is killed and later restarted. 605 | * 606 | * @see SimplePathStrategy 607 | */ 608 | interface PathStrategy { 609 | /** 610 | * Return a {@link Uri} that represents the given {@link File}. 611 | */ 612 | public Uri getUriForFile(File file); 613 | 614 | /** 615 | * Return a {@link File} that represents the given {@link Uri}. 616 | */ 617 | public File getFileForUri(Uri uri); 618 | } 619 | 620 | /** 621 | * Strategy that provides access to files living under a narrow whitelist of 622 | * filesystem roots. It will throw {@link SecurityException} if callers try 623 | * accessing files outside the configured roots. 624 | *
625 | * For example, if configured with
626 | * {@code addRoot("myfiles", context.getFilesDir())}, then
627 | * {@code context.getFileStreamPath("foo.txt")} would map to
628 | * {@code content://myauthority/myfiles/foo.txt}.
629 | */
630 | static class SimplePathStrategy implements PathStrategy {
631 | private final String mAuthority;
632 | private final HashMap
9 | * System screenshots are only available on API 21+. Telescope will automatically fall back to
10 | * {@link #CANVAS} mode on earlier platforms or if screen recording permission was not granted.
11 | * {@link #CANVAS} will also be used if Telescope has been configured to screenshot children only
12 | * or if a different target view has been specified.
13 | *
14 | *
15 | *
16 | * Requires the
17 | * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE} permission
18 | * on API 18 and below.
19 | *
20 | */
21 | SYSTEM,
22 | /**
23 | * Uses the drawing cache of the target view to create a screenshot.
24 | *
25 | *
26 | *
27 | * Requires the
28 | * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE} permission
29 | * on API 18 and below.
30 | *
31 | */
32 | CANVAS,
33 | /** Do not save a screenshot. */
34 | NONE,
35 | }
36 |
--------------------------------------------------------------------------------
/telescope/src/main/java/com/mattprecious/telescope/TelescopeFileProvider.java:
--------------------------------------------------------------------------------
1 | package com.mattprecious.telescope;
2 |
3 | import android.content.Context;
4 | import android.net.Uri;
5 | import java.io.File;
6 |
7 | public final class TelescopeFileProvider extends FileProvider {
8 | /**
9 | * Calls {@link #getUriForFile(Context, String, File)} using the correct authority for Telescope
10 | * screenshots.
11 | */
12 | public static Uri getUriForFile(Context context, File file) {
13 | return getUriForFile(context, context.getPackageName() + ".telescope.fileprovider", file);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/telescope/src/main/java/com/mattprecious/telescope/TelescopeLayout.java:
--------------------------------------------------------------------------------
1 | package com.mattprecious.telescope;
2 |
3 | import android.animation.ValueAnimator;
4 | import android.annotation.SuppressLint;
5 | import android.annotation.TargetApi;
6 | import android.app.Activity;
7 | import android.content.BroadcastReceiver;
8 | import android.content.Context;
9 | import android.content.ContextWrapper;
10 | import android.content.Intent;
11 | import android.content.IntentFilter;
12 | import android.content.res.TypedArray;
13 | import android.graphics.Bitmap;
14 | import android.graphics.Canvas;
15 | import android.graphics.Paint;
16 | import android.graphics.PixelFormat;
17 | import android.hardware.display.DisplayManager;
18 | import android.hardware.display.VirtualDisplay;
19 | import android.media.Image;
20 | import android.media.ImageReader;
21 | import android.media.projection.MediaProjection;
22 | import android.media.projection.MediaProjectionManager;
23 | import android.os.AsyncTask;
24 | import android.os.Build;
25 | import android.os.Handler;
26 | import android.os.HandlerThread;
27 | import android.os.Process;
28 | import android.os.Vibrator;
29 | import android.util.AttributeSet;
30 | import android.util.DisplayMetrics;
31 | import android.util.Log;
32 | import android.view.MotionEvent;
33 | import android.view.PixelCopy;
34 | import android.view.Surface;
35 | import android.view.View;
36 | import android.view.Window;
37 | import android.view.WindowManager;
38 | import android.widget.FrameLayout;
39 | import androidx.annotation.ColorInt;
40 | import androidx.annotation.IntRange;
41 | import androidx.annotation.NonNull;
42 | import java.io.File;
43 | import java.io.FileNotFoundException;
44 | import java.io.FileOutputStream;
45 | import java.io.IOException;
46 | import java.nio.ByteBuffer;
47 | import java.text.SimpleDateFormat;
48 | import java.util.Date;
49 | import java.util.Locale;
50 |
51 | import static android.Manifest.permission.VIBRATE;
52 | import static android.animation.ValueAnimator.AnimatorUpdateListener;
53 | import static android.content.pm.PackageManager.PERMISSION_GRANTED;
54 | import static android.graphics.Paint.Style;
55 | import static android.os.Build.VERSION.SDK_INT;
56 | import static android.view.PixelCopy.SUCCESS;
57 | import static com.mattprecious.telescope.Preconditions.checkNotNull;
58 |
59 | /**
60 | * A layout used to take a screenshot and initiate a callback when the user long-presses the
61 | * container.
62 | */
63 | public class TelescopeLayout extends FrameLayout {
64 | private static final String TAG = "Telescope";
65 | static final SimpleDateFormat SCREENSHOT_FILE_FORMAT =
66 | new SimpleDateFormat("'telescope'-yyyy-MM-dd-HHmmss.'png'", Locale.US);
67 | private static final int PROGRESS_STROKE_DP = 4;
68 | private static final long CANCEL_DURATION_MS = 250;
69 | private static final long DONE_DURATION_MS = 1000;
70 | private static final long TRIGGER_DURATION_MS = 1000;
71 | private static final long VIBRATION_DURATION_MS = 50;
72 |
73 | private static final int DEFAULT_POINTER_COUNT = 2;
74 | private static final int DEFAULT_PROGRESS_COLOR = 0xff2196f3;
75 |
76 | private static Handler backgroundHandler;
77 |
78 | final MediaProjectionManager projectionManager;
79 | final WindowManager windowManager;
80 | private final Vibrator vibrator;
81 | private final Handler handler = new Handler();
82 | private final Runnable trigger = this::trigger;
83 | private final IntentFilter requestCaptureFilter;
84 | private final BroadcastReceiver requestCaptureReceiver;
85 | private final IntentFilter serviceStartedFilter;
86 | private final BroadcastReceiver serviceStartedReceiver;
87 |
88 | private final float halfStrokeWidth;
89 | private final Paint progressPaint;
90 | private final ValueAnimator progressAnimator;
91 | private final ValueAnimator progressCancelAnimator;
92 | private final ValueAnimator doneAnimator;
93 |
94 | Lens lens;
95 | private View screenshotTarget;
96 | private int pointerCount;
97 | private ScreenshotMode screenshotMode;
98 | private boolean screenshotChildrenOnly;
99 | private boolean vibrate;
100 |
101 | // State.
102 | float progressFraction;
103 | float doneFraction;
104 | private boolean pressing;
105 | private boolean capturing;
106 | boolean saving;
107 |
108 | public TelescopeLayout(Context context) {
109 | this(context, null);
110 | }
111 |
112 | public TelescopeLayout(Context context, AttributeSet attrs) {
113 | this(context, attrs, 0);
114 | }
115 |
116 | public TelescopeLayout(Context context, AttributeSet attrs, int defStyle) {
117 | super(context, attrs, defStyle);
118 | setWillNotDraw(false);
119 | screenshotTarget = this;
120 |
121 | float density = context.getResources().getDisplayMetrics().density;
122 | halfStrokeWidth = PROGRESS_STROKE_DP * density / 2;
123 |
124 | TypedArray a =
125 | context.obtainStyledAttributes(attrs, R.styleable.telescope_TelescopeLayout, defStyle, 0);
126 | pointerCount = a.getInt(R.styleable.telescope_TelescopeLayout_telescope_pointerCount,
127 | DEFAULT_POINTER_COUNT);
128 | int progressColor = a.getColor(R.styleable.telescope_TelescopeLayout_telescope_progressColor,
129 | DEFAULT_PROGRESS_COLOR);
130 | screenshotMode = ScreenshotMode.values()[a.getInt(
131 | R.styleable.telescope_TelescopeLayout_telescope_screenshotMode,
132 | ScreenshotMode.SYSTEM.ordinal())];
133 | screenshotChildrenOnly =
134 | a.getBoolean(R.styleable.telescope_TelescopeLayout_telescope_screenshotChildrenOnly, false);
135 | vibrate = a.getBoolean(R.styleable.telescope_TelescopeLayout_telescope_vibrate, true);
136 | a.recycle();
137 |
138 | progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
139 | progressPaint.setColor(progressColor);
140 | progressPaint.setStrokeWidth(PROGRESS_STROKE_DP * density);
141 | progressPaint.setStyle(Style.STROKE);
142 |
143 | AnimatorUpdateListener progressUpdateListener = animation -> {
144 | progressFraction = (float) animation.getAnimatedValue();
145 | invalidate();
146 | };
147 |
148 | progressAnimator = new ValueAnimator();
149 | progressAnimator.setDuration(TRIGGER_DURATION_MS);
150 | progressAnimator.addUpdateListener(progressUpdateListener);
151 |
152 | progressCancelAnimator = new ValueAnimator();
153 | progressCancelAnimator.setDuration(CANCEL_DURATION_MS);
154 | progressCancelAnimator.addUpdateListener(progressUpdateListener);
155 |
156 | doneFraction = 1;
157 | doneAnimator = ValueAnimator.ofFloat(0, 1);
158 | doneAnimator.setDuration(DONE_DURATION_MS);
159 | doneAnimator.addUpdateListener(animation -> {
160 | doneFraction = (float) animation.getAnimatedValue();
161 | invalidate();
162 | });
163 |
164 | if (isInEditMode()) {
165 | projectionManager = null;
166 | windowManager = null;
167 | vibrator = null;
168 | requestCaptureFilter = null;
169 | requestCaptureReceiver = null;
170 | serviceStartedFilter = null;
171 | serviceStartedReceiver = null;
172 | return;
173 | }
174 |
175 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
176 | vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
177 |
178 | if (SDK_INT < 21) {
179 | projectionManager = null;
180 | requestCaptureFilter = null;
181 | requestCaptureReceiver = null;
182 | serviceStartedFilter = null;
183 | serviceStartedReceiver = null;
184 | } else {
185 | projectionManager =
186 | (MediaProjectionManager) context.getApplicationContext()
187 | .getSystemService(Context.MEDIA_PROJECTION_SERVICE);
188 |
189 | requestCaptureFilter =
190 | new IntentFilter(RequestCaptureActivity.getResultBroadcastAction(context));
191 | requestCaptureReceiver = new BroadcastReceiver() {
192 | @TargetApi(21) @Override
193 | public void onReceive(Context context, Intent intent) {
194 | unregisterRequestCaptureReceiver();
195 |
196 | int resultCode = intent.getIntExtra(RequestCaptureActivity.RESULT_EXTRA_CODE,
197 | Activity.RESULT_CANCELED);
198 |
199 | if (resultCode != Activity.RESULT_OK) {
200 | captureWindowScreenshot();
201 | return;
202 | }
203 |
204 | // The service needs to be running before we start the projection and there's no guarantee
205 | // that it will have started once we return from startForegroundService. Rather than using
206 | // binders, we'll just bounce the data through another broadcast from the service.
207 | registerServiceStartedReceiver();
208 |
209 | Intent data = intent.getParcelableExtra(RequestCaptureActivity.RESULT_EXTRA_DATA);
210 | startForegroundService(data);
211 | }
212 | };
213 |
214 | serviceStartedFilter =
215 | new IntentFilter(TelescopeProjectionService.getStartedBroadcastAction(context));
216 | serviceStartedReceiver = new BroadcastReceiver() {
217 | @TargetApi(21) @Override
218 | public void onReceive(Context context, Intent intent) {
219 | unregisterServiceStartedReceiver();
220 |
221 | Intent data = intent.getParcelableExtra(TelescopeProjectionService.EXTRA_DATA);
222 |
223 | final MediaProjection mediaProjection =
224 | projectionManager.getMediaProjection(Activity.RESULT_OK, data);
225 |
226 | if (intent.getBooleanExtra(RequestCaptureActivity.RESULT_EXTRA_PROMPT_SHOWN, true)) {
227 | // Delay capture until after the permission dialog is gone.
228 | postDelayed(() -> captureNativeScreenshot(mediaProjection), 500);
229 | } else {
230 | captureNativeScreenshot(mediaProjection);
231 | }
232 | }
233 | };
234 | }
235 | }
236 |
237 | /**
238 | * Delete the screenshot folder for this app. Be careful not to call this before any intents have
239 | * finished using a screenshot reference.
240 | */
241 | public static void cleanUp(Context context) {
242 | File path = getScreenshotFolder(context);
243 | if (!path.exists()) {
244 | return;
245 | }
246 |
247 | delete(path);
248 | }
249 |
250 | /** Set the {@link Lens} to be called when the user triggers a capture. */
251 | public void setLens(@NonNull Lens lens) {
252 | checkNotNull(lens, "lens == null");
253 | this.lens = lens;
254 | }
255 |
256 | /** Set the number of pointers requires to trigger the capture. Default is 2. */
257 | public void setPointerCount(@IntRange(from = 1) int pointerCount) {
258 | if (pointerCount < 1) {
259 | throw new IllegalArgumentException("pointerCount < 1");
260 | }
261 |
262 | this.pointerCount = pointerCount;
263 | }
264 |
265 | /** Set the color of the progress bars. */
266 | public void setProgressColor(@ColorInt int progressColor) {
267 | progressPaint.setColor(progressColor);
268 | }
269 |
270 | /** Sets the {@link ScreenshotMode} used to capture a screenshot. */
271 | public void setScreenshotMode(@NonNull ScreenshotMode screenshotMode) {
272 | checkNotNull(screenshotMode, "screenshotMode == null");
273 | this.screenshotMode = screenshotMode;
274 | }
275 |
276 | /**
277 | * Set whether the screenshot will capture the children of this view only, or if it will
278 | * capture the whole window this view is in. Default is false.
279 | */
280 | public void setScreenshotChildrenOnly(boolean screenshotChildrenOnly) {
281 | this.screenshotChildrenOnly = screenshotChildrenOnly;
282 | }
283 |
284 | /** Set the target view that the screenshot will capture. */
285 | public void setScreenshotTarget(@NonNull View screenshotTarget) {
286 | checkNotNull(screenshotTarget, "screenshotTarget == null");
287 | this.screenshotTarget = screenshotTarget;
288 | }
289 |
290 | /**
291 | * Set whether vibration is enabled when a capture is triggered. Default is true. Requires the {@link android.Manifest.permission#VIBRATE} permission.