Implement dialogs for wifi required + recommended limits.
Steve Howard [Fri, 17 Sep 2010 23:45:58 +0000 (16:45 -0700)]
This change extends the original work to add a size limit over which
wifi is required to download a file.

First, this change adds a second size limit, over which wifi is
recommended but not required.  The user has the option to bypass this
limit.

Second, this change implements dialogs shown to the user when either
limit is exceeded.  These dialogs are shown by the background download
manager service when a download is started and found to be over the
limit (and wifi is not connected).

I'm including one small fix to the unit tests needed from the previous
change.

Change-Id: Ia0f0acaa7b0d00e98355925c3446c0472048df10

AndroidManifest.xml
res/values/strings.xml
src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadThread.java
src/com/android/providers/downloads/RealSystemFacade.java
src/com/android/providers/downloads/SizeLimitActivity.java [new file with mode: 0644]
src/com/android/providers/downloads/SystemFacade.java
tests/src/com/android/providers/downloads/FakeSystemFacade.java
tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java

index 6108ac2..f7001ff 100644 (file)
@@ -76,5 +76,8 @@
                 <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
             </intent-filter>
         </receiver>
+
+        <activity android:name=".SizeLimitActivity"
+                  android:launchMode="singleTask" />
     </application>
 </manifest>
index 1623fbe..543c95c 100644 (file)
         [CHAR LIMIT=24] -->
     <string name="notification_need_wifi_for_size">Need wifi due to size</string>
 
+    <!-- Title for dialog when a download exceeds the carrier-specified maximum size of downloads
+        over the mobile network and WiFi is required.  The user has the choice to either queue the
+        download to start next time WiFi is available or cancel the download altogether. [CHAR
+        LIMIT=50] -->
+    <string name="wifi_required_title">Download too large for operator network</string>
+
+    <!-- Text for dialog when a download exceeds the carrier-specified maximum size of downloads
+        over the mobile network and WiFi is required.  The user has the choice to either queue the
+        download to start next time WiFi is available or cancel the download altogether. [CHAR
+        LIMIT=200] -->
+    <string name="wifi_required_body">You must use WiFi to complete this <xliff:g id="size"
+        example="12.3KB">%s</xliff:g> download.\n\nClick \"<xliff:g id="queue_text"
+        example="Queue">%s</xliff:g>\" below to begin this download the next time you are connected
+        to a WiFi network.</string>
+
+    <!-- Title for dialog when a download exceeds the carrier-specified recommended maximum size of
+        downloads over the mobile network, and the user may choose to start the download over mobile
+        anyway or to queue for download to start next time a WiFi connection is available [CHAR
+        LIMIT=50] -->
+    <string name="wifi_recommended_title">Queue for download later?</string>
+
+    <!-- Text for dialog when a download exceeds the carrier-specified recommended maximum size of
+        downloads over the mobile network, and the user may choose to start the download over mobile
+        anyway or to queue for download to start next time a WiFi connection is available [CHAR
+        LIMIT=200] -->
+    <string name="wifi_recommended_body">Starting this <xliff:g id="size" example="12.3KB">%s
+        </xliff:g> download now may shorten your battery life and/or result in excessive usage of
+        your mobile data connection, which can lead to charges by your mobile operator depending on
+        your data plan.\n\nClick \"<xliff:g id="queue_text" example="Queue">%s</xliff:g>\" below to
+        begin this download the next time you are connected to a WiFi network.</string>
+
+
+    <!-- Text for button to queue a download to start next time WiFi is available [CHAR LIMIT=25]
+        -->
+    <string name="button_queue_for_wifi">Queue</string>
+
+    <!-- Text for button to cancel a download because it's too large to proceed over the mobile
+         network and the user does not want to queue it for WiFi [CHAR LIMIT=25] -->
+    <string name="button_cancel_download">Cancel</string>
+
+    <!-- Text for button to start a download over the mobile connection now, even though it's over
+         the carrier-specified recommended maximum size for downloads over the mobile connection
+         [CHAR LIMIT=25] -->
+    <string name="button_start_now">Start now</string>
+
 
 </resources>
index eb9ac4b..467af83 100644 (file)
@@ -87,6 +87,8 @@ public class DownloadInfo {
             info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
             info.mTitle = getString(info.mTitle, Downloads.Impl.COLUMN_TITLE);
             info.mDescription = getString(info.mDescription, Downloads.Impl.COLUMN_DESCRIPTION);
+            info.mBypassRecommendedSizeLimit =
+                    getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
 
             synchronized (this) {
                 info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL);
@@ -159,6 +161,37 @@ public class DownloadInfo {
         }
     }
 
+    // the following NETWORK_* constants are used to indicates specfic reasons for disallowing a
+    // download from using a network, since specific causes can require special handling
+
+    /**
+     * The network is usable for the given download.
+     */
+    public static final int NETWORK_OK = 1;
+
+    /**
+     * The network is unusuable for some unspecified reason.
+     */
+    public static final int NETWORK_UNUSABLE_GENERIC = 2;
+
+    /**
+     * The download exceeds the maximum size for this network.
+     */
+    public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
+
+    /**
+     * The download exceeds the recommended maximum size for this network, the user must confirm for
+     * this download to proceed without WiFi.
+     */
+    public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
+
+    /**
+     * For intents used to notify the user that a download exceeds a size threshold, if this extra
+     * is true, WiFi is required for this download size; otherwise, it is only recommended.
+     */
+    public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
+
+
     public long mId;
     public String mUri;
     public boolean mNoIntegrity;
@@ -188,6 +221,7 @@ public class DownloadInfo {
     public boolean mAllowRoaming;
     public String mTitle;
     public String mDescription;
+    public int mBypassRecommendedSizeLimit;
     public String mPausedReason;
 
     public int mFuzz;
@@ -307,7 +341,7 @@ public class DownloadInfo {
         if (mStatus == Downloads.Impl.STATUS_RUNNING_PAUSED) {
             if (mNumFailed == 0) {
                 // download is waiting for network connectivity to return before it can resume
-                return canUseNetwork();
+                return checkCanUseNetwork() == NETWORK_OK;
             }
             if (restartTime() < now) {
                 // download was waiting for a delayed restart, and the delay has expired
@@ -333,19 +367,17 @@ public class DownloadInfo {
 
     /**
      * Returns whether this download is allowed to use the network.
+     * @return one of the NETWORK_* constants
      */
-    public boolean canUseNetwork() {
+    public int checkCanUseNetwork() {
         Integer networkType = mSystemFacade.getActiveNetworkType();
         if (networkType == null) {
-            return false;
-        }
-        if (!isNetworkTypeAllowed(networkType)) {
-            return false;
+            return NETWORK_UNUSABLE_GENERIC;
         }
         if (!isRoamingAllowed() && mSystemFacade.isNetworkRoaming()) {
-            return false;
+            return NETWORK_UNUSABLE_GENERIC;
         }
-        return true;
+        return checkIsNetworkTypeAllowed(networkType);
     }
 
     private boolean isRoamingAllowed() {
@@ -359,20 +391,16 @@ public class DownloadInfo {
     /**
      * Check if this download can proceed over the given network type.
      * @param networkType a constant from ConnectivityManager.TYPE_*.
+     * @return one of the NETWORK_* constants
      */
-    private boolean isNetworkTypeAllowed(int networkType) {
+    private int checkIsNetworkTypeAllowed(int networkType) {
         if (mIsPublicApi) {
             int flag = translateNetworkTypeToApiFlag(networkType);
             if ((flag & mAllowedNetworkTypes) == 0) {
-                return false;
+                return NETWORK_UNUSABLE_GENERIC;
             }
         }
-        if (!isSizeAllowedForNetwork(networkType)) {
-            mPausedReason = mContext.getResources().getString(
-                    R.string.notification_need_wifi_for_size);
-            return false;
-        }
-        return true;
+        return checkSizeAllowedForNetwork(networkType);
     }
 
     /**
@@ -397,19 +425,27 @@ public class DownloadInfo {
 
     /**
      * Check if the download's size prohibits it from running over the current network.
+     * @return one of the NETWORK_* constants
      */
-    private boolean isSizeAllowedForNetwork(int networkType) {
+    private int checkSizeAllowedForNetwork(int networkType) {
         if (mTotalBytes <= 0) {
-            return true; // we don't know the size yet
+            return NETWORK_OK; // we don't know the size yet
         }
         if (networkType == ConnectivityManager.TYPE_WIFI) {
-            return true; // anything goes over wifi
+            return NETWORK_OK; // anything goes over wifi
         }
         Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile();
-        if (maxBytesOverMobile == null) {
-            return true; // no limit
+        if (maxBytesOverMobile != null && mTotalBytes > maxBytesOverMobile) {
+            return NETWORK_UNUSABLE_DUE_TO_SIZE;
+        }
+        if (mBypassRecommendedSizeLimit == 0) {
+            Long recommendedMaxBytesOverMobile = mSystemFacade.getRecommendedMaxBytesOverMobile();
+            if (recommendedMaxBytesOverMobile != null
+                    && mTotalBytes > recommendedMaxBytesOverMobile) {
+                return NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
+            }
         }
-        return mTotalBytes <= maxBytesOverMobile;
+        return NETWORK_OK;
     }
 
     void start(long now) {
@@ -505,4 +541,16 @@ public class DownloadInfo {
                 && Downloads.Impl.isStatusSuccess(mStatus)
                 && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mMimeType);
     }
+
+    void notifyPauseDueToSize(boolean isWifiRequired) {
+        mPausedReason = mContext.getResources().getString(
+                R.string.notification_need_wifi_for_size);
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setData(getAllDownloadsUri());
+        intent.setClassName(SizeLimitActivity.class.getPackage().getName(),
+                SizeLimitActivity.class.getName());
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRA_IS_WIFI_REQUIRED, isWifiRequired);
+        mContext.startActivity(intent);
+    }
 }
index 102c611..2606501 100644 (file)
@@ -57,7 +57,7 @@ public final class DownloadProvider extends ContentProvider {
     /** Database filename */
     private static final String DB_NAME = "downloads.db";
     /** Current database version */
-    private static final int DB_VERSION = 103;
+    private static final int DB_VERSION = 104;
     /** Name of table in the database */
     private static final String DB_TABLE = "downloads";
 
@@ -223,6 +223,11 @@ public final class DownloadProvider extends ContentProvider {
                     makeCacheDownloadsInvisible(db);
                     break;
 
+                case 104:
+                    addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT,
+                            "INTEGER NOT NULL DEFAULT 0");
+                    break;
+
                 default:
                     throw new IllegalStateException("Don't know how to upgrade to " + version);
             }
@@ -839,7 +844,9 @@ public final class DownloadProvider extends ContentProvider {
 
             Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
             boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING;
-            if (isRestart) {
+            boolean isUserBypassingSizeLimit =
+                values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
+            if (isRestart || isUserBypassingSizeLimit) {
                 startService = true;
             }
         }
index 57007f4..79778b0 100644 (file)
@@ -240,7 +240,13 @@ public class DownloadThread extends Thread {
      * Check if current connectivity is valid for this request.
      */
     private void checkConnectivity(State state) throws StopRequest {
-        if (!mInfo.canUseNetwork()) {
+        int networkUsable = mInfo.checkCanUseNetwork();
+        if (networkUsable != DownloadInfo.NETWORK_OK) {
+            if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
+                mInfo.notifyPauseDueToSize(true);
+            } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
+                mInfo.notifyPauseDueToSize(false);
+            }
             throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
         }
     }
index 421fc2b..ce86f73 100644 (file)
@@ -71,6 +71,16 @@ class RealSystemFacade implements SystemFacade {
     }
 
     @Override
+    public Long getRecommendedMaxBytesOverMobile() {
+        try {
+            return Settings.Secure.getLong(mContext.getContentResolver(),
+                    Settings.Secure.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE);
+        } catch (SettingNotFoundException exc) {
+            return null;
+        }
+    }
+
+    @Override
     public void sendBroadcast(Intent intent) {
         mContext.sendBroadcast(intent);
     }
diff --git a/src/com/android/providers/downloads/SizeLimitActivity.java b/src/com/android/providers/downloads/SizeLimitActivity.java
new file mode 100644 (file)
index 0000000..53e70de
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Downloads;
+import android.text.format.Formatter;
+import android.util.Log;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Activity to show dialogs to the user when a download exceeds a limit on download sizes for
+ * mobile networks.  This activity gets started by the background download service when a download's
+ * size is discovered to be exceeded one of these thresholds.
+ */
+public class SizeLimitActivity extends Activity
+        implements DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
+    private Dialog mDialog;
+    private Queue<Intent> mDownloadsToShow = new LinkedList<Intent>();
+    private Uri mCurrentUri;
+    private Intent mCurrentIntent;
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        Intent intent = getIntent();
+        if (intent != null) {
+            mDownloadsToShow.add(intent);
+            setIntent(null);
+            showNextDialog();
+        }
+        if (mDialog != null && !mDialog.isShowing()) {
+            mDialog.show();
+        }
+    }
+
+    private void showNextDialog() {
+        if (mDialog != null) {
+            return;
+        }
+
+        if (mDownloadsToShow.isEmpty()) {
+            finish();
+            return;
+        }
+
+        mCurrentIntent = mDownloadsToShow.poll();
+        mCurrentUri = mCurrentIntent.getData();
+        Cursor cursor = getContentResolver().query(mCurrentUri, null, null, null, null);
+        try {
+            if (!cursor.moveToFirst()) {
+                Log.e(Constants.TAG, "Empty cursor for URI " + mCurrentUri);
+                dialogClosed();
+                return;
+            }
+            showDialog(cursor);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void showDialog(Cursor cursor) {
+        int size = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES));
+        String sizeString = Formatter.formatFileSize(this, size);
+        String queueText = getString(R.string.button_queue_for_wifi);
+        boolean isWifiRequired =
+            mCurrentIntent.getExtras().getBoolean(DownloadInfo.EXTRA_IS_WIFI_REQUIRED);
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        if (isWifiRequired) {
+            builder.setTitle(R.string.wifi_required_title)
+                    .setMessage(getString(R.string.wifi_required_body, sizeString, queueText))
+                    .setPositiveButton(R.string.button_queue_for_wifi, this)
+                    .setNegativeButton(R.string.button_cancel_download, this);
+        } else {
+            builder.setTitle(R.string.wifi_recommended_title)
+                    .setMessage(getString(R.string.wifi_recommended_body, sizeString, queueText))
+                    .setPositiveButton(R.string.button_start_now, this)
+                    .setNegativeButton(R.string.button_queue_for_wifi, this);
+        }
+        mDialog = builder.setOnCancelListener(this).show();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        dialogClosed();
+    }
+
+    private void dialogClosed() {
+        mDialog = null;
+        mCurrentUri = null;
+        showNextDialog();
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        boolean isRequired =
+                mCurrentIntent.getExtras().getBoolean(DownloadInfo.EXTRA_IS_WIFI_REQUIRED);
+        if (isRequired && which == AlertDialog.BUTTON_NEGATIVE) {
+            getContentResolver().delete(mCurrentUri, null, null);
+        } else if (!isRequired && which == AlertDialog.BUTTON_POSITIVE) {
+            ContentValues values = new ContentValues();
+            values.put(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT, true);
+            getContentResolver().update(mCurrentUri, values , null, null);
+        }
+        dialogClosed();
+    }
+}
index 50624c3..ed0d330 100644 (file)
@@ -30,6 +30,13 @@ interface SystemFacade {
     public Long getMaxBytesOverMobile();
 
     /**
+     * @return recommended maximum size, in bytes, of downloads that may go over a mobile
+     * connection; or null if there's no recommended limit.  The user will have the option to bypass
+     * this limit.
+     */
+    public Long getRecommendedMaxBytesOverMobile();
+
+    /**
      * Send a broadcast intent.
      */
     public void sendBroadcast(Intent intent);
index d80bd4a..5263015 100644 (file)
@@ -18,6 +18,7 @@ public class FakeSystemFacade implements SystemFacade {
     Integer mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
     boolean mIsRoaming = false;
     Long mMaxBytesOverMobile = null;
+    Long mRecommendedMaxBytesOverMobile = null;
     List<Intent> mBroadcastsSent = new ArrayList<Intent>();
     Map<Long,Notification> mActiveNotifications = new HashMap<Long,Notification>();
     List<Notification> mCanceledNotifications = new ArrayList<Notification>();
@@ -43,6 +44,10 @@ public class FakeSystemFacade implements SystemFacade {
         return mMaxBytesOverMobile ;
     }
 
+    public Long getRecommendedMaxBytesOverMobile() {
+        return mRecommendedMaxBytesOverMobile ;
+    }
+
     @Override
     public void sendBroadcast(Intent intent) {
         mBroadcastsSent.add(intent);
index 554cc1e..6c81bc6 100644 (file)
@@ -536,7 +536,6 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
 
     public void testEmptyFields() throws Exception {
         Download download = enqueueRequest(getRequest());
-        assertNull(download.getStringField(DownloadManager.COLUMN_LOCAL_URI));
         assertEquals("", download.getStringField(DownloadManager.COLUMN_TITLE));
         assertEquals("", download.getStringField(DownloadManager.COLUMN_DESCRIPTION));
         assertNull(download.getStringField(DownloadManager.COLUMN_MEDIA_TYPE));