• 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.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