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