1 /*
2  * Copyright (C) 2019 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.chooser;
18 
19 import android.app.Activity;
20 import android.content.ComponentName;
21 import android.content.Intent;
22 import android.content.pm.ActivityInfo;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.ResolveInfo;
25 import android.os.Bundle;
26 import android.os.UserHandle;
27 
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 /**
35  * A TargetInfo plus additional information needed to render it (such as icon and label) and
36  * resolve it to an activity.
37  */
38 public class DisplayResolveInfo implements TargetInfo {
39     private final ResolveInfo mResolveInfo;
40     private volatile CharSequence mDisplayLabel;
41     private volatile CharSequence mExtendedInfo;
42     private final Intent mResolvedIntent;
43     private final List<Intent> mSourceIntents = new ArrayList<>();
44     private final boolean mIsSuspended;
45     private boolean mPinned = false;
46     private final IconHolder mDisplayIconHolder = new SettableIconHolder();
47 
48     /** Create a new {@code DisplayResolveInfo} instance. */
newDisplayResolveInfo( Intent originalIntent, ResolveInfo resolveInfo, @NonNull Intent resolvedIntent)49     public static DisplayResolveInfo newDisplayResolveInfo(
50             Intent originalIntent,
51             ResolveInfo resolveInfo,
52             @NonNull Intent resolvedIntent) {
53         return newDisplayResolveInfo(
54                 originalIntent,
55                 resolveInfo,
56                 /* displayLabel=*/ null,
57                 /* extendedInfo=*/ null,
58                 resolvedIntent);
59     }
60 
61     /** Create a new {@code DisplayResolveInfo} instance. */
newDisplayResolveInfo( Intent originalIntent, ResolveInfo resolveInfo, CharSequence displayLabel, CharSequence extendedInfo, @NonNull Intent resolvedIntent)62     public static DisplayResolveInfo newDisplayResolveInfo(
63             Intent originalIntent,
64             ResolveInfo resolveInfo,
65             CharSequence displayLabel,
66             CharSequence extendedInfo,
67             @NonNull Intent resolvedIntent) {
68         return new DisplayResolveInfo(
69                 originalIntent,
70                 resolveInfo,
71                 displayLabel,
72                 extendedInfo,
73                 resolvedIntent);
74     }
75 
DisplayResolveInfo( Intent originalIntent, ResolveInfo resolveInfo, CharSequence displayLabel, CharSequence extendedInfo, @NonNull Intent resolvedIntent)76     private DisplayResolveInfo(
77             Intent originalIntent,
78             ResolveInfo resolveInfo,
79             CharSequence displayLabel,
80             CharSequence extendedInfo,
81             @NonNull Intent resolvedIntent) {
82         mSourceIntents.add(originalIntent);
83         mResolveInfo = resolveInfo;
84         mDisplayLabel = displayLabel;
85         mExtendedInfo = extendedInfo;
86 
87         final ActivityInfo ai = mResolveInfo.activityInfo;
88         mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
89 
90         mResolvedIntent = createResolvedIntent(resolvedIntent, ai);
91     }
92 
DisplayResolveInfo( DisplayResolveInfo other, @Nullable Intent baseIntentToSend)93     private DisplayResolveInfo(
94             DisplayResolveInfo other,
95             @Nullable Intent baseIntentToSend) {
96         mSourceIntents.addAll(other.getAllSourceIntents());
97         mResolveInfo = other.mResolveInfo;
98         mIsSuspended = other.mIsSuspended;
99         mDisplayLabel = other.mDisplayLabel;
100         mExtendedInfo = other.mExtendedInfo;
101 
102         mResolvedIntent = createResolvedIntent(
103                 baseIntentToSend == null ? other.mResolvedIntent : baseIntentToSend,
104                 mResolveInfo.activityInfo);
105 
106         mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
107     }
108 
DisplayResolveInfo(DisplayResolveInfo other)109     protected DisplayResolveInfo(DisplayResolveInfo other) {
110         mSourceIntents.addAll(other.getAllSourceIntents());
111         mResolveInfo = other.mResolveInfo;
112         mIsSuspended = other.mIsSuspended;
113         mDisplayLabel = other.mDisplayLabel;
114         mExtendedInfo = other.mExtendedInfo;
115         mResolvedIntent = other.mResolvedIntent;
116 
117         mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
118     }
119 
createResolvedIntent(Intent resolvedIntent, ActivityInfo ai)120     private static Intent createResolvedIntent(Intent resolvedIntent, ActivityInfo ai) {
121         final Intent result = new Intent(resolvedIntent);
122         result.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
123                 | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
124         result.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
125         return result;
126     }
127 
128     @Override
isDisplayResolveInfo()129     public final boolean isDisplayResolveInfo() {
130         return true;
131     }
132 
getResolveInfo()133     public ResolveInfo getResolveInfo() {
134         return mResolveInfo;
135     }
136 
getDisplayLabel()137     public CharSequence getDisplayLabel() {
138         return mDisplayLabel;
139     }
140 
hasDisplayLabel()141     public boolean hasDisplayLabel() {
142         return mDisplayLabel != null;
143     }
144 
setDisplayLabel(CharSequence displayLabel)145     public void setDisplayLabel(CharSequence displayLabel) {
146         mDisplayLabel = displayLabel;
147     }
148 
setExtendedInfo(CharSequence extendedInfo)149     public void setExtendedInfo(CharSequence extendedInfo) {
150         mExtendedInfo = extendedInfo;
151     }
152 
153     @Override
getDisplayIconHolder()154     public IconHolder getDisplayIconHolder() {
155         return mDisplayIconHolder;
156     }
157 
158     @Override
159     @Nullable
tryToCloneWithAppliedRefinement(Intent proposedRefinement)160     public DisplayResolveInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
161         Intent matchingBase =
162                 getAllSourceIntents()
163                         .stream()
164                         .filter(i -> i.filterEquals(proposedRefinement))
165                         .findFirst()
166                         .orElse(null);
167         if (matchingBase == null) {
168             return null;
169         }
170 
171         return new DisplayResolveInfo(
172                 this,
173                 TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement));
174     }
175 
176     @Override
getAllSourceIntents()177     public List<Intent> getAllSourceIntents() {
178         return mSourceIntents;
179     }
180 
181     @Override
getAllDisplayTargets()182     public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
183         return new ArrayList<>(List.of(this));
184     }
185 
addAlternateSourceIntent(Intent alt)186     public void addAlternateSourceIntent(Intent alt) {
187         mSourceIntents.add(alt);
188     }
189 
getExtendedInfo()190     public CharSequence getExtendedInfo() {
191         return mExtendedInfo;
192     }
193 
getResolvedIntent()194     public Intent getResolvedIntent() {
195         return mResolvedIntent;
196     }
197 
198     @Override
199     @NonNull
getResolvedComponentName()200     public ComponentName getResolvedComponentName() {
201         return new ComponentName(mResolveInfo.activityInfo.packageName,
202                 mResolveInfo.activityInfo.name);
203     }
204 
205     @Override
startAsCaller(Activity activity, Bundle options, int userId)206     public boolean startAsCaller(Activity activity, Bundle options, int userId) {
207         TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
208         activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
209         return true;
210     }
211 
212     @Override
startAsUser(Activity activity, Bundle options, UserHandle user)213     public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
214         TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
215         // TODO: is this equivalent to `startActivityAsCaller` with `ignoreTargetSecurity=true`? If
216         // so, we can consolidate on the one API method to show that this flag is the only
217         // distinction between `startAsCaller` and `startAsUser`. We can even bake that flag into
218         // the `TargetActivityStarter` upfront since it just reflects our "safe forwarding mode" --
219         // which is constant for the duration of our lifecycle, leaving clients no other
220         // responsibilities in this logic.
221         activity.startActivityAsUser(mResolvedIntent, options, user);
222         return false;
223     }
224 
225     @Override
getTargetIntent()226     public Intent getTargetIntent() {
227         return mResolvedIntent;
228     }
229 
isSuspended()230     public boolean isSuspended() {
231         return mIsSuspended;
232     }
233 
234     @Override
isPinned()235     public boolean isPinned() {
236         return mPinned;
237     }
238 
setPinned(boolean pinned)239     public void setPinned(boolean pinned) {
240         mPinned = pinned;
241     }
242 }
243