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