1 /*
2  * Copyright (C) 2020 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.systemui.toast;
18 
19 import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
20 import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
21 
22 import android.animation.Animator;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.UserIdInt;
26 import android.app.Application;
27 import android.content.Context;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.graphics.Bitmap;
34 import android.graphics.drawable.BitmapDrawable;
35 import android.graphics.drawable.Drawable;
36 import android.os.Build;
37 import android.os.UserHandle;
38 import android.util.Log;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.widget.ImageView;
42 import android.widget.TextView;
43 
44 import com.android.internal.R;
45 import com.android.launcher3.icons.IconFactory;
46 import com.android.settingslib.applications.ApplicationsState;
47 import com.android.settingslib.applications.ApplicationsState.AppEntry;
48 import com.android.systemui.plugins.ToastPlugin;
49 
50 /**
51  * SystemUI TextToast that can be customized by ToastPlugins. Should never instantiate this class
52  * directly. Instead, use {@link ToastFactory#createToast}.
53  */
54 public class SystemUIToast implements ToastPlugin.Toast {
55     static final String TAG = "SystemUIToast";
56     final Context mContext;
57     final CharSequence mText;
58     final ToastPlugin.Toast mPluginToast;
59 
60     private final String mPackageName;
61     @UserIdInt private final int mUserId;
62     private final LayoutInflater mLayoutInflater;
63 
64     final int mDefaultX = 0;
65     final int mDefaultHorizontalMargin = 0;
66     final int mDefaultVerticalMargin = 0;
67 
68     private int mDefaultY;
69     private int mDefaultGravity;
70 
71     @NonNull private final View mToastView;
72     @Nullable private final Animator mInAnimator;
73     @Nullable private final Animator mOutAnimator;
74 
SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text, String packageName, int userId, int orientation)75     SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text,
76             String packageName, int userId, int orientation) {
77         this(layoutInflater, context, text, null, packageName, userId,
78                 orientation);
79     }
80 
SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text, ToastPlugin.Toast pluginToast, String packageName, @UserIdInt int userId, int orientation)81     SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text,
82             ToastPlugin.Toast pluginToast, String packageName, @UserIdInt int userId,
83             int orientation) {
84         mLayoutInflater = layoutInflater;
85         mContext = context;
86         mText = text;
87         mPluginToast = pluginToast;
88         mPackageName = packageName;
89         mUserId = userId;
90         mToastView = inflateToastView();
91         mInAnimator = createInAnimator();
92         mOutAnimator = createOutAnimator();
93 
94         onOrientationChange(orientation);
95     }
96 
97     @Override
98     @NonNull
getGravity()99     public Integer getGravity() {
100         if (isPluginToast() && mPluginToast.getGravity() != null) {
101             return mPluginToast.getGravity();
102         }
103         return mDefaultGravity;
104     }
105 
106     @Override
107     @NonNull
getXOffset()108     public Integer getXOffset() {
109         if (isPluginToast() && mPluginToast.getXOffset() != null) {
110             return mPluginToast.getXOffset();
111         }
112         return mDefaultX;
113     }
114 
115     @Override
116     @NonNull
getYOffset()117     public Integer getYOffset() {
118         if (isPluginToast() && mPluginToast.getYOffset() != null) {
119             return mPluginToast.getYOffset();
120         }
121         return mDefaultY;
122     }
123 
124     @Override
125     @NonNull
getHorizontalMargin()126     public Integer getHorizontalMargin() {
127         if (isPluginToast() && mPluginToast.getHorizontalMargin() != null) {
128             return mPluginToast.getHorizontalMargin();
129         }
130         return mDefaultHorizontalMargin;
131     }
132 
133     @Override
134     @NonNull
getVerticalMargin()135     public Integer getVerticalMargin() {
136         if (isPluginToast() && mPluginToast.getVerticalMargin() != null) {
137             return mPluginToast.getVerticalMargin();
138         }
139         return mDefaultVerticalMargin;
140     }
141 
142     @Override
143     @NonNull
getView()144     public View getView() {
145         return mToastView;
146     }
147 
148     @Override
149     @Nullable
getInAnimation()150     public Animator getInAnimation() {
151         return mInAnimator;
152     }
153 
154     @Override
155     @Nullable
getOutAnimation()156     public Animator getOutAnimation() {
157         return mOutAnimator;
158     }
159 
160     /**
161      * Whether this toast has a custom animation.
162      */
hasCustomAnimation()163     public boolean hasCustomAnimation() {
164         return getInAnimation() != null || getOutAnimation() != null;
165     }
166 
isPluginToast()167     private boolean isPluginToast() {
168         return mPluginToast != null;
169     }
170 
inflateToastView()171     private View inflateToastView() {
172         if (isPluginToast() && mPluginToast.getView() != null) {
173             return mPluginToast.getView();
174         }
175 
176         final View toastView = mLayoutInflater.inflate(
177                     com.android.systemui.R.layout.text_toast, null);
178         final TextView textView = toastView.findViewById(com.android.systemui.R.id.text);
179         final ImageView iconView = toastView.findViewById(com.android.systemui.R.id.icon);
180         textView.setText(mText);
181 
182         ApplicationInfo appInfo = null;
183         try {
184             appInfo = mContext.getPackageManager()
185                     .getApplicationInfoAsUser(mPackageName, 0, mUserId);
186         } catch (PackageManager.NameNotFoundException e) {
187             Log.e(TAG, "Package name not found package=" + mPackageName
188                     + " user=" + mUserId);
189         }
190 
191         if (appInfo != null && appInfo.targetSdkVersion < Build.VERSION_CODES.S) {
192             // no two-line limit
193             textView.setMaxLines(Integer.MAX_VALUE);
194 
195             // no app icon
196             toastView.findViewById(com.android.systemui.R.id.icon).setVisibility(View.GONE);
197         } else {
198             Drawable icon = getBadgedIcon(mContext, mPackageName, mUserId);
199             if (icon == null) {
200                 iconView.setVisibility(View.GONE);
201             } else {
202                 iconView.setImageDrawable(icon);
203                 if (appInfo == null) {
204                     Log.d(TAG, "No appInfo for pkg=" + mPackageName + " usr=" + mUserId);
205                 } else if (appInfo.labelRes != 0) {
206                     try {
207                         Resources res = mContext.getPackageManager().getResourcesForApplication(
208                                 appInfo,
209                                 new Configuration(mContext.getResources().getConfiguration()));
210                         iconView.setContentDescription(res.getString(appInfo.labelRes));
211                     } catch (PackageManager.NameNotFoundException e) {
212                         Log.d(TAG, "Cannot find application resources for icon label.");
213                     }
214                 }
215             }
216         }
217         return toastView;
218     }
219 
220     /**
221      * Called on orientation changes to update parameters associated with the toast placement.
222      */
onOrientationChange(int orientation)223     public void onOrientationChange(int orientation) {
224         if (mPluginToast != null) {
225             mPluginToast.onOrientationChange(orientation);
226         }
227 
228         mDefaultY = mContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset);
229         mDefaultGravity =
230                 mContext.getResources().getInteger(R.integer.config_toastDefaultGravity);
231     }
232 
createInAnimator()233     private Animator createInAnimator() {
234         if (isPluginToast() && mPluginToast.getInAnimation() != null) {
235             return mPluginToast.getInAnimation();
236         }
237 
238         return ToastDefaultAnimation.Companion.toastIn(getView());
239     }
240 
createOutAnimator()241     private Animator createOutAnimator() {
242         if (isPluginToast() && mPluginToast.getOutAnimation() != null) {
243             return mPluginToast.getOutAnimation();
244         }
245         return ToastDefaultAnimation.Companion.toastOut(getView());
246     }
247 
248     /**
249      * Get badged app icon if necessary, similar as used in the Settings UI.
250      * @return The icon to use
251      */
getBadgedIcon(@onNull Context context, String packageName, int userId)252     public static Drawable getBadgedIcon(@NonNull Context context, String packageName,
253             int userId) {
254         if (!(context.getApplicationContext() instanceof Application)) {
255             return null;
256         }
257 
258         final Context userContext;
259         try {
260             userContext = context.createPackageContextAsUser("android",
261                 0, new UserHandle(userId));
262         } catch (NameNotFoundException e) {
263             Log.e(TAG, "Could not create user package context");
264             return null;
265         }
266 
267         final ApplicationsState appState =
268                 ApplicationsState.getInstance((Application) context.getApplicationContext());
269         if (!appState.isUserAdded(userId)) {
270             Log.d(TAG, "user hasn't been fully initialized, not showing an app icon for "
271                     + "packageName=" + packageName);
272             return null;
273         }
274 
275         final PackageManager packageManager = userContext.getPackageManager();
276         final AppEntry appEntry = appState.getEntry(packageName, userId);
277         if (appEntry == null || appEntry.info == null
278                 || !showApplicationIcon(appEntry.info, packageManager)) {
279             return null;
280         }
281 
282         final ApplicationInfo appInfo = appEntry.info;
283         UserHandle user = UserHandle.getUserHandleForUid(appInfo.uid);
284         IconFactory iconFactory = IconFactory.obtain(context);
285         Bitmap iconBmp = iconFactory.createBadgedIconBitmap(
286                 appInfo.loadUnbadgedIcon(packageManager), user, true).icon;
287         return new BitmapDrawable(context.getResources(), iconBmp);
288     }
289 
showApplicationIcon(ApplicationInfo appInfo, PackageManager packageManager)290     private static boolean showApplicationIcon(ApplicationInfo appInfo,
291             PackageManager packageManager) {
292         if (hasFlag(appInfo.flags, FLAG_UPDATED_SYSTEM_APP)) {
293             return packageManager.getLaunchIntentForPackage(appInfo.packageName)
294                 != null;
295         }
296         return !hasFlag(appInfo.flags, FLAG_SYSTEM);
297     }
298 
hasFlag(int flags, int flag)299     private static boolean hasFlag(int flags, int flag) {
300         return (flags & flag) != 0;
301     }
302 }
303