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