1 /*
2  * Copyright (C) 2023 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.screenshot.appclips;
18 
19 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
20 
21 import android.content.Intent;
22 import android.graphics.Bitmap;
23 import android.graphics.HardwareRenderer;
24 import android.graphics.RecordingCanvas;
25 import android.graphics.Rect;
26 import android.graphics.RenderNode;
27 import android.graphics.drawable.Drawable;
28 import android.net.Uri;
29 import android.os.UserHandle;
30 
31 import androidx.annotation.NonNull;
32 import androidx.lifecycle.LiveData;
33 import androidx.lifecycle.MutableLiveData;
34 import androidx.lifecycle.ViewModel;
35 import androidx.lifecycle.ViewModelProvider;
36 
37 import com.android.systemui.dagger.qualifiers.Background;
38 import com.android.systemui.dagger.qualifiers.Main;
39 import com.android.systemui.screenshot.ImageExporter;
40 
41 import com.google.common.util.concurrent.ListenableFuture;
42 
43 import java.util.UUID;
44 import java.util.concurrent.CancellationException;
45 import java.util.concurrent.ExecutionException;
46 import java.util.concurrent.Executor;
47 
48 import javax.inject.Inject;
49 
50 /** A {@link ViewModel} to help with the App Clips screenshot flow. */
51 final class AppClipsViewModel extends ViewModel {
52 
53     private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
54     private final ImageExporter mImageExporter;
55     @Main
56     private final Executor mMainExecutor;
57     @Background
58     private final Executor mBgExecutor;
59 
60     private final MutableLiveData<Bitmap> mScreenshotLiveData;
61     private final MutableLiveData<Uri> mResultLiveData;
62     private final MutableLiveData<Integer> mErrorLiveData;
63 
AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, @Main Executor mainExecutor, @Background Executor bgExecutor)64     AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper,
65             ImageExporter imageExporter, @Main Executor mainExecutor,
66             @Background Executor bgExecutor) {
67         mAppClipsCrossProcessHelper = appClipsCrossProcessHelper;
68         mImageExporter = imageExporter;
69         mMainExecutor = mainExecutor;
70         mBgExecutor = bgExecutor;
71 
72         mScreenshotLiveData = new MutableLiveData<>();
73         mResultLiveData = new MutableLiveData<>();
74         mErrorLiveData = new MutableLiveData<>();
75     }
76 
77     /** Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link LiveData}. */
performScreenshot()78     void performScreenshot() {
79         mBgExecutor.execute(() -> {
80             Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot();
81             mMainExecutor.execute(() -> {
82                 if (screenshot == null) {
83                     mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
84                 } else {
85                     mScreenshotLiveData.setValue(screenshot);
86                 }
87             });
88         });
89     }
90 
91     /** Returns a {@link LiveData} that holds the captured screenshot. */
getScreenshot()92     LiveData<Bitmap> getScreenshot() {
93         return mScreenshotLiveData;
94     }
95 
96     /** Returns a {@link LiveData} that holds the {@link Uri} where screenshot is saved. */
getResultLiveData()97     LiveData<Uri> getResultLiveData() {
98         return mResultLiveData;
99     }
100 
101     /**
102      * Returns a {@link LiveData} that holds the error codes for
103      * {@link Intent#EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE}.
104      */
getErrorLiveData()105     LiveData<Integer> getErrorLiveData() {
106         return mErrorLiveData;
107     }
108 
109     /**
110      * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to
111      * {@link LiveData}.
112      */
saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds, UserHandle user)113     void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds, UserHandle user) {
114         mBgExecutor.execute(() -> {
115             // Render the screenshot bitmap in background.
116             Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds);
117 
118             // Export and save the screenshot in background.
119             ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
120                     mBgExecutor, UUID.randomUUID(), screenshotBitmap, user);
121 
122             // Get the result and update state on main thread.
123             exportFuture.addListener(() -> {
124                 try {
125                     ImageExporter.Result result = exportFuture.get();
126                     if (result.uri == null) {
127                         mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
128                         return;
129                     }
130 
131                     mResultLiveData.setValue(result.uri);
132                 } catch (CancellationException | InterruptedException | ExecutionException e) {
133                     mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
134                 }
135             }, mMainExecutor);
136         });
137     }
138 
renderBitmap(Drawable drawable, Rect bounds)139     private static Bitmap renderBitmap(Drawable drawable, Rect bounds) {
140         final RenderNode output = new RenderNode("Screenshot save");
141         output.setPosition(0, 0, bounds.width(), bounds.height());
142         RecordingCanvas canvas = output.beginRecording();
143         canvas.translate(-bounds.left, -bounds.top);
144         canvas.clipRect(bounds);
145         drawable.draw(canvas);
146         output.endRecording();
147         return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height());
148     }
149 
150     /** Helper factory to help with injecting {@link AppClipsViewModel}. */
151     static final class Factory implements ViewModelProvider.Factory {
152 
153         private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
154         private final ImageExporter mImageExporter;
155         @Main
156         private final Executor mMainExecutor;
157         @Background
158         private final Executor mBgExecutor;
159 
160         @Inject
Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, @Main Executor mainExecutor, @Background Executor bgExecutor)161         Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper,  ImageExporter imageExporter,
162                 @Main Executor mainExecutor, @Background Executor bgExecutor) {
163             mAppClipsCrossProcessHelper = appClipsCrossProcessHelper;
164             mImageExporter = imageExporter;
165             mMainExecutor = mainExecutor;
166             mBgExecutor = bgExecutor;
167         }
168 
169         @NonNull
170         @Override
create(@onNull Class<T> modelClass)171         public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
172             if (modelClass != AppClipsViewModel.class) {
173                 throw new IllegalArgumentException();
174             }
175 
176             //noinspection unchecked
177             return (T) new AppClipsViewModel(mAppClipsCrossProcessHelper, mImageExporter,
178                     mMainExecutor, mBgExecutor);
179         }
180     }
181 }
182