• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.ActivityTaskManager.INVALID_TASK_ID;
20 
21 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_ACCEPTED;
22 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_CANCELLED;
23 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
24 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME;
25 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_TASK_ID;
26 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CLIP_DATA;
27 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER;
28 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
29 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.PERMISSION_SELF;
30 
31 import android.app.Activity;
32 import android.content.BroadcastReceiver;
33 import android.content.ClipData;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.pm.PackageManager;
38 import android.content.pm.PackageManager.ApplicationInfoFlags;
39 import android.content.pm.PackageManager.NameNotFoundException;
40 import android.graphics.Bitmap;
41 import android.graphics.Rect;
42 import android.graphics.drawable.BitmapDrawable;
43 import android.graphics.drawable.Drawable;
44 import android.net.Uri;
45 import android.os.Bundle;
46 import android.os.ResultReceiver;
47 import android.util.Log;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.widget.ArrayAdapter;
51 import android.widget.Button;
52 import android.widget.CheckBox;
53 import android.widget.ImageView;
54 import android.widget.ListPopupWindow;
55 import android.widget.TextView;
56 
57 import androidx.activity.ComponentActivity;
58 import androidx.annotation.Nullable;
59 import androidx.appcompat.content.res.AppCompatResources;
60 import androidx.core.graphics.Insets;
61 import androidx.core.view.ViewCompat;
62 import androidx.core.view.WindowInsetsCompat;
63 import androidx.lifecycle.ViewModelProvider;
64 
65 import com.android.internal.logging.UiEventLogger;
66 import com.android.internal.logging.UiEventLogger.UiEventEnum;
67 import com.android.settingslib.Utils;
68 import com.android.systemui.Flags;
69 import com.android.systemui.log.DebugLogger;
70 import com.android.systemui.res.R;
71 import com.android.systemui.screenshot.appclips.InternalBacklinksData.BacklinksData;
72 import com.android.systemui.screenshot.appclips.InternalBacklinksData.CrossProfileError;
73 import com.android.systemui.screenshot.scroll.CropView;
74 import com.android.systemui.settings.UserTracker;
75 
76 import java.util.HashMap;
77 import java.util.List;
78 import java.util.Map;
79 import java.util.Set;
80 
81 import javax.inject.Inject;
82 
83 /**
84  * An {@link Activity} to take a screenshot for the App Clips flow and presenting a screenshot
85  * editing tool.
86  *
87  * <p>An App Clips flow includes:
88  * <ul>
89  *     <li>Checking if calling activity meets the prerequisites. This is done by
90  *     {@link AppClipsTrampolineActivity}.
91  *     <li>Performing the screenshot.
92  *     <li>Showing a screenshot editing tool.
93  *     <li>Returning the screenshot to the {@link AppClipsTrampolineActivity} so that it can return
94  *     the screenshot to the calling activity after explicit user consent.
95  * </ul>
96  *
97  * <p>This {@link Activity} runs in its own separate process to isolate memory intensive image
98  * editing from SysUI process.
99  */
100 public class AppClipsActivity extends ComponentActivity {
101 
102     private static final String TAG = AppClipsActivity.class.getSimpleName();
103     private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);
104     private static final int DRAWABLE_END = 2;
105     private static final float DISABLE_ALPHA = 0.5f;
106 
107     private final AppClipsViewModel.Factory mViewModelFactory;
108     private final PackageManager mPackageManager;
109     private final UserTracker mUserTracker;
110     private final UiEventLogger mUiEventLogger;
111     private final BroadcastReceiver mBroadcastReceiver;
112     private final IntentFilter mIntentFilter;
113 
114     private View mLayout;
115     private View mRoot;
116     private ImageView mPreview;
117     private CropView mCropView;
118     private Button mSave;
119     private Button mCancel;
120     private CheckBox mBacklinksIncludeDataCheckBox;
121     private TextView mBacklinksDataTextView;
122     private TextView mBacklinksCrossProfileError;
123     private AppClipsViewModel mViewModel;
124 
125     private ResultReceiver mResultReceiver;
126     @Nullable
127     private String mCallingPackageName;
128     private int mCallingPackageUid;
129 
130     @Inject
AppClipsActivity(AppClipsViewModel.Factory viewModelFactory, PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger)131     public AppClipsActivity(AppClipsViewModel.Factory viewModelFactory,
132             PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger) {
133         mViewModelFactory = viewModelFactory;
134         mPackageManager = packageManager;
135         mUserTracker = userTracker;
136         mUiEventLogger = uiEventLogger;
137 
138         mBroadcastReceiver = new BroadcastReceiver() {
139             @Override
140             public void onReceive(Context context, Intent intent) {
141                 // Trampoline activity was dismissed so finish this activity.
142                 if (ACTION_FINISH_FROM_TRAMPOLINE.equals(intent.getAction())) {
143                     if (!isFinishing()) {
144                         // Nullify the ResultReceiver so that result cannot be sent as trampoline
145                         // activity is already finishing.
146                         mResultReceiver = null;
147                         finish();
148                     }
149                 }
150             }
151         };
152 
153         mIntentFilter = new IntentFilter(ACTION_FINISH_FROM_TRAMPOLINE);
154     }
155 
156     @Override
onCreate(Bundle savedInstanceState)157     public void onCreate(Bundle savedInstanceState) {
158         overridePendingTransition(0, 0);
159         super.onCreate(savedInstanceState);
160 
161         // Register the broadcast receiver that informs when the trampoline activity is dismissed.
162         registerReceiver(mBroadcastReceiver, mIntentFilter, PERMISSION_SELF, null,
163                 RECEIVER_NOT_EXPORTED);
164 
165         Intent intent = getIntent();
166         setUpUiLogging(intent);
167         mResultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER, ResultReceiver.class);
168         if (mResultReceiver == null) {
169             setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
170             return;
171         }
172 
173         // Inflate layout but don't add it yet as it should be added after the screenshot is ready
174         // for preview.
175         mLayout = getLayoutInflater().inflate(R.layout.app_clips_screenshot, null);
176         mRoot = mLayout.findViewById(R.id.root);
177 
178         // Manually handle window insets post Android V to support edge-to-edge display.
179         ViewCompat.setOnApplyWindowInsetsListener(mRoot, (v, windowInsets) -> {
180             Insets insets = windowInsets.getInsets(
181                     WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
182             v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
183             return WindowInsetsCompat.CONSUMED;
184         });
185 
186         mSave = mLayout.findViewById(R.id.save);
187         mCancel = mLayout.findViewById(R.id.cancel);
188         mSave.setOnClickListener(this::onClick);
189         mCancel.setOnClickListener(this::onClick);
190         mCropView = mLayout.findViewById(R.id.crop_view);
191         mPreview = mLayout.findViewById(R.id.preview);
192         mPreview.addOnLayoutChangeListener(
193                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
194                         updateImageDimensions());
195 
196         mBacklinksDataTextView = mLayout.findViewById(R.id.backlinks_data);
197         mBacklinksIncludeDataCheckBox = mLayout.findViewById(R.id.backlinks_include_data);
198         mBacklinksIncludeDataCheckBox.setOnCheckedChangeListener(
199                 this::backlinksIncludeDataCheckBoxCheckedChangeListener);
200         mBacklinksCrossProfileError = mLayout.findViewById(R.id.backlinks_cross_profile_error);
201 
202         mViewModel = new ViewModelProvider(this, mViewModelFactory).get(AppClipsViewModel.class);
203         mViewModel.getScreenshot().observe(this, this::setScreenshot);
204         mViewModel.getResultLiveData().observe(this, this::setResultThenFinish);
205         mViewModel.getErrorLiveData().observe(this, this::setErrorThenFinish);
206         mViewModel.getBacklinksLiveData().observe(this, this::setBacklinksData);
207         mViewModel.mSelectedBacklinksLiveData.observe(this, this::updateBacklinksTextView);
208 
209         if (savedInstanceState == null) {
210             int displayId = getDisplayId();
211             mViewModel.performScreenshot(displayId);
212 
213             if (Flags.appClipsBacklinks()) {
214                 int appClipsTaskId = getTaskId();
215                 int callingPackageTaskId = intent.getIntExtra(EXTRA_CALLING_PACKAGE_TASK_ID,
216                         INVALID_TASK_ID);
217                 Set<Integer> taskIdsToIgnore = Set.of(appClipsTaskId, callingPackageTaskId);
218                 mViewModel.triggerBacklinks(taskIdsToIgnore, displayId);
219             }
220         }
221     }
222 
223     @Override
finish()224     public void finish() {
225         super.finish();
226         overridePendingTransition(0, 0);
227     }
228 
229     @Override
onDestroy()230     protected void onDestroy() {
231         super.onDestroy();
232 
233         unregisterReceiver(mBroadcastReceiver);
234 
235         // If neither error nor result was set, it implies that the activity is finishing due to
236         // some other reason such as user dismissing this activity using back gesture. Inform error.
237         if (isFinishing() && mViewModel.getErrorLiveData().getValue() == null
238                 && mViewModel.getResultLiveData().getValue() == null) {
239             // Set error but don't finish as the activity is already finishing.
240             setError(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
241         }
242     }
243 
setUpUiLogging(Intent intent)244     private void setUpUiLogging(Intent intent) {
245         mCallingPackageName = intent.getStringExtra(EXTRA_CALLING_PACKAGE_NAME);
246         mCallingPackageUid = 0;
247         try {
248             mCallingPackageUid = mPackageManager.getApplicationInfoAsUser(mCallingPackageName,
249                     APPLICATION_INFO_FLAGS, mUserTracker.getUserId()).uid;
250         } catch (NameNotFoundException e) {
251             Log.d(TAG, "Couldn't find notes app UID " + e);
252         }
253     }
254 
setScreenshot(Bitmap screenshot)255     private void setScreenshot(Bitmap screenshot) {
256         // Set background, status and navigation bar colors as the activity is no longer
257         // translucent.
258         int colorBackgroundFloating = Utils.getColorAttr(this,
259                 android.R.attr.colorBackgroundFloating).getDefaultColor();
260         mRoot.setBackgroundColor(colorBackgroundFloating);
261 
262         BitmapDrawable drawable = new BitmapDrawable(getResources(), screenshot);
263         mPreview.setImageDrawable(drawable);
264         mPreview.setAlpha(1f);
265 
266         // Screenshot is now available so set content view.
267         setContentView(mLayout);
268 
269         // Request view to apply insets as it is added late and not when activity was first created.
270         mRoot.requestApplyInsets();
271     }
272 
onClick(View view)273     private void onClick(View view) {
274         mSave.setEnabled(false);
275         mCancel.setEnabled(false);
276 
277         int id = view.getId();
278         if (id == R.id.save) {
279             saveScreenshotThenFinish();
280         } else {
281             setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
282         }
283     }
284 
saveScreenshotThenFinish()285     private void saveScreenshotThenFinish() {
286         Drawable drawable = mPreview.getDrawable();
287         if (drawable == null) {
288             setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
289             return;
290         }
291 
292         Rect bounds = mCropView.getCropBoundaries(drawable.getIntrinsicWidth(),
293                 drawable.getIntrinsicHeight());
294 
295         if (bounds.isEmpty()) {
296             setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
297             return;
298         }
299 
300         updateImageDimensions();
301         mViewModel.saveScreenshotThenFinish(drawable, bounds, getUser());
302     }
303 
setResultThenFinish(Uri uri)304     private void setResultThenFinish(Uri uri) {
305         if (mResultReceiver == null) {
306             return;
307         }
308 
309         // Grant permission here instead of in the trampoline activity because this activity can run
310         // as work profile user so the URI can belong to the work profile user while the trampoline
311         // activity always runs as main user.
312         grantUriPermission(mCallingPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
313 
314         Bundle data = new Bundle();
315         data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
316                 Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
317         data.putParcelable(EXTRA_SCREENSHOT_URI, uri);
318 
319         InternalBacklinksData selectedBacklink = mViewModel.mSelectedBacklinksLiveData.getValue();
320         if (mBacklinksIncludeDataCheckBox.getVisibility() == View.VISIBLE
321                 && mBacklinksIncludeDataCheckBox.isChecked()
322                 && selectedBacklink instanceof BacklinksData) {
323             ClipData backlinksData = ((BacklinksData) selectedBacklink).getClipData();
324             data.putParcelable(EXTRA_CLIP_DATA, backlinksData);
325 
326             DebugLogger.INSTANCE.logcatMessage(this,
327                     () -> "setResultThenFinish: sending notes app ClipData");
328         }
329 
330         try {
331             mResultReceiver.send(Activity.RESULT_OK, data);
332             logUiEvent(SCREENSHOT_FOR_NOTE_ACCEPTED);
333         } catch (Exception e) {
334             Log.e(TAG, "Error while sending data to trampoline activity", e);
335         }
336 
337         // Nullify the ResultReceiver before finishing to avoid resending the result.
338         mResultReceiver = null;
339         finish();
340     }
341 
setErrorThenFinish(int errorCode)342     private void setErrorThenFinish(int errorCode) {
343         setError(errorCode);
344         finish();
345     }
346 
setBacklinksData(List<InternalBacklinksData> backlinksData)347     private void setBacklinksData(List<InternalBacklinksData> backlinksData) {
348         mBacklinksIncludeDataCheckBox.setVisibility(View.VISIBLE);
349         mBacklinksDataTextView.setVisibility(
350                 mBacklinksIncludeDataCheckBox.isChecked() ? View.VISIBLE : View.GONE);
351 
352         // Set up the dropdown when multiple backlinks are available.
353         if (backlinksData.size() > 1) {
354             setUpListPopupWindow(updateBacklinkLabelsWithDuplicateNames(backlinksData),
355                     mBacklinksDataTextView);
356         }
357     }
358 
359     /**
360      * If there are more than 1 backlinks that have the same app name, then this method appends
361      * a numerical suffix to such backlinks to help users distinguish.
362      */
updateBacklinkLabelsWithDuplicateNames( List<InternalBacklinksData> backlinksData)363     private List<InternalBacklinksData> updateBacklinkLabelsWithDuplicateNames(
364             List<InternalBacklinksData> backlinksData) {
365         // Check if there are multiple backlinks with same name.
366         Map<String, Integer> duplicateNamedBacklinksCountMap = new HashMap<>();
367         for (InternalBacklinksData data : backlinksData) {
368             if (duplicateNamedBacklinksCountMap.containsKey(data.getDisplayLabel())) {
369                 int duplicateCount = duplicateNamedBacklinksCountMap.get(data.getDisplayLabel());
370                 if (duplicateCount == 0) {
371                     // If this is the first time the loop is coming across a duplicate name, set the
372                     // count to 2. This way the count starts from 1 for all duplicate named
373                     // backlinks.
374                     duplicateNamedBacklinksCountMap.put(data.getDisplayLabel(), 2);
375                 } else {
376                     // For all duplicate named backlinks, increase the duplicate count by 1.
377                     duplicateNamedBacklinksCountMap.put(data.getDisplayLabel(), duplicateCount + 1);
378                 }
379             } else {
380                 // This is the first time the loop is coming across a backlink with this name. Set
381                 // its count to 0. The loop will increase its count by 1 when a duplicate is found.
382                 duplicateNamedBacklinksCountMap.put(data.getDisplayLabel(), 0);
383             }
384         }
385 
386         // Go through the backlinks in reverse order as it is easier to assign the numerical suffix
387         // in descending order of frequency using the duplicate map that was built earlier. For
388         // example, if "App A" is present 3 times, then we assign display label "App A (3)" first
389         // and then "App A (2)", lastly "App A (1)".
390         for (InternalBacklinksData data : backlinksData.reversed()) {
391             String originalBacklinkLabel = data.getDisplayLabel();
392             int duplicateCount = duplicateNamedBacklinksCountMap.get(originalBacklinkLabel);
393 
394             // The display label should only be updated if there are multiple backlinks with the
395             // same name.
396             if (duplicateCount > 0) {
397                 // Update the display label to: "App name (count)"
398                 data.setDisplayLabel(
399                         getString(R.string.backlinks_duplicate_label_format, originalBacklinkLabel,
400                                 duplicateCount));
401 
402                 // Decrease the duplicate count and update the map.
403                 duplicateCount--;
404                 duplicateNamedBacklinksCountMap.put(originalBacklinkLabel, duplicateCount);
405             }
406         }
407 
408         return backlinksData;
409     }
410 
setUpListPopupWindow(List<InternalBacklinksData> backlinksData, View anchor)411     private void setUpListPopupWindow(List<InternalBacklinksData> backlinksData, View anchor) {
412         ListPopupWindow listPopupWindow = new ListPopupWindow(this);
413         listPopupWindow.setAnchorView(anchor);
414         listPopupWindow.setOverlapAnchor(true);
415         listPopupWindow.setBackgroundDrawable(
416                 AppCompatResources.getDrawable(this, R.drawable.backlinks_rounded_rectangle));
417         listPopupWindow.setOnItemClickListener((parent, view, position, id) -> {
418             mViewModel.mSelectedBacklinksLiveData.setValue(backlinksData.get(position));
419             listPopupWindow.dismiss();
420         });
421 
422         ArrayAdapter<InternalBacklinksData> adapter = new ArrayAdapter<>(this,
423                 R.layout.app_clips_backlinks_drop_down_entry) {
424             @Override
425             public View getView(int position, @Nullable View convertView, ViewGroup parent) {
426                 TextView itemView = (TextView) super.getView(position, convertView, parent);
427                 InternalBacklinksData data = backlinksData.get(position);
428                 itemView.setText(data.getDisplayLabel());
429 
430                 Drawable icon = data.getAppIcon();
431                 icon.setBounds(createBacklinksTextViewDrawableBounds());
432                 itemView.setCompoundDrawablesRelative(/* start= */ icon, /* top= */ null,
433                         /* end= */ null, /* bottom= */ null);
434 
435                 return itemView;
436             }
437         };
438         adapter.addAll(backlinksData);
439         listPopupWindow.setAdapter(adapter);
440 
441         mBacklinksDataTextView.setOnClickListener(unused -> listPopupWindow.show());
442     }
443 
444     /**
445      * Updates the {@link #mBacklinksDataTextView} with the currently selected
446      * {@link InternalBacklinksData}. The {@link AppClipsViewModel#getBacklinksLiveData()} is
447      * expected to be already set when this method is called.
448      */
updateBacklinksTextView(InternalBacklinksData backlinksData)449     private void updateBacklinksTextView(InternalBacklinksData backlinksData) {
450         mBacklinksDataTextView.setText(backlinksData.getDisplayLabel());
451         Drawable appIcon = backlinksData.getAppIcon();
452         Rect compoundDrawableBounds = createBacklinksTextViewDrawableBounds();
453         appIcon.setBounds(compoundDrawableBounds);
454 
455         // Try to reuse the dropdown down arrow icon if available, will be null if never set.
456         Drawable dropDownIcon = mBacklinksDataTextView.getCompoundDrawablesRelative()[DRAWABLE_END];
457         if (mViewModel.getBacklinksLiveData().getValue().size() > 1 && dropDownIcon == null) {
458             // Set up the dropdown down arrow drawable only if it is required.
459             dropDownIcon = AppCompatResources.getDrawable(this, R.drawable.arrow_pointing_down);
460             dropDownIcon.setBounds(compoundDrawableBounds);
461             dropDownIcon.setTint(Utils.getColorAttr(this,
462                     android.R.attr.textColorSecondary).getDefaultColor());
463         }
464 
465         mBacklinksDataTextView.setCompoundDrawablesRelative(/* start= */ appIcon, /* top= */
466                 null, /* end= */ dropDownIcon, /* bottom= */ null);
467 
468         updateViewsToShowOrHideBacklinkError(backlinksData);
469     }
470 
471     /** Updates views to show or hide error with backlink.  */
updateViewsToShowOrHideBacklinkError(InternalBacklinksData backlinksData)472     private void updateViewsToShowOrHideBacklinkError(InternalBacklinksData backlinksData) {
473         // Remove the check box change listener before updating it to avoid updating backlink text
474         // view visibility.
475         mBacklinksIncludeDataCheckBox.setOnCheckedChangeListener(null);
476         if (backlinksData instanceof CrossProfileError) {
477             // There's error with the backlink, unselect the checkbox and disable it.
478             mBacklinksIncludeDataCheckBox.setEnabled(false);
479             mBacklinksIncludeDataCheckBox.setChecked(false);
480             mBacklinksIncludeDataCheckBox.setAlpha(DISABLE_ALPHA);
481 
482             mBacklinksCrossProfileError.setVisibility(View.VISIBLE);
483         } else {
484             // When there is no error, ensure the check box is enabled and checked.
485             mBacklinksIncludeDataCheckBox.setEnabled(true);
486             mBacklinksIncludeDataCheckBox.setChecked(true);
487             mBacklinksIncludeDataCheckBox.setAlpha(1.0f);
488 
489             mBacklinksCrossProfileError.setVisibility(View.GONE);
490         }
491 
492         // (Re)Set the check box change listener as we're done making changes to the check box.
493         mBacklinksIncludeDataCheckBox.setOnCheckedChangeListener(
494                 this::backlinksIncludeDataCheckBoxCheckedChangeListener);
495     }
496 
backlinksIncludeDataCheckBoxCheckedChangeListener(View unused, boolean isChecked)497     private void backlinksIncludeDataCheckBoxCheckedChangeListener(View unused, boolean isChecked) {
498         mBacklinksDataTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
499     }
500 
createBacklinksTextViewDrawableBounds()501     private Rect createBacklinksTextViewDrawableBounds() {
502         int size = getResources().getDimensionPixelSize(R.dimen.appclips_backlinks_icon_size);
503         Rect bounds = new Rect();
504         bounds.set(/* left= */ 0, /* top= */ 0, /* right= */ size, /* bottom= */ size);
505         return bounds;
506     }
507 
setError(int errorCode)508     private void setError(int errorCode) {
509         if (mResultReceiver == null) {
510             return;
511         }
512 
513         Bundle data = new Bundle();
514         data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode);
515         try {
516             mResultReceiver.send(RESULT_OK, data);
517             if (errorCode == Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED) {
518                 logUiEvent(SCREENSHOT_FOR_NOTE_CANCELLED);
519             }
520         } catch (Exception e) {
521             // Do nothing.
522             Log.e(TAG, "Error while sending trampoline activity error code: " + errorCode, e);
523         }
524 
525         // Nullify the ResultReceiver to avoid resending the result.
526         mResultReceiver = null;
527     }
528 
logUiEvent(UiEventEnum uiEvent)529     private void logUiEvent(UiEventEnum uiEvent) {
530         mUiEventLogger.log(uiEvent, mCallingPackageUid, mCallingPackageName);
531     }
532 
updateImageDimensions()533     private void updateImageDimensions() {
534         Drawable drawable = mPreview.getDrawable();
535         if (drawable == null) {
536             return;
537         }
538 
539         Rect bounds = drawable.getBounds();
540         float imageRatio = bounds.width() / (float) bounds.height();
541         int previewWidth = mPreview.getWidth() - mPreview.getPaddingLeft()
542                 - mPreview.getPaddingRight();
543         int previewHeight = mPreview.getHeight() - mPreview.getPaddingTop()
544                 - mPreview.getPaddingBottom();
545         float viewRatio = previewWidth / (float) previewHeight;
546 
547         if (imageRatio > viewRatio) {
548             // Image is full width and height is constrained, compute extra padding to inform
549             // CropView.
550             int imageHeight = (int) (previewHeight * viewRatio / imageRatio);
551             int extraPadding = (previewHeight - imageHeight) / 2;
552             mCropView.setExtraPadding(extraPadding, extraPadding);
553             mCropView.setImageWidth(previewWidth);
554         } else {
555             // Image is full height.
556             mCropView.setExtraPadding(mPreview.getPaddingTop(), mPreview.getPaddingBottom());
557             mCropView.setImageWidth((int) (previewHeight * imageRatio));
558         }
559     }
560 }
561