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