1 /*
2  * Copyright (C) 2014 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.launcher3.compat;
18 
19 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
20 
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageInstaller;
24 import android.content.pm.PackageInstaller.SessionCallback;
25 import android.content.pm.PackageInstaller.SessionInfo;
26 import android.content.pm.PackageManager;
27 import android.os.UserHandle;
28 import android.text.TextUtils;
29 import android.util.SparseArray;
30 
31 import com.android.launcher3.SessionCommitReceiver;
32 import com.android.launcher3.Utilities;
33 import com.android.launcher3.icons.IconCache;
34 import com.android.launcher3.LauncherAppState;
35 import com.android.launcher3.config.FeatureFlags;
36 import com.android.launcher3.util.IntArray;
37 import com.android.launcher3.util.IntSet;
38 import com.android.launcher3.util.PackageUserKey;
39 import com.android.launcher3.util.Thunk;
40 
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.Iterator;
44 import java.util.List;
45 
46 import static com.android.launcher3.Utilities.getPrefs;
47 
48 public class PackageInstallerCompatVL extends PackageInstallerCompat {
49 
50     private static final boolean DEBUG = false;
51 
52     @Thunk final SparseArray<PackageUserKey> mActiveSessions = new SparseArray<>();
53 
54     @Thunk final PackageInstaller mInstaller;
55     private final IconCache mCache;
56     private final Context mAppContext;
57     private final HashMap<String,Boolean> mSessionVerifiedMap = new HashMap<>();
58     private final LauncherAppsCompat mLauncherApps;
59     private final IntSet mPromiseIconIds;
60 
PackageInstallerCompatVL(Context context)61     PackageInstallerCompatVL(Context context) {
62         mAppContext = context.getApplicationContext();
63         mInstaller = context.getPackageManager().getPackageInstaller();
64         mCache = LauncherAppState.getInstance(context).getIconCache();
65         mLauncherApps = LauncherAppsCompat.getInstance(context);
66         mLauncherApps.registerSessionCallback(MODEL_EXECUTOR, mCallback);
67         mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString(
68                 getPrefs(context).getString(PROMISE_ICON_IDS, "")));
69 
70         cleanUpPromiseIconIds();
71     }
72 
cleanUpPromiseIconIds()73     private void cleanUpPromiseIconIds() {
74         IntArray existingIds = new IntArray();
75         for (SessionInfo info : updateAndGetActiveSessionCache().values()) {
76             existingIds.add(info.getSessionId());
77         }
78         IntArray idsToRemove = new IntArray();
79 
80         for (int i = mPromiseIconIds.size() - 1; i >= 0; --i) {
81             if (!existingIds.contains(mPromiseIconIds.getArray().get(i))) {
82                 idsToRemove.add(mPromiseIconIds.getArray().get(i));
83             }
84         }
85         for (int i = idsToRemove.size() - 1; i >= 0; --i) {
86             mPromiseIconIds.getArray().removeValue(idsToRemove.get(i));
87         }
88     }
89 
90     @Override
updateAndGetActiveSessionCache()91     public HashMap<PackageUserKey, SessionInfo> updateAndGetActiveSessionCache() {
92         HashMap<PackageUserKey, SessionInfo> activePackages = new HashMap<>();
93         for (SessionInfo info : getAllVerifiedSessions()) {
94             addSessionInfoToCache(info, getUserHandle(info));
95             if (info.getAppPackageName() != null) {
96                 activePackages.put(new PackageUserKey(info.getAppPackageName(),
97                         getUserHandle(info)), info);
98                 mActiveSessions.put(info.getSessionId(),
99                         new PackageUserKey(info.getAppPackageName(), getUserHandle(info)));
100             }
101         }
102         return activePackages;
103     }
104 
getActiveSessionInfo(UserHandle user, String pkg)105     public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) {
106         for (SessionInfo info : getAllVerifiedSessions()) {
107             boolean match = pkg.equals(info.getAppPackageName());
108             if (Utilities.ATLEAST_Q && !user.equals(getUserHandle(info))) {
109                 match = false;
110             }
111             if (match) {
112                 return info;
113             }
114         }
115         return null;
116     }
117 
addSessionInfoToCache(SessionInfo info, UserHandle user)118     @Thunk void addSessionInfoToCache(SessionInfo info, UserHandle user) {
119         String packageName = info.getAppPackageName();
120         if (packageName != null) {
121             mCache.cachePackageInstallInfo(packageName, user, info.getAppIcon(),
122                     info.getAppLabel());
123         }
124     }
125 
126     @Override
onStop()127     public void onStop() {
128         mLauncherApps.unregisterSessionCallback(mCallback);
129     }
130 
sendUpdate(PackageInstallInfo info)131     @Thunk void sendUpdate(PackageInstallInfo info) {
132         LauncherAppState app = LauncherAppState.getInstanceNoCreate();
133         if (app != null) {
134             app.getModel().setPackageState(info);
135         }
136     }
137 
138     /**
139      * Add a promise app icon to the workspace iff:
140      * - The settings for it are enabled
141      * - The user installed the app
142      * - There is an app icon and label (For apps with no launching activity, no icon is provided).
143      * - The app is not already installed
144      * - A promise icon for the session has not already been created
145      */
tryQueuePromiseAppIcon(SessionInfo sessionInfo)146     private void tryQueuePromiseAppIcon(SessionInfo sessionInfo) {
147         if (Utilities.ATLEAST_OREO && FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
148                 && SessionCommitReceiver.isEnabled(mAppContext)
149                 && verify(sessionInfo) != null
150                 && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
151                 && sessionInfo.getAppIcon() != null
152                 && !TextUtils.isEmpty(sessionInfo.getAppLabel())
153                 && !mPromiseIconIds.contains(sessionInfo.getSessionId())
154                 && mLauncherApps.getApplicationInfo(sessionInfo.getAppPackageName(), 0,
155                         getUserHandle(sessionInfo)) == null) {
156             SessionCommitReceiver.queuePromiseAppIconAddition(mAppContext, sessionInfo);
157             mPromiseIconIds.add(sessionInfo.getSessionId());
158             updatePromiseIconPrefs();
159         }
160     }
161 
162     private final SessionCallback mCallback = new SessionCallback() {
163 
164         @Override
165         public void onCreated(int sessionId) {
166             SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId);
167             if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS && sessionInfo != null) {
168                 LauncherAppState app = LauncherAppState.getInstanceNoCreate();
169                 if (app != null) {
170                     app.getModel().onInstallSessionCreated(
171                             PackageInstallInfo.fromInstallingState(sessionInfo));
172                 }
173             }
174 
175             tryQueuePromiseAppIcon(sessionInfo);
176         }
177 
178         @Override
179         public void onFinished(int sessionId, boolean success) {
180             // For a finished session, we can't get the session info. So use the
181             // packageName from our local cache.
182             PackageUserKey key = mActiveSessions.get(sessionId);
183             mActiveSessions.remove(sessionId);
184 
185             if (key != null && key.mPackageName != null) {
186                 String packageName = key.mPackageName;
187                 sendUpdate(PackageInstallInfo.fromState(success ? STATUS_INSTALLED : STATUS_FAILED,
188                         packageName, key.mUser));
189 
190                 if (!success && FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
191                         && mPromiseIconIds.contains(sessionId)) {
192                     LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
193                     if (appState != null) {
194                         appState.getModel().onSessionFailure(packageName, key.mUser);
195                     }
196                     // If it is successful, the id is removed in the the package added flow.
197                     removePromiseIconId(sessionId);
198                 }
199             }
200         }
201 
202         @Override
203         public void onProgressChanged(int sessionId, float progress) {
204             SessionInfo session = verify(mInstaller.getSessionInfo(sessionId));
205             if (session != null && session.getAppPackageName() != null) {
206                 sendUpdate(PackageInstallInfo.fromInstallingState(session));
207             }
208         }
209 
210         @Override
211         public void onActiveChanged(int sessionId, boolean active) { }
212 
213         @Override
214         public void onBadgingChanged(int sessionId) {
215             SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId);
216             if (sessionInfo != null) {
217                 tryQueuePromiseAppIcon(sessionInfo);
218             }
219         }
220 
221         private SessionInfo pushSessionDisplayToLauncher(int sessionId) {
222             SessionInfo session = verify(mInstaller.getSessionInfo(sessionId));
223             if (session != null && session.getAppPackageName() != null) {
224                 UserHandle user = getUserHandle(session);
225                 mActiveSessions.put(session.getSessionId(),
226                         new PackageUserKey(session.getAppPackageName(), user));
227                 addSessionInfoToCache(session, user);
228                 LauncherAppState app = LauncherAppState.getInstanceNoCreate();
229                 if (app != null) {
230                     app.getModel().updateSessionDisplayInfo(session.getAppPackageName(),
231                             user);
232                 }
233                 return session;
234             }
235             return null;
236         }
237     };
238 
verify(PackageInstaller.SessionInfo sessionInfo)239     private PackageInstaller.SessionInfo verify(PackageInstaller.SessionInfo sessionInfo) {
240         if (sessionInfo == null
241                 || sessionInfo.getInstallerPackageName() == null
242                 || TextUtils.isEmpty(sessionInfo.getAppPackageName())) {
243             return null;
244         }
245         String pkg = sessionInfo.getInstallerPackageName();
246         synchronized (mSessionVerifiedMap) {
247             if (!mSessionVerifiedMap.containsKey(pkg)) {
248                 LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mAppContext);
249                 boolean hasSystemFlag = launcherApps.getApplicationInfo(pkg,
250                         ApplicationInfo.FLAG_SYSTEM, getUserHandle(sessionInfo)) != null;
251                 mSessionVerifiedMap.put(pkg, DEBUG || hasSystemFlag);
252             }
253         }
254         return mSessionVerifiedMap.get(pkg) ? sessionInfo : null;
255     }
256 
257     @Override
getAllVerifiedSessions()258     public List<SessionInfo> getAllVerifiedSessions() {
259         List<SessionInfo> list = new ArrayList<>(Utilities.ATLEAST_Q
260                 ? mLauncherApps.getAllPackageInstallerSessions()
261                 : mInstaller.getAllSessions());
262         Iterator<SessionInfo> it = list.iterator();
263         while (it.hasNext()) {
264             if (verify(it.next()) == null) {
265                 it.remove();
266             }
267         }
268         return list;
269     }
270 
271     @Override
promiseIconAddedForId(int sessionId)272     public boolean promiseIconAddedForId(int sessionId) {
273         return mPromiseIconIds.contains(sessionId);
274     }
275 
276     @Override
removePromiseIconId(int sessionId)277     public void removePromiseIconId(int sessionId) {
278         if (mPromiseIconIds.contains(sessionId)) {
279             mPromiseIconIds.getArray().removeValue(sessionId);
280             updatePromiseIconPrefs();
281         }
282     }
283 
updatePromiseIconPrefs()284     private void updatePromiseIconPrefs() {
285         getPrefs(mAppContext).edit()
286                 .putString(PROMISE_ICON_IDS, mPromiseIconIds.getArray().toConcatString())
287                 .apply();
288     }
289 }
290