1 /* 2 * Copyright (C) 2016 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.retaildemo; 18 19 import android.app.DownloadManager; 20 import android.app.ProgressDialog; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.database.Cursor; 26 import android.net.ConnectivityManager; 27 import android.net.NetworkInfo; 28 import android.net.Uri; 29 import android.os.Handler; 30 import android.os.HandlerThread; 31 import android.os.Looper; 32 import android.os.Message; 33 import android.support.annotation.VisibleForTesting; 34 import android.util.Log; 35 import android.view.ContextThemeWrapper; 36 37 import java.io.File; 38 import java.io.IOException; 39 import java.net.HttpURLConnection; 40 import java.net.URL; 41 42 /** 43 * Downloads the video from the specified url. If the video is previously downloaded, then uses 44 * that but checks if there is a more recent version of the video available. 45 */ 46 class DownloadVideoTask { 47 private static final String TAG = "DownloadVideoTask"; 48 private static final boolean DEBUG = false; 49 50 static final int MSG_CHECK_FOR_UPDATE = 1; 51 static final int MSG_DOWNLOAD_COMPLETE = 2; 52 static final int MSG_CLEANUP_DOWNLOAD_DIR = 3; 53 54 private static final int CLEANUP_DELAY_MILLIS = 2 * 1000; // 2 seconds 55 56 private final Injector mInjector; 57 private final Context mContext; 58 private final DownloadManager mDlm; 59 private final File mDownloadFile; 60 private final ResultListener mListener; 61 62 private Handler mHandler; 63 64 private ProgressDialog mProgressDialog; 65 private DownloadResultReceiver mDownloadReceiver; 66 private NetworkChangeReceiver mNetworkChangeReceiver; 67 private String mDownloadUrl; 68 private long mVideoDownloadId; 69 private long mVideoUpdateDownloadId; 70 private String mDownloadedPath; 71 private boolean mVideoAlreadySet; 72 private File mPreloadVideoFile; 73 DownloadVideoTask(Context context, String downloadPath, File preloadVideoFile, ResultListener listener)74 public DownloadVideoTask(Context context, String downloadPath, File preloadVideoFile, 75 ResultListener listener) { 76 this(context, downloadPath, preloadVideoFile, listener, new Injector(context)); 77 } 78 79 @VisibleForTesting DownloadVideoTask(Context context, String downloadPath, File preloadVideoFile, ResultListener listener, Injector injector)80 DownloadVideoTask(Context context, String downloadPath, File preloadVideoFile, 81 ResultListener listener, Injector injector) { 82 mInjector = injector; 83 mContext = context; 84 mDownloadFile = new File(downloadPath); 85 mListener = listener; 86 mPreloadVideoFile = preloadVideoFile; 87 mDlm = injector.getDownloadManager(); 88 mDownloadUrl = injector.getDownloadUrl(); 89 } 90 run()91 public void run() { 92 mDownloadReceiver = new DownloadResultReceiver(); 93 mContext.registerReceiver(mDownloadReceiver, 94 new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); 95 96 mHandler = mInjector.getHandler(this); 97 98 mVideoAlreadySet = 99 mDownloadFile.exists() || mPreloadVideoFile.exists(); 100 // If file already exists, no need to download it again. 101 if (mVideoAlreadySet) { 102 if (DEBUG) Log.d(TAG, "Video already exists at either " + mDownloadFile.getPath() 103 + " or " + mPreloadVideoFile + ", checking for an update... "); 104 mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_FOR_UPDATE)); 105 } else { 106 if (!isConnectedToNetwork()) { 107 mListener.onError(); 108 mNetworkChangeReceiver = new NetworkChangeReceiver(); 109 mContext.registerReceiver(mNetworkChangeReceiver, 110 new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); 111 return; 112 } 113 startDownload(); 114 } 115 } 116 startDownload()117 private void startDownload() { 118 final DownloadManager.Request request = createDownloadRequest(); 119 mVideoDownloadId = mDlm.enqueue(request); 120 if (DEBUG) Log.d(TAG, "Started downloading the video at " + mDownloadUrl 121 + " to " + mDownloadFile.getPath()); 122 showProgressDialog(); 123 } 124 createDownloadRequest()125 private DownloadManager.Request createDownloadRequest() { 126 final DownloadManager.Request request = new DownloadManager.Request( 127 Uri.parse(mDownloadUrl)); 128 request.setDestinationUri(Uri.fromFile(mDownloadFile)); 129 return request; 130 } 131 132 private class DownloadResultReceiver extends BroadcastReceiver { 133 @Override onReceive(Context context, Intent intent)134 public void onReceive(Context context, Intent intent) { 135 if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { 136 return; 137 } 138 139 final long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); 140 if (id == mVideoDownloadId) { 141 final int status = checkDownloadsAndSetVideo(id); 142 if (status == DownloadManager.STATUS_SUCCESSFUL || 143 status == DownloadManager.STATUS_FAILED) { 144 if (mProgressDialog != null) { 145 mProgressDialog.dismiss(); 146 } 147 if (mDownloadReceiver != null) { 148 mContext.unregisterReceiver(mDownloadReceiver); 149 mDownloadReceiver = null; 150 } 151 if (status == DownloadManager.STATUS_FAILED) { 152 mListener.onError(); 153 } 154 } 155 } else if (id == mVideoUpdateDownloadId) { 156 mHandler.sendMessage(mHandler.obtainMessage(MSG_DOWNLOAD_COMPLETE)); 157 } 158 } 159 }; 160 161 final class ThreadHandler extends Handler { ThreadHandler(Looper looper)162 public ThreadHandler(Looper looper) { 163 super(looper); 164 } 165 166 @Override handleMessage(Message msg)167 public void handleMessage(Message msg) { 168 switch (msg.what) { 169 case MSG_CHECK_FOR_UPDATE: 170 if (!isConnectedToNetwork()) { 171 mNetworkChangeReceiver = new NetworkChangeReceiver(); 172 mContext.registerReceiver(mNetworkChangeReceiver, 173 new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); 174 return; 175 } 176 HttpURLConnection conn = null; 177 try { 178 conn = mInjector.openConnection(mDownloadUrl); 179 final long lastModified = mDownloadFile.lastModified(); 180 conn.setIfModifiedSince(lastModified); 181 conn.connect(); 182 if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { 183 return; 184 } 185 final DownloadManager.Request request = createDownloadRequest(); 186 mVideoUpdateDownloadId = mDlm.enqueue(request); 187 if (DEBUG) Log.d(TAG, "Started downloading the updated video at " 188 + mDownloadUrl); 189 } catch (IOException e) { 190 Log.e(TAG, "Error while checking for an updated video", e); 191 } finally { 192 if (conn != null) { 193 conn.disconnect(); 194 } 195 } 196 break; 197 case MSG_DOWNLOAD_COMPLETE: 198 checkDownloadsAndSetVideo(mVideoUpdateDownloadId); 199 break; 200 case MSG_CLEANUP_DOWNLOAD_DIR: 201 // If the video was downloaded to the same location as we needed, then 202 // nothing else to do. 203 if (mDownloadFile.getPath().equals(mDownloadedPath)) { 204 return; 205 } 206 if (mDownloadFile.exists()) { 207 mDownloadFile.delete(); 208 } 209 if (new File(mDownloadedPath).renameTo(mDownloadFile)) { 210 mListener.onFileDownloaded(mDownloadFile.getPath()); 211 final String downloadFileName = getFileBaseName(mDownloadFile.getName()); 212 // Delete other files in the directory 213 for (File file : mDownloadFile.getParentFile().listFiles()) { 214 if (getFileBaseName(file.getName()).startsWith(downloadFileName) 215 && !file.getPath().equals(mDownloadFile.getPath())) { 216 file.delete(); 217 } 218 } 219 } 220 break; 221 } 222 } 223 } 224 checkDownloadsAndSetVideo(long downloadId)225 private int checkDownloadsAndSetVideo(long downloadId) { 226 final DownloadManager.Query query = 227 new DownloadManager.Query().setFilterById(downloadId); 228 Cursor cursor = mDlm.query(query); 229 try { 230 if (cursor != null & cursor.moveToFirst()) { 231 final int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); 232 final int status = cursor.getInt(columnIndex); 233 if (status == DownloadManager.STATUS_SUCCESSFUL) { 234 final String fileUri = cursor.getString( 235 cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); 236 mDownloadedPath = Uri.parse(fileUri).getPath(); 237 if (DEBUG) Log.d(TAG, "Video successfully downloaded at " + mDownloadedPath); 238 mListener.onFileDownloaded(mDownloadedPath); 239 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CLEANUP_DOWNLOAD_DIR), 240 CLEANUP_DELAY_MILLIS); 241 } 242 return status; 243 } 244 } finally { 245 if (cursor != null) { 246 cursor.close(); 247 } 248 if (mNetworkChangeReceiver != null) { 249 mContext.unregisterReceiver(mNetworkChangeReceiver); 250 mNetworkChangeReceiver = null; 251 } 252 } 253 return -1; 254 } 255 256 private class NetworkChangeReceiver extends BroadcastReceiver { 257 @Override onReceive(Context context, Intent intent)258 public void onReceive(Context context, Intent intent) { 259 if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()) 260 && isConnectedToNetwork()) { 261 if (mVideoAlreadySet) { 262 mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_FOR_UPDATE)); 263 } else { 264 startDownload(); 265 } 266 } 267 } 268 }; 269 showProgressDialog()270 private void showProgressDialog() { 271 mProgressDialog = mInjector.getProgressDialog(); 272 mProgressDialog.show(); 273 } 274 isConnectedToNetwork()275 private boolean isConnectedToNetwork() { 276 ConnectivityManager cm = mInjector.getConnectivityManager(); 277 NetworkInfo info = cm.getActiveNetworkInfo(); 278 return info != null && info.isConnected(); 279 } 280 getFileBaseName(String fileName)281 private String getFileBaseName(String fileName) { 282 final int pos = fileName.lastIndexOf("."); 283 return pos > 0 ? fileName.substring(0, pos) : fileName; 284 } 285 286 interface ResultListener { onFileDownloaded(String downloadedFilePath)287 void onFileDownloaded(String downloadedFilePath); onError()288 void onError(); 289 } 290 291 /** 292 * Unit test will subclass this to inject mocks. 293 */ 294 @VisibleForTesting 295 static class Injector { 296 private final Context mContext; 297 Injector(Context context)298 Injector(Context context) { 299 mContext = context; 300 } 301 getDownloadManager()302 DownloadManager getDownloadManager() { 303 return (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); 304 } 305 getDownloadUrl()306 String getDownloadUrl() { 307 return mContext.getString(R.string.retail_demo_video_download_url); 308 } 309 getConnectivityManager()310 ConnectivityManager getConnectivityManager() { 311 return (ConnectivityManager) mContext.getSystemService( 312 Context.CONNECTIVITY_SERVICE); 313 } 314 getHandler(DownloadVideoTask task)315 Handler getHandler(DownloadVideoTask task) { 316 // Initialize handler 317 HandlerThread thread = new HandlerThread(TAG); 318 thread.start(); 319 return task.new ThreadHandler(thread.getLooper()); 320 } 321 getProgressDialog()322 ProgressDialog getProgressDialog() { 323 final ProgressDialog dialog = new ProgressDialog( 324 new ContextThemeWrapper(mContext, android.R.style.Theme_Material_Light_Dialog)); 325 dialog.setMessage(mContext.getString(R.string.downloading_video_msg)); 326 dialog.setIndeterminate(false); 327 dialog.setCancelable(false); 328 dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); 329 return dialog; 330 } 331 openConnection(String downloadUri)332 HttpURLConnection openConnection(String downloadUri) throws IOException { 333 return (HttpURLConnection) new URL(downloadUri).openConnection(); 334 } 335 } 336 }