summaryrefslogtreecommitdiff
path: root/service/java/com/android/safetycenter/PendingIntentFactory.java
blob: 4f54e0b82220fe26b72a5dd8fa073b2658c6818c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.safetycenter;

import static android.os.Build.VERSION_CODES.TIRAMISU;

import static java.util.Objects.requireNonNull;

import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ResolveInfoFlags;
import android.content.pm.ResolveInfo;
import android.os.Binder;
import android.os.UserHandle;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.android.safetycenter.resources.SafetyCenterResourcesContext;

import java.util.Arrays;

/**
 * Helps build or retrieve {@link PendingIntent} instances.
 *
 * @hide
 */
@RequiresApi(TIRAMISU)
public final class PendingIntentFactory {

    private static final String TAG = "PendingIntentFactory";

    private static final int DEFAULT_REQUEST_CODE = 0;

    private static final String IS_SETTINGS_HOMEPAGE = "is_from_settings_homepage";

    private final Context mContext;
    private final SafetyCenterResourcesContext mSafetyCenterResourcesContext;

    PendingIntentFactory(
            Context context, SafetyCenterResourcesContext safetyCenterResourcesContext) {
        mContext = context;
        mSafetyCenterResourcesContext = safetyCenterResourcesContext;
    }

    /**
     * Creates or retrieves a {@link PendingIntent} that will start a new {@code Activity} matching
     * the given {@code intentAction}.
     *
     * <p>If the given {@code intentAction} resolves for the given {@code packageName}, the {@link
     * PendingIntent} will explicitly target the {@code packageName}. If the {@code intentAction}
     * resolves elsewhere, the {@link PendingIntent} will be implicit.
     *
     * <p>The {@code PendingIntent} is associated with a specific source given by {@code sourceId}.
     *
     * <p>Returns {@code null} if the required {@link PendingIntent} cannot be created or if there
     * is no valid target for the given {@code intentAction}.
     */
    @Nullable
    PendingIntent getPendingIntent(
            String sourceId,
            @Nullable String intentAction,
            String packageName,
            @UserIdInt int userId,
            boolean isQuietModeEnabled) {
        if (intentAction == null) {
            return null;
        }
        Context packageContext = createPackageContextAsUser(mContext, packageName, userId);
        if (packageContext == null) {
            return null;
        }
        Intent intent = createIntent(packageContext, sourceId, intentAction, isQuietModeEnabled);
        if (intent == null) {
            return null;
        }
        return getActivityPendingIntent(
                packageContext, DEFAULT_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE);
    }

    @Nullable
    private Intent createIntent(
            Context packageContext,
            String sourceId,
            String intentAction,
            boolean isQuietModeEnabled) {
        Intent intent = new Intent(intentAction);

        if (shouldAddSettingsHomepageExtra(sourceId)) {
            // Identify this intent as coming from Settings. Because this intent is actually coming
            // from Safety Center, which is served by PermissionController, this is useful to
            // indicate that it is presented as part of the Settings app.
            //
            // In particular, the AOSP Settings app uses this to ensure that two-pane mode works
            // correctly.
            intent.putExtra(IS_SETTINGS_HOMEPAGE, true);
            // Given we've added an extra to this intent, set an ID on it to ensure that it is not
            // considered equal to the same intent without the extra. PendingIntents are cached
            // using Intent equality as the key, and we want to make sure the extra is propagated.
            intent.setIdentifier("with_settings_homepage_extra");
        }

        // If the intent resolves for the package provided, then we make the assumption that it is
        // the desired app and make the intent explicit. This is to workaround implicit internal
        // intents that may not be exported which will stop working on Android U+.
        // This assumes that the source or the caller has the highest priority to resolve the intent
        // action.
        Intent explicitIntent = new Intent(intent).setPackage(packageContext.getPackageName());
        if (intentResolves(packageContext, explicitIntent)) {
            return explicitIntent;
        }

        if (intentResolves(packageContext, intent)) {
            return intent;
        }

        // resolveActivity does not return any activity when the work profile is in quiet mode, even
        // though it opens the quiet mode dialog and/or the original intent would otherwise resolve
        // when quiet mode is turned off. So, we assume that the explicit intent will always resolve
        // to this dialog. This heuristic is preferable on U+ as it has a higher chance of resolving
        // once the work profile is enabled considering the implicit internal intent restriction.
        if (isQuietModeEnabled) {
            return explicitIntent;
        }

        return null;
    }

    private boolean shouldAddSettingsHomepageExtra(String sourceId) {
        return Arrays.asList(
                        mSafetyCenterResourcesContext
                                .getStringByName("config_useSettingsHomepageIntentExtra")
                                .split(","))
                .contains(sourceId);
    }

    private static boolean intentResolves(Context packageContext, Intent intent) {
        return resolveActivity(packageContext, intent) != null;
    }

    @Nullable
    private static ResolveInfo resolveActivity(Context packageContext, Intent intent) {
        PackageManager packageManager = packageContext.getPackageManager();
        // This call requires the INTERACT_ACROSS_USERS permission as the `packageContext` could
        // belong to another user.
        final long callingId = Binder.clearCallingIdentity();
        try {
            return packageManager.resolveActivity(intent, ResolveInfoFlags.of(0));
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }

    /**
     * Creates a {@link PendingIntent} to start an Activity from the given {@code packageContext}.
     *
     * <p>This function can only return {@code null} if the {@link PendingIntent#FLAG_NO_CREATE}
     * flag is passed in.
     */
    @Nullable
    public static PendingIntent getNullableActivityPendingIntent(
            Context packageContext, int requestCode, Intent intent, int flags) {
        // This call requires Binder identity to be cleared for getIntentSender() to be allowed to
        // send as another package.
        final long callingId = Binder.clearCallingIdentity();
        try {
            return PendingIntent.getActivity(packageContext, requestCode, intent, flags);
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }

    /**
     * Creates a {@link PendingIntent} to start an Activity from the given {@code packageContext}.
     *
     * <p>{@code flags} must not include {@link PendingIntent#FLAG_NO_CREATE}
     */
    public static PendingIntent getActivityPendingIntent(
            Context packageContext, int requestCode, Intent intent, int flags) {
        if ((flags & PendingIntent.FLAG_NO_CREATE) != 0) {
            throw new IllegalArgumentException("flags must not include FLAG_NO_CREATE");
        }
        return requireNonNull(
                getNullableActivityPendingIntent(packageContext, requestCode, intent, flags));
    }

    /**
     * Creates a non-protected broadcast {@link PendingIntent} which can only be received by the
     * system. Use this method to create PendingIntents to be received by Context-registered
     * receivers, for example for notification-related callbacks.
     *
     * <p>{@code flags} must include {@link PendingIntent#FLAG_IMMUTABLE} and must not include
     * {@link PendingIntent#FLAG_NO_CREATE}
     */
    public static PendingIntent getNonProtectedSystemOnlyBroadcastPendingIntent(
            Context context, int requestCode, Intent intent, int flags) {
        if ((flags & PendingIntent.FLAG_IMMUTABLE) == 0) {
            throw new IllegalArgumentException("flags must include FLAG_IMMUTABLE");
        }
        if ((flags & PendingIntent.FLAG_NO_CREATE) != 0) {
            throw new IllegalArgumentException("flags must not include FLAG_NO_CREATE");
        }
        intent.setPackage("android");
        // This call is needed to be allowed to send the broadcast as the "android" package.
        final long callingId = Binder.clearCallingIdentity();
        try {
            return PendingIntent.getBroadcast(context, requestCode, intent, flags);
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }

    /** Creates a {@link Context} for the given {@code packageName} and {@code userId}. */
    @Nullable
    public static Context createPackageContextAsUser(
            Context context, String packageName, @UserIdInt int userId) {
        // This call requires the INTERACT_ACROSS_USERS permission.
        final long callingId = Binder.clearCallingIdentity();
        try {
            return context.createPackageContextAsUser(
                    packageName, /* flags= */ 0, UserHandle.of(userId));
        } catch (PackageManager.NameNotFoundException e) {
            Log.w(TAG, "Package name " + packageName + " not found", e);
            return null;
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}