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.intentresolver; 18 19 import android.app.Activity; 20 import android.app.Application; 21 import android.content.Intent; 22 import android.content.IntentSender; 23 import android.os.Bundle; 24 import android.os.Handler; 25 import android.os.Parcel; 26 import android.os.ResultReceiver; 27 import android.util.Log; 28 29 import androidx.annotation.Nullable; 30 import androidx.annotation.UiThread; 31 import androidx.lifecycle.LiveData; 32 import androidx.lifecycle.MutableLiveData; 33 import androidx.lifecycle.ViewModel; 34 35 import com.android.intentresolver.chooser.TargetInfo; 36 37 import dagger.hilt.android.lifecycle.HiltViewModel; 38 39 import java.util.List; 40 import java.util.function.Consumer; 41 42 import javax.inject.Inject; 43 44 /** 45 * Helper class to manage Sharesheet's "refinement" flow, where callers supply a "refinement 46 * activity" that will be invoked when a target is selected, allowing the calling app to add 47 * additional extras and other refinements (subject to {@link Intent#filterEquals}), e.g., to 48 * convert the format of the payload, or lazy-download some data that was deferred in the original 49 * call). 50 */ 51 @HiltViewModel 52 @UiThread 53 public final class ChooserRefinementManager extends ViewModel { 54 private static final String TAG = "ChooserRefinement"; 55 56 @Nullable // Non-null only during an active refinement session. 57 private RefinementResultReceiver mRefinementResultReceiver; 58 59 private boolean mConfigurationChangeInProgress = false; 60 61 /** 62 * The types of selections that may be sent to refinement. 63 * 64 * The refinement flow results in a refined intent, but the interpretation of that intent 65 * depends on the type of selection that prompted the refinement. 66 */ 67 public enum RefinementType { 68 TARGET_INFO, // A normal (`TargetInfo`) target. 69 70 // System actions derived from the refined intent (from `ChooserActionFactory`). 71 COPY_ACTION, 72 EDIT_ACTION 73 } 74 75 /** 76 * A token for the completion of a refinement process that can be consumed exactly once. 77 */ 78 public static class RefinementCompletion { 79 private TargetInfo mTargetInfo; 80 private boolean mConsumed; 81 private final RefinementType mType; 82 83 @Nullable 84 private final TargetInfo mOriginalTargetInfo; 85 86 @Nullable 87 private final Intent mRefinedIntent; 88 RefinementCompletion( @ullable RefinementType type, @Nullable TargetInfo originalTargetInfo, @Nullable Intent refinedIntent)89 RefinementCompletion( 90 @Nullable RefinementType type, 91 @Nullable TargetInfo originalTargetInfo, 92 @Nullable Intent refinedIntent) { 93 mType = type; 94 mOriginalTargetInfo = originalTargetInfo; 95 mRefinedIntent = refinedIntent; 96 } 97 getType()98 public RefinementType getType() { 99 return mType; 100 } 101 102 @Nullable getOriginalTargetInfo()103 public TargetInfo getOriginalTargetInfo() { 104 return mOriginalTargetInfo; 105 } 106 107 /** 108 * @return The output of the completed refinement process. Null if the process was aborted 109 * or failed. 110 */ 111 @Nullable getRefinedIntent()112 public Intent getRefinedIntent() { 113 return mRefinedIntent; 114 } 115 116 /** 117 * Mark this event as consumed if it wasn't already. 118 * 119 * @return true if this had not already been consumed. 120 */ consume()121 public boolean consume() { 122 if (!mConsumed) { 123 mConsumed = true; 124 return true; 125 } 126 return false; 127 } 128 } 129 130 private MutableLiveData<RefinementCompletion> mRefinementCompletion = new MutableLiveData<>(); 131 132 @Inject ChooserRefinementManager()133 public ChooserRefinementManager() {} 134 getRefinementCompletion()135 public LiveData<RefinementCompletion> getRefinementCompletion() { 136 return mRefinementCompletion; 137 } 138 139 /** 140 * Delegate the user's {@code selectedTarget} to the refinement flow, if possible. 141 * @return true if the selection should wait for a now-started refinement flow, or false if it 142 * can proceed by the default (non-refinement) logic. 143 */ maybeHandleSelection( TargetInfo selectedTarget, IntentSender refinementIntentSender, Application application, Handler mainHandler)144 public boolean maybeHandleSelection( 145 TargetInfo selectedTarget, 146 IntentSender refinementIntentSender, 147 Application application, 148 Handler mainHandler) { 149 if (selectedTarget.isSuspended()) { 150 // We expect all launches to fail for this target, so don't make the user go through the 151 // refinement flow first. Besides, the default (non-refinement) handling displays a 152 // warning in this case and recovers the session; we won't be equipped to recover if 153 // problems only come up after refinement. 154 return false; 155 } 156 157 return maybeHandleSelection( 158 RefinementType.TARGET_INFO, 159 selectedTarget.getAllSourceIntents(), 160 selectedTarget, 161 refinementIntentSender, 162 application, 163 mainHandler); 164 } 165 166 /** 167 * Delegate the user's selection of targets (with one or more matching {@code sourceIntents} to 168 * the refinement flow, if possible. 169 * @return true if the selection should wait for a now-started refinement flow, or false if it 170 * can proceed by the default (non-refinement) logic. 171 */ maybeHandleSelection( RefinementType refinementType, List<Intent> sourceIntents, @Nullable TargetInfo originalTargetInfo, IntentSender refinementIntentSender, Application application, Handler mainHandler)172 public boolean maybeHandleSelection( 173 RefinementType refinementType, 174 List<Intent> sourceIntents, 175 @Nullable TargetInfo originalTargetInfo, 176 IntentSender refinementIntentSender, 177 Application application, 178 Handler mainHandler) { 179 // Our requests have a non-null `originalTargetInfo` in exactly the 180 // cases when `refinementType == TARGET_INFO`. 181 assert ((originalTargetInfo == null) == (refinementType == RefinementType.TARGET_INFO)); 182 183 if (refinementIntentSender == null) { 184 return false; 185 } 186 if (sourceIntents.isEmpty()) { 187 return false; 188 } 189 190 destroy(); // Terminate any prior sessions. 191 mRefinementResultReceiver = new RefinementResultReceiver( 192 refinementType, 193 refinedIntent -> { 194 destroy(); 195 mRefinementCompletion.setValue( 196 new RefinementCompletion( 197 refinementType, originalTargetInfo, refinedIntent)); 198 }, 199 () -> { 200 destroy(); 201 mRefinementCompletion.setValue( 202 new RefinementCompletion( 203 refinementType, originalTargetInfo, null)); 204 }, 205 mainHandler); 206 207 Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, sourceIntents); 208 try { 209 refinementIntentSender.sendIntent(application, 0, refinementRequest, null, null); 210 return true; 211 } catch (IntentSender.SendIntentException e) { 212 Log.e(TAG, "Refinement IntentSender failed to send", e); 213 } 214 return true; 215 } 216 217 /** ChooserActivity has stopped */ onActivityStop(boolean configurationChanging)218 public void onActivityStop(boolean configurationChanging) { 219 mConfigurationChangeInProgress = configurationChanging; 220 } 221 222 /** ChooserActivity has resumed */ onActivityResume()223 public void onActivityResume() { 224 if (mConfigurationChangeInProgress) { 225 mConfigurationChangeInProgress = false; 226 } else { 227 if (mRefinementResultReceiver != null) { 228 // This can happen if the refinement activity terminates without ever sending a 229 // response to our `ResultReceiver`. We're probably not prepared to return the user 230 // into a valid Chooser session, so we'll treat it as a cancellation instead. 231 Log.w(TAG, "Chooser resumed while awaiting refinement result; aborting"); 232 destroy(); 233 mRefinementCompletion.setValue(new RefinementCompletion(null, null, null)); 234 } 235 } 236 } 237 238 @Override onCleared()239 protected void onCleared() { 240 // App lifecycle over, time to clean up. 241 destroy(); 242 } 243 244 /** Clean up any ongoing refinement session. */ destroy()245 private void destroy() { 246 if (mRefinementResultReceiver != null) { 247 mRefinementResultReceiver.destroyReceiver(); 248 mRefinementResultReceiver = null; 249 } 250 } 251 makeRefinementRequest( RefinementResultReceiver resultReceiver, List<Intent> sourceIntents)252 private static Intent makeRefinementRequest( 253 RefinementResultReceiver resultReceiver, List<Intent> sourceIntents) { 254 final Intent fillIn = new Intent(); 255 fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0)); 256 final int sourceIntentCount = sourceIntents.size(); 257 if (sourceIntentCount > 1) { 258 fillIn.putExtra( 259 Intent.EXTRA_ALTERNATE_INTENTS, 260 sourceIntents 261 .subList(1, sourceIntentCount) 262 .toArray(new Intent[sourceIntentCount - 1])); 263 } 264 fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, resultReceiver.copyForSending()); 265 return fillIn; 266 } 267 268 private static class RefinementResultReceiver extends ResultReceiver { 269 private final RefinementType mType; 270 private final Consumer<Intent> mOnSelectionRefined; 271 private final Runnable mOnRefinementCancelled; 272 273 private boolean mDestroyed; 274 RefinementResultReceiver( RefinementType type, Consumer<Intent> onSelectionRefined, Runnable onRefinementCancelled, Handler handler)275 RefinementResultReceiver( 276 RefinementType type, 277 Consumer<Intent> onSelectionRefined, 278 Runnable onRefinementCancelled, 279 Handler handler) { 280 super(handler); 281 mType = type; 282 mOnSelectionRefined = onSelectionRefined; 283 mOnRefinementCancelled = onRefinementCancelled; 284 } 285 destroyReceiver()286 public void destroyReceiver() { 287 mDestroyed = true; 288 } 289 290 @Override onReceiveResult(int resultCode, Bundle resultData)291 protected void onReceiveResult(int resultCode, Bundle resultData) { 292 if (mDestroyed) { 293 Log.e(TAG, "Destroyed RefinementResultReceiver received a result"); 294 return; 295 } 296 297 destroyReceiver(); // This is the single callback we'll accept from this session. 298 299 Intent refinedResult = tryToExtractRefinedResult(resultCode, resultData); 300 if (refinedResult == null) { 301 mOnRefinementCancelled.run(); 302 } else { 303 mOnSelectionRefined.accept(refinedResult); 304 } 305 } 306 307 /** 308 * Apps can't load this class directly, so we need a regular ResultReceiver copy for 309 * sending. Obtain this by parceling and unparceling (one weird trick). 310 */ copyForSending()311 ResultReceiver copyForSending() { 312 Parcel parcel = Parcel.obtain(); 313 writeToParcel(parcel, 0); 314 parcel.setDataPosition(0); 315 ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel); 316 parcel.recycle(); 317 return receiverForSending; 318 } 319 320 /** 321 * Get the refinement from the result data, if possible, or log diagnostics and return null. 322 */ 323 @Nullable tryToExtractRefinedResult(int resultCode, Bundle resultData)324 private static Intent tryToExtractRefinedResult(int resultCode, Bundle resultData) { 325 if (Activity.RESULT_CANCELED == resultCode) { 326 Log.i(TAG, "Refinement canceled by caller"); 327 } else if (Activity.RESULT_OK != resultCode) { 328 Log.w(TAG, "Canceling refinement on unrecognized result code " + resultCode); 329 } else if (resultData == null) { 330 Log.e(TAG, "RefinementResultReceiver received null resultData; canceling"); 331 } else if (!(resultData.getParcelable(Intent.EXTRA_INTENT) instanceof Intent)) { 332 Log.e(TAG, "No valid Intent.EXTRA_INTENT in 'OK' refinement result data"); 333 } else { 334 return resultData.getParcelable(Intent.EXTRA_INTENT, Intent.class); 335 } 336 return null; 337 } 338 } 339 } 340