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