1/*
2 * Copyright (C) 2015 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
17package com.android.packageinstaller.wear;
18
19import android.app.Notification;
20import android.app.NotificationChannel;
21import android.app.NotificationManager;
22import android.app.Service;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.FeatureInfo;
26import android.content.pm.IPackageDeleteObserver;
27import android.content.pm.PackageInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.PackageParser;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.Build;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.HandlerThread;
36import android.os.IBinder;
37import android.os.Looper;
38import android.os.Message;
39import android.os.ParcelFileDescriptor;
40import android.os.PowerManager;
41import android.os.Process;
42import android.util.ArrayMap;
43import android.util.Log;
44import android.util.Pair;
45
46import com.android.packageinstaller.DeviceUtils;
47import com.android.packageinstaller.PackageUtil;
48import com.android.packageinstaller.R;
49
50import java.io.File;
51import java.io.FileNotFoundException;
52import java.util.HashSet;
53import java.util.List;
54import java.util.Map;
55import java.util.Set;
56
57/**
58 * Service that will install/uninstall packages. It will check for permissions and features as well.
59 *
60 * -----------
61 *
62 * Debugging information:
63 *
64 *  Install Action example:
65 *  adb shell am startservice -a com.android.packageinstaller.wear.INSTALL_PACKAGE \
66 *     -d package://com.google.android.gms \
67 *     --eu com.google.android.clockwork.EXTRA_ASSET_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/wearable/com.google.android.gms/apk \
68 *     --es android.intent.extra.INSTALLER_PACKAGE_NAME com.google.android.gms \
69 *     --ez com.google.android.clockwork.EXTRA_CHECK_PERMS false \
70 *     --eu com.google.android.clockwork.EXTRA_PERM_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/permissions \
71 *     com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService
72 *
73 *  Uninstall Action example:
74 *  adb shell am startservice -a com.android.packageinstaller.wear.UNINSTALL_PACKAGE \
75 *     -d package://com.google.android.gms \
76 *     com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService
77 *
78 *  Retry GMS:
79 *  adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \
80 *     com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService
81 */
82public class WearPackageInstallerService extends Service {
83    private static final String TAG = "WearPkgInstallerService";
84
85    private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall";
86
87    private final int START_INSTALL = 1;
88    private final int START_UNINSTALL = 2;
89
90    private int mInstallNotificationId = 1;
91    private final Map<String, Integer> mNotifIdMap = new ArrayMap<>();
92
93    private final class ServiceHandler extends Handler {
94        public ServiceHandler(Looper looper) {
95            super(looper);
96        }
97
98        public void handleMessage(Message msg) {
99            switch (msg.what) {
100                case START_INSTALL:
101                    installPackage(msg.getData());
102                    break;
103                case START_UNINSTALL:
104                    uninstallPackage(msg.getData());
105                    break;
106            }
107        }
108    }
109    private ServiceHandler mServiceHandler;
110    private NotificationChannel mNotificationChannel;
111    private static volatile PowerManager.WakeLock lockStatic = null;
112
113    @Override
114    public IBinder onBind(Intent intent) {
115        return null;
116    }
117
118    @Override
119    public void onCreate() {
120        super.onCreate();
121        HandlerThread thread = new HandlerThread("PackageInstallerThread",
122                Process.THREAD_PRIORITY_BACKGROUND);
123        thread.start();
124
125        mServiceHandler = new ServiceHandler(thread.getLooper());
126    }
127
128    @Override
129    public int onStartCommand(Intent intent, int flags, int startId) {
130        if (!DeviceUtils.isWear(this)) {
131            Log.w(TAG, "Not running on wearable.");
132            finishService(null, startId);
133            return START_NOT_STICKY;
134        }
135
136        if (intent == null) {
137            Log.w(TAG, "Got null intent.");
138            finishService(null, startId);
139            return START_NOT_STICKY;
140        }
141
142        if (Log.isLoggable(TAG, Log.DEBUG)) {
143            Log.d(TAG, "Got install/uninstall request " + intent);
144        }
145
146        Uri packageUri = intent.getData();
147        if (packageUri == null) {
148            Log.e(TAG, "No package URI in intent");
149            finishService(null, startId);
150            return START_NOT_STICKY;
151        }
152        final String packageName = WearPackageUtil.getSanitizedPackageName(packageUri);
153        if (packageName == null) {
154            Log.e(TAG, "Invalid package name in URI (expected package:<pkgName>): " + packageUri);
155            finishService(null, startId);
156            return START_NOT_STICKY;
157        }
158
159        PowerManager.WakeLock lock = getLock(this.getApplicationContext());
160        if (!lock.isHeld()) {
161            lock.acquire();
162        }
163
164        Bundle intentBundle = intent.getExtras();
165        if (intentBundle == null) {
166            intentBundle = new Bundle();
167        }
168        WearPackageArgs.setStartId(intentBundle, startId);
169        WearPackageArgs.setPackageName(intentBundle, packageName);
170        String notifTitle;
171        if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) {
172            Message msg = mServiceHandler.obtainMessage(START_INSTALL);
173            msg.setData(intentBundle);
174            mServiceHandler.sendMessage(msg);
175            notifTitle = getString(R.string.installing);
176        } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) {
177            Message msg = mServiceHandler.obtainMessage(START_UNINSTALL);
178            msg.setData(intentBundle);
179            mServiceHandler.sendMessage(msg);
180            notifTitle = getString(R.string.uninstalling);
181        } else {
182            Log.e(TAG, "Unknown action : " + intent.getAction());
183            finishService(null, startId);
184            return START_NOT_STICKY;
185        }
186        Pair<Integer, Notification> notifPair = buildNotification(packageName, notifTitle);
187        startForeground(notifPair.first, notifPair.second);
188        return START_NOT_STICKY;
189    }
190
191    private void installPackage(Bundle argsBundle) {
192        int startId = WearPackageArgs.getStartId(argsBundle);
193        final String packageName = WearPackageArgs.getPackageName(argsBundle);
194        final Uri assetUri = WearPackageArgs.getAssetUri(argsBundle);
195        final Uri permUri = WearPackageArgs.getPermUri(argsBundle);
196        boolean checkPerms = WearPackageArgs.checkPerms(argsBundle);
197        boolean skipIfSameVersion = WearPackageArgs.skipIfSameVersion(argsBundle);
198        int companionSdkVersion = WearPackageArgs.getCompanionSdkVersion(argsBundle);
199        int companionDeviceVersion = WearPackageArgs.getCompanionDeviceVersion(argsBundle);
200        String compressionAlg = WearPackageArgs.getCompressionAlg(argsBundle);
201        boolean skipIfLowerVersion = WearPackageArgs.skipIfLowerVersion(argsBundle);
202
203        if (Log.isLoggable(TAG, Log.DEBUG)) {
204            Log.d(TAG, "Installing package: " + packageName + ", assetUri: " + assetUri +
205                    ",permUri: " + permUri + ", startId: " + startId + ", checkPerms: " +
206                    checkPerms + ", skipIfSameVersion: " + skipIfSameVersion +
207                    ", compressionAlg: " + compressionAlg + ", companionSdkVersion: " +
208                    companionSdkVersion + ", companionDeviceVersion: " + companionDeviceVersion +
209                    ", skipIfLowerVersion: " + skipIfLowerVersion);
210        }
211        final PackageManager pm = getPackageManager();
212        File tempFile = null;
213        int installFlags = 0;
214        PowerManager.WakeLock lock = getLock(this.getApplicationContext());
215        boolean messageSent = false;
216        try {
217            PackageInfo existingPkgInfo = null;
218            try {
219                existingPkgInfo = pm.getPackageInfo(packageName,
220                        PackageManager.MATCH_ANY_USER | PackageManager.GET_PERMISSIONS);
221                if (existingPkgInfo != null) {
222                    installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
223                }
224            } catch (PackageManager.NameNotFoundException e) {
225                // Ignore this exception. We could not find the package, will treat as a new
226                // installation.
227            }
228            if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
229                if (Log.isLoggable(TAG, Log.DEBUG)) {
230                    Log.d(TAG, "Replacing package:" + packageName);
231                }
232            }
233            // TODO(28021618): This was left as a temp file due to the fact that this code is being
234            //       deprecated and that we need the bare minimum to continue working moving forward
235            //       If this code is used as reference, this permission logic might want to be
236            //       reworked to use a stream instead of a file so that we don't need to write a
237            //       file at all.  Note that there might be some trickiness with opening a stream
238            //       for multiple users.
239            ParcelFileDescriptor parcelFd = getContentResolver()
240                    .openFileDescriptor(assetUri, "r");
241            tempFile = WearPackageUtil.getFileFromFd(WearPackageInstallerService.this,
242                    parcelFd, packageName, compressionAlg);
243            if (tempFile == null) {
244                Log.e(TAG, "Could not create a temp file from FD for " + packageName);
245                return;
246            }
247            PackageParser.Package pkg = PackageUtil.getPackageInfo(this, tempFile);
248            if (pkg == null) {
249                Log.e(TAG, "Could not parse apk information for " + packageName);
250                return;
251            }
252
253            if (!pkg.packageName.equals(packageName)) {
254                Log.e(TAG, "Wearable Package Name has to match what is provided for " +
255                        packageName);
256                return;
257            }
258
259            getLabelAndUpdateNotification(packageName,
260                    getString(R.string.installing_app, pkg.applicationInfo.loadLabel(pm)));
261
262            List<String> wearablePerms = pkg.requestedPermissions;
263
264            // Log if the installed pkg has a higher version number.
265            if (existingPkgInfo != null) {
266                if (existingPkgInfo.versionCode == pkg.mVersionCode) {
267                    if (skipIfSameVersion) {
268                        Log.w(TAG, "Version number (" + pkg.mVersionCode +
269                                ") of new app is equal to existing app for " + packageName +
270                                "; not installing due to versionCheck");
271                        return;
272                    } else {
273                        Log.w(TAG, "Version number of new app (" + pkg.mVersionCode +
274                                ") is equal to existing app for " + packageName);
275                    }
276                } else if (existingPkgInfo.versionCode > pkg.mVersionCode) {
277                    if (skipIfLowerVersion) {
278                        // Starting in Feldspar, we are not going to allow downgrades of any app.
279                        Log.w(TAG, "Version number of new app (" + pkg.mVersionCode +
280                                ") is lower than existing app ( " + existingPkgInfo.versionCode +
281                                ") for " + packageName + "; not installing due to versionCheck");
282                        return;
283                    } else {
284                        Log.w(TAG, "Version number of new app (" + pkg.mVersionCode +
285                                ") is lower than existing app ( " + existingPkgInfo.versionCode +
286                                ") for " + packageName);
287                    }
288                }
289
290                // Following the Android Phone model, we should only check for permissions for any
291                // newly defined perms.
292                if (existingPkgInfo.requestedPermissions != null) {
293                    for (int i = 0; i < existingPkgInfo.requestedPermissions.length; ++i) {
294                        // If the permission is granted, then we will not ask to request it again.
295                        if ((existingPkgInfo.requestedPermissionsFlags[i] &
296                                PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) {
297                            if (Log.isLoggable(TAG, Log.DEBUG)) {
298                                Log.d(TAG, existingPkgInfo.requestedPermissions[i] +
299                                        " is already granted for " + packageName);
300                            }
301                            wearablePerms.remove(existingPkgInfo.requestedPermissions[i]);
302                        }
303                    }
304                }
305            }
306
307            // Check that the wearable has all the features.
308            boolean hasAllFeatures = true;
309            if (pkg.reqFeatures != null) {
310                for (FeatureInfo feature : pkg.reqFeatures) {
311                    if (feature.name != null && !pm.hasSystemFeature(feature.name) &&
312                            (feature.flags & FeatureInfo.FLAG_REQUIRED) != 0) {
313                        Log.e(TAG, "Wearable does not have required feature: " + feature +
314                                " for " + packageName);
315                        hasAllFeatures = false;
316                    }
317                }
318            }
319
320            if (!hasAllFeatures) {
321                return;
322            }
323
324            // Check permissions on both the new wearable package and also on the already installed
325            // wearable package.
326            // If the app is targeting API level 23, we will also start a service in ClockworkHome
327            // which will ultimately prompt the user to accept/reject permissions.
328            if (checkPerms && !checkPermissions(pkg, companionSdkVersion, companionDeviceVersion,
329                    permUri, wearablePerms, tempFile)) {
330                Log.w(TAG, "Wearable does not have enough permissions.");
331                return;
332            }
333
334            // Finally install the package.
335            ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(assetUri, "r");
336            PackageInstallerFactory.getPackageInstaller(this).install(packageName, fd,
337                    new PackageInstallListener(this, lock, startId, packageName));
338
339            messageSent = true;
340            Log.i(TAG, "Sent installation request for " + packageName);
341        } catch (FileNotFoundException e) {
342            Log.e(TAG, "Could not find the file with URI " + assetUri, e);
343        } finally {
344            if (!messageSent) {
345                // Some error happened. If the message has been sent, we can wait for the observer
346                // which will finish the service.
347                if (tempFile != null) {
348                    tempFile.delete();
349                }
350                finishService(lock, startId);
351            }
352        }
353    }
354
355    // TODO: This was left using the old PackageManager API due to the fact that this code is being
356    //       deprecated and that we need the bare minimum to continue working moving forward
357    //       If this code is used as reference, this logic should be reworked to use the new
358    //       PackageInstaller APIs similar to how installPackage was reworked
359    private void uninstallPackage(Bundle argsBundle) {
360        int startId = WearPackageArgs.getStartId(argsBundle);
361        final String packageName = WearPackageArgs.getPackageName(argsBundle);
362
363        PowerManager.WakeLock lock = getLock(this.getApplicationContext());
364        final PackageManager pm = getPackageManager();
365        try {
366            PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0);
367            getLabelAndUpdateNotification(packageName,
368                    getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm)));
369
370            // Found package, send uninstall request.
371            pm.deletePackage(packageName, new PackageDeleteObserver(lock, startId),
372                    PackageManager.DELETE_ALL_USERS);
373
374            Log.i(TAG, "Sent delete request for " + packageName);
375        } catch (IllegalArgumentException | PackageManager.NameNotFoundException e) {
376            // Couldn't find the package, no need to call uninstall.
377            Log.w(TAG, "Could not find package, not deleting " + packageName, e);
378            finishService(lock, startId);
379        }
380    }
381
382    private boolean checkPermissions(PackageParser.Package pkg, int companionSdkVersion,
383            int companionDeviceVersion, Uri permUri, List<String> wearablePermissions,
384            File apkFile) {
385        // Assumption: We are running on Android O.
386        // If the Phone App is targeting M, all permissions may not have been granted to the phone
387        // app. If the Wear App is then not targeting M, there may be permissions that are not
388        // granted on the Phone app (by the user) right now and we cannot just grant it for the Wear
389        // app.
390        if (pkg.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M) {
391            // Install the app if Wear App is ready for the new perms model.
392            return true;
393        }
394
395        if (!doesWearHaveUngrantedPerms(pkg.packageName, permUri, wearablePermissions)) {
396            // All permissions requested by the watch are already granted on the phone, no need
397            // to do anything.
398            return true;
399        }
400
401        // Log an error if Wear is targeting < 23 and phone is targeting >= 23.
402        if (companionSdkVersion == 0 || companionSdkVersion >= Build.VERSION_CODES.M) {
403            Log.e(TAG, "MNC: Wear app's targetSdkVersion should be at least 23, if "
404                    + "phone app is targeting at least 23, will continue.");
405        }
406
407        return false;
408    }
409
410    /**
411     * Given a {@string packageName} corresponding to a phone app, query the provider for all the
412     * perms that are granted.
413     *
414     * @return true if the Wear App has any perms that have not been granted yet on the phone side.
415     * @return true if there is any error cases.
416     */
417    private boolean doesWearHaveUngrantedPerms(String packageName, Uri permUri,
418            List<String> wearablePermissions) {
419        if (permUri == null) {
420            Log.e(TAG, "Permission URI is null");
421            // Pretend there is an ungranted permission to avoid installing for error cases.
422            return true;
423        }
424        Cursor permCursor = getContentResolver().query(permUri, null, null, null, null);
425        if (permCursor == null) {
426            Log.e(TAG, "Could not get the cursor for the permissions");
427            // Pretend there is an ungranted permission to avoid installing for error cases.
428            return true;
429        }
430
431        Set<String> grantedPerms = new HashSet<>();
432        Set<String> ungrantedPerms = new HashSet<>();
433        while(permCursor.moveToNext()) {
434            // Make sure that the MatrixCursor returned by the ContentProvider has 2 columns and
435            // verify their types.
436            if (permCursor.getColumnCount() == 2
437                    && Cursor.FIELD_TYPE_STRING == permCursor.getType(0)
438                    && Cursor.FIELD_TYPE_INTEGER == permCursor.getType(1)) {
439                String perm = permCursor.getString(0);
440                Integer granted = permCursor.getInt(1);
441                if (granted == 1) {
442                    grantedPerms.add(perm);
443                } else {
444                    ungrantedPerms.add(perm);
445                }
446            }
447        }
448        permCursor.close();
449
450        boolean hasUngrantedPerm = false;
451        for (String wearablePerm : wearablePermissions) {
452            if (!grantedPerms.contains(wearablePerm)) {
453                hasUngrantedPerm = true;
454                if (!ungrantedPerms.contains(wearablePerm)) {
455                    // This is an error condition. This means that the wearable has permissions that
456                    // are not even declared in its host app. This is a developer error.
457                    Log.e(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm
458                            + "\" that is not defined in the host application's manifest.");
459                } else {
460                    Log.w(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm +
461                            "\" that is not granted in the host application.");
462                }
463            }
464        }
465        return hasUngrantedPerm;
466    }
467
468    private void finishService(PowerManager.WakeLock lock, int startId) {
469        if (lock != null && lock.isHeld()) {
470            lock.release();
471        }
472        stopSelf(startId);
473    }
474
475    private synchronized PowerManager.WakeLock getLock(Context context) {
476        if (lockStatic == null) {
477            PowerManager mgr =
478                    (PowerManager) context.getSystemService(Context.POWER_SERVICE);
479            lockStatic = mgr.newWakeLock(
480                    PowerManager.PARTIAL_WAKE_LOCK, context.getClass().getSimpleName());
481            lockStatic.setReferenceCounted(true);
482        }
483        return lockStatic;
484    }
485
486    private class PackageInstallListener implements PackageInstallerImpl.InstallListener {
487        private Context mContext;
488        private PowerManager.WakeLock mWakeLock;
489        private int mStartId;
490        private String mApplicationPackageName;
491        private PackageInstallListener(Context context, PowerManager.WakeLock wakeLock,
492                int startId, String applicationPackageName) {
493            mContext = context;
494            mWakeLock = wakeLock;
495            mStartId = startId;
496            mApplicationPackageName = applicationPackageName;
497        }
498
499        @Override
500        public void installBeginning() {
501            Log.i(TAG, "Package " + mApplicationPackageName + " is being installed.");
502        }
503
504        @Override
505        public void installSucceeded() {
506            try {
507                Log.i(TAG, "Package " + mApplicationPackageName + " was installed.");
508
509                // Delete tempFile from the file system.
510                File tempFile = WearPackageUtil.getTemporaryFile(mContext, mApplicationPackageName);
511                if (tempFile != null) {
512                    tempFile.delete();
513                }
514            } finally {
515                finishService(mWakeLock, mStartId);
516            }
517        }
518
519        @Override
520        public void installFailed(int errorCode, String errorDesc) {
521            Log.e(TAG, "Package install failed " + mApplicationPackageName
522                    + ", errorCode " + errorCode);
523            finishService(mWakeLock, mStartId);
524        }
525    }
526
527    private class PackageDeleteObserver extends IPackageDeleteObserver.Stub {
528        private PowerManager.WakeLock mWakeLock;
529        private int mStartId;
530
531        private PackageDeleteObserver(PowerManager.WakeLock wakeLock, int startId) {
532            mWakeLock = wakeLock;
533            mStartId = startId;
534        }
535
536        public void packageDeleted(String packageName, int returnCode) {
537            try {
538                if (returnCode >= 0) {
539                    Log.i(TAG, "Package " + packageName + " was uninstalled.");
540                } else {
541                    Log.e(TAG, "Package uninstall failed " + packageName + ", returnCode " +
542                            returnCode);
543                }
544            } finally {
545                finishService(mWakeLock, mStartId);
546            }
547        }
548    }
549
550    private synchronized Pair<Integer, Notification> buildNotification(final String packageName,
551            final String title) {
552        int notifId;
553        if (mNotifIdMap.containsKey(packageName)) {
554            notifId = mNotifIdMap.get(packageName);
555        } else {
556            notifId = mInstallNotificationId++;
557            mNotifIdMap.put(packageName, notifId);
558        }
559
560        if (mNotificationChannel == null) {
561            mNotificationChannel = new NotificationChannel(WEAR_APPS_CHANNEL,
562                    getString(R.string.wear_app_channel), NotificationManager.IMPORTANCE_MIN);
563            NotificationManager notificationManager = getSystemService(NotificationManager.class);
564            notificationManager.createNotificationChannel(mNotificationChannel);
565        }
566        return new Pair<>(notifId, new Notification.Builder(this, WEAR_APPS_CHANNEL)
567            .setSmallIcon(R.drawable.ic_file_download)
568            .setContentTitle(title)
569            .build());
570    }
571
572    private void getLabelAndUpdateNotification(String packageName, String title) {
573        // Update notification since we have a label now.
574        NotificationManager notificationManager = getSystemService(NotificationManager.class);
575        Pair<Integer, Notification> notifPair = buildNotification(packageName, title);
576        notificationManager.notify(notifPair.first, notifPair.second);
577    }
578}
579