1/*
2 * Copyright (C) 2008 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.providers.downloads;
18
19import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
20import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
21
22import static com.android.providers.downloads.Constants.TAG;
23import static com.android.providers.downloads.Helpers.getAsyncHandler;
24import static com.android.providers.downloads.Helpers.getDownloadNotifier;
25import static com.android.providers.downloads.Helpers.getInt;
26import static com.android.providers.downloads.Helpers.getString;
27import static com.android.providers.downloads.Helpers.getSystemFacade;
28
29import android.app.DownloadManager;
30import android.app.NotificationManager;
31import android.content.BroadcastReceiver;
32import android.content.ContentResolver;
33import android.content.ContentUris;
34import android.content.ContentValues;
35import android.content.Context;
36import android.content.Intent;
37import android.database.Cursor;
38import android.net.Uri;
39import android.provider.Downloads;
40import android.text.TextUtils;
41import android.util.Log;
42import android.util.Slog;
43import android.widget.Toast;
44
45/**
46 * Receives system broadcasts (boot, network connectivity)
47 */
48public class DownloadReceiver extends BroadcastReceiver {
49    /**
50     * Intent extra included with {@link Constants#ACTION_CANCEL} intents,
51     * indicating the IDs (as array of long) of the downloads that were
52     * canceled.
53     */
54    public static final String EXTRA_CANCELED_DOWNLOAD_IDS =
55            "com.android.providers.downloads.extra.CANCELED_DOWNLOAD_IDS";
56
57    /**
58     * Intent extra included with {@link Constants#ACTION_CANCEL} intents,
59     * indicating the tag of the notification corresponding to the download(s)
60     * that were canceled; this notification must be canceled.
61     */
62    public static final String EXTRA_CANCELED_DOWNLOAD_NOTIFICATION_TAG =
63            "com.android.providers.downloads.extra.CANCELED_DOWNLOAD_NOTIFICATION_TAG";
64
65    @Override
66    public void onReceive(final Context context, final Intent intent) {
67        final String action = intent.getAction();
68        if (Intent.ACTION_BOOT_COMPLETED.equals(action)
69                || Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
70            final PendingResult result = goAsync();
71            getAsyncHandler().post(new Runnable() {
72                @Override
73                public void run() {
74                    handleBootCompleted(context);
75                    result.finish();
76                }
77            });
78        } else if (Intent.ACTION_UID_REMOVED.equals(action)) {
79            final PendingResult result = goAsync();
80            getAsyncHandler().post(new Runnable() {
81                @Override
82                public void run() {
83                    handleUidRemoved(context, intent);
84                    result.finish();
85                }
86            });
87
88        } else if (Constants.ACTION_OPEN.equals(action)
89                || Constants.ACTION_LIST.equals(action)
90                || Constants.ACTION_HIDE.equals(action)) {
91
92            final PendingResult result = goAsync();
93            if (result == null) {
94                // TODO: remove this once test is refactored
95                handleNotificationBroadcast(context, intent);
96            } else {
97                getAsyncHandler().post(new Runnable() {
98                    @Override
99                    public void run() {
100                        handleNotificationBroadcast(context, intent);
101                        result.finish();
102                    }
103                });
104            }
105        } else if (Constants.ACTION_CANCEL.equals(action)) {
106            long[] downloadIds = intent.getLongArrayExtra(
107                    DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_IDS);
108            DownloadManager manager = (DownloadManager) context.getSystemService(
109                    Context.DOWNLOAD_SERVICE);
110            manager.remove(downloadIds);
111
112            String notifTag = intent.getStringExtra(
113                    DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_NOTIFICATION_TAG);
114            NotificationManager notifManager = (NotificationManager) context.getSystemService(
115                    Context.NOTIFICATION_SERVICE);
116            notifManager.cancel(notifTag, 0);
117        }
118    }
119
120    private void handleBootCompleted(Context context) {
121        // Show any relevant notifications for completed downloads
122        getDownloadNotifier(context).update();
123
124        // Schedule all downloads that are ready
125        final ContentResolver resolver = context.getContentResolver();
126        try (Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, null,
127                null, null)) {
128            final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
129            final DownloadInfo info = new DownloadInfo(context);
130            while (cursor.moveToNext()) {
131                reader.updateFromDatabase(info);
132                Helpers.scheduleJob(context, info);
133            }
134        }
135
136        // Schedule idle pass to clean up orphaned files
137        DownloadIdleService.scheduleIdlePass(context);
138    }
139
140    private void handleUidRemoved(Context context, Intent intent) {
141        final ContentResolver resolver = context.getContentResolver();
142        final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
143
144        // First, disown any downloads that live in shared storage
145        final ContentValues values = new ContentValues();
146        values.putNull(Constants.UID);
147        final int disowned = resolver.update(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, values,
148                Constants.UID + "=" + uid + " AND " + Downloads.Impl.COLUMN_DESTINATION + " IN ("
149                        + Downloads.Impl.DESTINATION_EXTERNAL + ","
150                        + Downloads.Impl.DESTINATION_FILE_URI + ")",
151                null);
152
153        // Finally, delete any remaining downloads owned by UID
154        final int deleted = resolver.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
155                Constants.UID + "=" + uid, null);
156
157        if ((disowned + deleted) > 0) {
158            Slog.d(TAG, "Disowned " + disowned + " and deleted " + deleted
159                    + " downloads owned by UID " + uid);
160        }
161    }
162
163    /**
164     * Handle any broadcast related to a system notification.
165     */
166    private void handleNotificationBroadcast(Context context, Intent intent) {
167        final String action = intent.getAction();
168        if (Constants.ACTION_LIST.equals(action)) {
169            final long[] ids = intent.getLongArrayExtra(
170                    DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
171            sendNotificationClickedIntent(context, ids);
172
173        } else if (Constants.ACTION_OPEN.equals(action)) {
174            final long id = ContentUris.parseId(intent.getData());
175            openDownload(context, id);
176            hideNotification(context, id);
177
178        } else if (Constants.ACTION_HIDE.equals(action)) {
179            final long id = ContentUris.parseId(intent.getData());
180            hideNotification(context, id);
181        }
182    }
183
184    /**
185     * Mark the given {@link DownloadManager#COLUMN_ID} as being acknowledged by
186     * user so it's not renewed later.
187     */
188    private void hideNotification(Context context, long id) {
189        final int status;
190        final int visibility;
191
192        final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
193        final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
194        try {
195            if (cursor.moveToFirst()) {
196                status = getInt(cursor, Downloads.Impl.COLUMN_STATUS);
197                visibility = getInt(cursor, Downloads.Impl.COLUMN_VISIBILITY);
198            } else {
199                Log.w(TAG, "Missing details for download " + id);
200                return;
201            }
202        } finally {
203            cursor.close();
204        }
205
206        if (Downloads.Impl.isStatusCompleted(status) &&
207                (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
208                || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)) {
209            final ContentValues values = new ContentValues();
210            values.put(Downloads.Impl.COLUMN_VISIBILITY,
211                    Downloads.Impl.VISIBILITY_VISIBLE);
212            context.getContentResolver().update(uri, values, null, null);
213        }
214    }
215
216    /**
217     * Start activity to display the file represented by the given
218     * {@link DownloadManager#COLUMN_ID}.
219     */
220    private void openDownload(Context context, long id) {
221        if (!OpenHelper.startViewIntent(context, id, Intent.FLAG_ACTIVITY_NEW_TASK)) {
222            Toast.makeText(context, R.string.download_no_application_title, Toast.LENGTH_SHORT)
223                    .show();
224        }
225    }
226
227    /**
228     * Notify the owner of a running download that its notification was clicked.
229     */
230    private void sendNotificationClickedIntent(Context context, long[] ids) {
231        final String packageName;
232        final String clazz;
233        final boolean isPublicApi;
234
235        final Uri uri = ContentUris.withAppendedId(
236                Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, ids[0]);
237        final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
238        try {
239            if (cursor.moveToFirst()) {
240                packageName = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
241                clazz = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
242                isPublicApi = getInt(cursor, Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
243            } else {
244                Log.w(TAG, "Missing details for download " + ids[0]);
245                return;
246            }
247        } finally {
248            cursor.close();
249        }
250
251        if (TextUtils.isEmpty(packageName)) {
252            Log.w(TAG, "Missing package; skipping broadcast");
253            return;
254        }
255
256        Intent appIntent = null;
257        if (isPublicApi) {
258            appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
259            appIntent.setPackage(packageName);
260            appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids);
261
262        } else { // legacy behavior
263            if (TextUtils.isEmpty(clazz)) {
264                Log.w(TAG, "Missing class; skipping broadcast");
265                return;
266            }
267
268            appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
269            appIntent.setClassName(packageName, clazz);
270            appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids);
271
272            if (ids.length == 1) {
273                appIntent.setData(uri);
274            } else {
275                appIntent.setData(Downloads.Impl.CONTENT_URI);
276            }
277        }
278
279        getSystemFacade(context).sendBroadcast(appIntent);
280    }
281}
282