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