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.mtp;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.Notification;
22import android.app.Service;
23import android.app.NotificationManager;
24import android.content.Intent;
25import android.os.IBinder;
26import android.os.Parcelable;
27import android.service.notification.StatusBarNotification;
28import android.util.Log;
29import com.android.internal.util.Preconditions;
30import java.util.HashSet;
31import java.util.Set;
32
33/**
34 * Service to manage lifetime of DocumentsProvider's process.
35 * The service prevents the system from killing the process that holds USB connections. The service
36 * starts to run when the first MTP device is opened, and stops when the last MTP device is closed.
37 */
38public class MtpDocumentsService extends Service {
39    static final String ACTION_UPDATE_NOTIFICATION = "com.android.mtp.UPDATE_NOTIFICATION";
40    static final String EXTRA_DEVICE_IDS = "deviceIds";
41    static final String EXTRA_DEVICE_NOTIFICATIONS = "deviceNotifications";
42
43    private NotificationManager mNotificationManager;
44
45    @Override
46    public IBinder onBind(Intent intent) {
47        // The service is used via intents.
48        return null;
49    }
50
51    @Override
52    public void onCreate() {
53        super.onCreate();
54        mNotificationManager = getSystemService(NotificationManager.class);
55    }
56
57    @Override
58    public int onStartCommand(Intent intent, int flags, int startId) {
59        // If intent is null, the service was restarted.
60        if (intent == null || ACTION_UPDATE_NOTIFICATION.equals(intent.getAction())) {
61            final int[] ids = intent.hasExtra(EXTRA_DEVICE_IDS) ?
62                    intent.getExtras().getIntArray(EXTRA_DEVICE_IDS) : null;
63            final Notification[] notifications = intent.hasExtra(EXTRA_DEVICE_NOTIFICATIONS) ?
64                    castToNotifications(intent.getExtras().getParcelableArray(
65                            EXTRA_DEVICE_NOTIFICATIONS)) : null;
66            return updateForegroundState(ids, notifications) ? START_STICKY : START_NOT_STICKY;
67        }
68        return START_NOT_STICKY;
69    }
70
71    /**
72     * Updates the foreground state of the service.
73     * @return Whether the service is foreground or not.
74     */
75    private boolean updateForegroundState(
76            @Nullable int[] ids, @Nullable Notification[] notifications) {
77        final Set<Integer> openedNotification = new HashSet<>();
78        final int size = ids != null ? ids.length : 0;
79        if (size != 0) {
80            Preconditions.checkArgument(ids != null);
81            Preconditions.checkArgument(notifications != null);
82            Preconditions.checkArgument(ids.length == notifications.length);
83        }
84
85        for (int i = 0; i < size; i++) {
86            if (i == 0) {
87                // Mark this service as foreground with the notification so that the process is
88                // not killed by the system while a MTP device is opened.
89                startForeground(ids[i], notifications[i]);
90            } else {
91                // Only one notification can be shown as a foreground notification. We need to
92                // show the rest as normal notification.
93                mNotificationManager.notify(ids[i], notifications[i]);
94            }
95            openedNotification.add(ids[i]);
96        }
97
98        final StatusBarNotification[] activeNotifications =
99                mNotificationManager.getActiveNotifications();
100        for (final StatusBarNotification notification : activeNotifications) {
101            if (!openedNotification.contains(notification.getId())) {
102                mNotificationManager.cancel(notification.getId());
103            }
104        }
105
106        if (size == 0) {
107            // There is no opened device.
108            stopForeground(true /* removeNotification */);
109            stopSelf();
110            return false;
111        }
112
113        return true;
114    }
115
116    private static @NonNull Notification[] castToNotifications(@NonNull Parcelable[] src) {
117        Preconditions.checkNotNull(src);
118        final Notification[] notifications = new Notification[src.length];
119        for (int i = 0; i < src.length; i++) {
120            notifications[i] = (Notification) src[i];
121        }
122        return notifications;
123    }
124}
125