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 
17 package com.android.providers.downloads;
18 
19 import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
20 import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
21 
22 import static com.android.providers.downloads.Constants.TAG;
23 import static com.android.providers.downloads.Helpers.getAsyncHandler;
24 import static com.android.providers.downloads.Helpers.getDownloadNotifier;
25 import static com.android.providers.downloads.Helpers.getInt;
26 import static com.android.providers.downloads.Helpers.getString;
27 import static com.android.providers.downloads.Helpers.getSystemFacade;
28 
29 import android.app.DownloadManager;
30 import android.app.NotificationManager;
31 import android.content.BroadcastReceiver;
32 import android.content.ContentResolver;
33 import android.content.ContentUris;
34 import android.content.ContentValues;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.database.Cursor;
38 import android.net.Uri;
39 import android.provider.Downloads;
40 import android.text.TextUtils;
41 import android.util.Log;
42 import android.util.Slog;
43 import android.widget.Toast;
44 
45 /**
46  * Receives system broadcasts (boot, network connectivity)
47  */
48 public 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
onReceive(final Context context, final Intent intent)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 
handleBootCompleted(Context context)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 
handleUidRemoved(Context context, Intent intent)140     private void handleUidRemoved(Context context, Intent intent) {
141         final ContentResolver resolver = context.getContentResolver();
142 
143         final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
144         final int count = resolver.delete(
145                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, Constants.UID + "=" + uid, null);
146 
147         if (count > 0) {
148             Slog.d(TAG, "Deleted " + count + " downloads owned by UID " + uid);
149         }
150     }
151 
152     /**
153      * Handle any broadcast related to a system notification.
154      */
handleNotificationBroadcast(Context context, Intent intent)155     private void handleNotificationBroadcast(Context context, Intent intent) {
156         final String action = intent.getAction();
157         if (Constants.ACTION_LIST.equals(action)) {
158             final long[] ids = intent.getLongArrayExtra(
159                     DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
160             sendNotificationClickedIntent(context, ids);
161 
162         } else if (Constants.ACTION_OPEN.equals(action)) {
163             final long id = ContentUris.parseId(intent.getData());
164             openDownload(context, id);
165             hideNotification(context, id);
166 
167         } else if (Constants.ACTION_HIDE.equals(action)) {
168             final long id = ContentUris.parseId(intent.getData());
169             hideNotification(context, id);
170         }
171     }
172 
173     /**
174      * Mark the given {@link DownloadManager#COLUMN_ID} as being acknowledged by
175      * user so it's not renewed later.
176      */
hideNotification(Context context, long id)177     private void hideNotification(Context context, long id) {
178         final int status;
179         final int visibility;
180 
181         final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
182         final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
183         try {
184             if (cursor.moveToFirst()) {
185                 status = getInt(cursor, Downloads.Impl.COLUMN_STATUS);
186                 visibility = getInt(cursor, Downloads.Impl.COLUMN_VISIBILITY);
187             } else {
188                 Log.w(TAG, "Missing details for download " + id);
189                 return;
190             }
191         } finally {
192             cursor.close();
193         }
194 
195         if (Downloads.Impl.isStatusCompleted(status) &&
196                 (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
197                 || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)) {
198             final ContentValues values = new ContentValues();
199             values.put(Downloads.Impl.COLUMN_VISIBILITY,
200                     Downloads.Impl.VISIBILITY_VISIBLE);
201             context.getContentResolver().update(uri, values, null, null);
202         }
203     }
204 
205     /**
206      * Start activity to display the file represented by the given
207      * {@link DownloadManager#COLUMN_ID}.
208      */
openDownload(Context context, long id)209     private void openDownload(Context context, long id) {
210         if (!OpenHelper.startViewIntent(context, id, Intent.FLAG_ACTIVITY_NEW_TASK)) {
211             Toast.makeText(context, R.string.download_no_application_title, Toast.LENGTH_SHORT)
212                     .show();
213         }
214     }
215 
216     /**
217      * Notify the owner of a running download that its notification was clicked.
218      */
sendNotificationClickedIntent(Context context, long[] ids)219     private void sendNotificationClickedIntent(Context context, long[] ids) {
220         final String packageName;
221         final String clazz;
222         final boolean isPublicApi;
223 
224         final Uri uri = ContentUris.withAppendedId(
225                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, ids[0]);
226         final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
227         try {
228             if (cursor.moveToFirst()) {
229                 packageName = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
230                 clazz = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
231                 isPublicApi = getInt(cursor, Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
232             } else {
233                 Log.w(TAG, "Missing details for download " + ids[0]);
234                 return;
235             }
236         } finally {
237             cursor.close();
238         }
239 
240         if (TextUtils.isEmpty(packageName)) {
241             Log.w(TAG, "Missing package; skipping broadcast");
242             return;
243         }
244 
245         Intent appIntent = null;
246         if (isPublicApi) {
247             appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
248             appIntent.setPackage(packageName);
249             appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids);
250 
251         } else { // legacy behavior
252             if (TextUtils.isEmpty(clazz)) {
253                 Log.w(TAG, "Missing class; skipping broadcast");
254                 return;
255             }
256 
257             appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
258             appIntent.setClassName(packageName, clazz);
259             appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids);
260 
261             if (ids.length == 1) {
262                 appIntent.setData(uri);
263             } else {
264                 appIntent.setData(Downloads.Impl.CONTENT_URI);
265             }
266         }
267 
268         getSystemFacade(context).sendBroadcast(appIntent);
269     }
270 }
271