1 /* 2 * Copyright (C) 2017 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.phone.testapps.embmsdownload; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.net.Uri; 23 import android.os.Bundle; 24 import android.os.Handler; 25 import android.os.HandlerThread; 26 import android.support.v7.widget.LinearLayoutManager; 27 import android.support.v7.widget.RecyclerView; 28 import android.telephony.MbmsDownloadSession; 29 import android.telephony.SubscriptionManager; 30 import android.telephony.mbms.DownloadRequest; 31 import android.telephony.mbms.DownloadStateCallback; 32 import android.telephony.mbms.FileInfo; 33 import android.telephony.mbms.FileServiceInfo; 34 import android.telephony.mbms.MbmsDownloadSessionCallback; 35 import android.util.Log; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.ArrayAdapter; 39 import android.widget.Button; 40 import android.widget.ImageView; 41 import android.widget.NumberPicker; 42 import android.widget.Spinner; 43 import android.widget.TextView; 44 import android.widget.Toast; 45 46 import java.io.File; 47 import java.util.ArrayList; 48 import java.util.Collections; 49 import java.util.List; 50 51 public class EmbmsTestDownloadApp extends Activity { 52 private static final String LOG_TAG = "EmbmsDownloadApp"; 53 54 public static final String DOWNLOAD_DONE_ACTION = 55 "com.android.phone.testapps.embmsdownload.DOWNLOAD_DONE"; 56 57 private static final String CUSTOM_EMBMS_TEMP_FILE_LOCATION = "customEmbmsTempFiles"; 58 59 private static final String FILE_AUTHORITY = "com.android.phone.testapps"; 60 private static final String FILE_DOWNLOAD_SCHEME = "filedownload"; 61 62 private static EmbmsTestDownloadApp sInstance; 63 64 private static final class ImageAdapter 65 extends RecyclerView.Adapter<ImageAdapter.ImageViewHolder> { 66 static class ImageViewHolder extends RecyclerView.ViewHolder { 67 public ImageView imageView; 68 public ImageViewHolder(ImageView view) { 69 super(view); 70 imageView = view; 71 } 72 } 73 74 private final List<Uri> mImageUris = new ArrayList<>(); 75 76 @Override 77 public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 78 ImageView view = new ImageView(parent.getContext()); 79 view.setAdjustViewBounds(true); 80 view.setMaxHeight(500); 81 return new ImageViewHolder(view); 82 } 83 84 @Override 85 public void onBindViewHolder(ImageViewHolder holder, int position) { 86 holder.imageView.setImageURI(mImageUris.get(position)); 87 } 88 89 @Override 90 public int getItemCount() { 91 return mImageUris.size(); 92 } 93 94 public void addImage(Uri uri) { 95 mImageUris.add(uri); 96 notifyDataSetChanged(); 97 } 98 } 99 100 private final class FileServiceInfoAdapter 101 extends ArrayAdapter<FileServiceInfo> { 102 public FileServiceInfoAdapter(Context context) { 103 super(context, android.R.layout.simple_spinner_item); 104 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 105 } 106 107 @Override 108 public View getView(int position, View convertView, ViewGroup parent) { 109 FileServiceInfo info = getItem(position); 110 TextView result = new TextView(EmbmsTestDownloadApp.this); 111 result.setText(info.getNameForLocale(info.getLocales().get(0))); 112 return result; 113 } 114 115 @Override 116 public View getDropDownView(int position, View convertView, ViewGroup parent) { 117 FileServiceInfo info = getItem(position); 118 TextView result = new TextView(EmbmsTestDownloadApp.this); 119 String text = "name=" 120 + info.getNameForLocale(info.getLocales().get(0)) 121 + ", " 122 + "numFiles=" 123 + info.getFiles().size(); 124 result.setText(text); 125 return result; 126 } 127 128 public void update(List<FileServiceInfo> services) { 129 clear(); 130 addAll(services); 131 } 132 } 133 134 private final class DownloadRequestAdapter 135 extends ArrayAdapter<DownloadRequest> { 136 public DownloadRequestAdapter(Context context) { 137 super(context, android.R.layout.simple_spinner_item); 138 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 139 } 140 141 @Override 142 public View getView(int position, View convertView, ViewGroup parent) { 143 DownloadRequest request = getItem(position); 144 TextView result = new TextView(EmbmsTestDownloadApp.this); 145 result.setText(request.getSourceUri().toSafeString()); 146 return result; 147 } 148 149 @Override 150 public View getDropDownView(int position, View convertView, ViewGroup parent) { 151 return getView(position, convertView, parent); 152 } 153 } 154 155 156 private MbmsDownloadSessionCallback mCallback = new MbmsDownloadSessionCallback() { 157 @Override 158 public void onError(int errorCode, String message) { 159 runOnUiThread(() -> Toast.makeText(EmbmsTestDownloadApp.this, 160 "Error " + errorCode + ": " + message, Toast.LENGTH_SHORT).show()); 161 } 162 163 @Override 164 public void onFileServicesUpdated(List<FileServiceInfo> services) { 165 EmbmsTestDownloadApp.this.runOnUiThread(() -> 166 Toast.makeText(EmbmsTestDownloadApp.this, 167 "Got services length " + services.size(), 168 Toast.LENGTH_SHORT).show()); 169 updateFileServicesList(services); 170 } 171 172 @Override 173 public void onMiddlewareReady() { 174 runOnUiThread(() -> Toast.makeText(EmbmsTestDownloadApp.this, 175 "Initialization done", Toast.LENGTH_SHORT).show()); 176 } 177 }; 178 179 private MbmsDownloadSession mDownloadManager; 180 private Handler mHandler; 181 private HandlerThread mHandlerThread; 182 private FileServiceInfoAdapter mFileServiceInfoAdapter; 183 private DownloadRequestAdapter mDownloadRequestAdapter; 184 private ImageAdapter mImageAdapter; 185 186 @Override 187 protected void onCreate(Bundle savedInstanceState) { 188 super.onCreate(savedInstanceState); 189 setContentView(R.layout.activity_main); 190 191 sInstance = this; 192 mHandlerThread = new HandlerThread("EmbmsDownloadWorker"); 193 mHandlerThread.start(); 194 mHandler = new Handler(mHandlerThread.getLooper()); 195 mFileServiceInfoAdapter = new FileServiceInfoAdapter(this); 196 mDownloadRequestAdapter = new DownloadRequestAdapter(this); 197 198 RecyclerView downloadedImages = (RecyclerView) findViewById(R.id.downloaded_images); 199 downloadedImages.setLayoutManager( 200 new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); 201 mImageAdapter = new ImageAdapter(); 202 downloadedImages.setAdapter(mImageAdapter); 203 204 Button bindButton = (Button) findViewById(R.id.bind_button); 205 bindButton.setOnClickListener((view) -> { 206 mDownloadManager = MbmsDownloadSession.create(this, mCallback, mHandler); 207 }); 208 209 Button setTempFileRootButton = (Button) findViewById(R.id.set_temp_root_button); 210 setTempFileRootButton.setOnClickListener((view) -> { 211 File downloadDir = new File(EmbmsTestDownloadApp.this.getFilesDir(), 212 CUSTOM_EMBMS_TEMP_FILE_LOCATION); 213 downloadDir.mkdirs(); 214 mDownloadManager.setTempFileRootDirectory(downloadDir); 215 Toast.makeText(EmbmsTestDownloadApp.this, 216 "temp file root set to " + downloadDir, Toast.LENGTH_SHORT).show(); 217 }); 218 219 Button getFileServicesButton = (Button) findViewById(R.id.get_file_services_button); 220 getFileServicesButton.setOnClickListener((view) -> mHandler.post(() -> { 221 mDownloadManager.requestUpdateFileServices(Collections.singletonList("Class1")); 222 })); 223 224 final Spinner serviceSelector = (Spinner) findViewById(R.id.available_file_services); 225 serviceSelector.setAdapter(mFileServiceInfoAdapter); 226 227 Button requestDlButton = (Button) findViewById(R.id.request_dl_button); 228 requestDlButton.setOnClickListener((view) -> { 229 if (mDownloadManager == null) { 230 Toast.makeText(EmbmsTestDownloadApp.this, 231 "No download service bound", Toast.LENGTH_SHORT).show(); 232 return; 233 } 234 FileServiceInfo serviceInfo = 235 (FileServiceInfo) serviceSelector.getSelectedItem(); 236 if (serviceInfo == null) { 237 Toast.makeText(EmbmsTestDownloadApp.this, 238 "No file service selected", Toast.LENGTH_SHORT).show(); 239 return; 240 } 241 242 performDownload(serviceInfo); 243 }); 244 245 Button requestCleanupButton = (Button) findViewById(R.id.request_cleanup_button); 246 requestCleanupButton.setOnClickListener((view) -> 247 SideChannel.triggerCleanup(EmbmsTestDownloadApp.this)); 248 249 Button requestSpuriousTempFilesButton = 250 (Button) findViewById(R.id.request_spurious_temp_files_button); 251 requestSpuriousTempFilesButton.setOnClickListener((view) -> 252 SideChannel.requestSpuriousTempFiles(EmbmsTestDownloadApp.this, 253 (FileServiceInfo) serviceSelector.getSelectedItem())); 254 255 NumberPicker downloadDelayPicker = (NumberPicker) findViewById(R.id.delay_factor); 256 downloadDelayPicker.setMinValue(1); 257 downloadDelayPicker.setMaxValue(50); 258 259 Button delayDownloadButton = (Button) findViewById(R.id.delay_download_button); 260 delayDownloadButton.setOnClickListener((view) -> 261 SideChannel.delayDownloads(EmbmsTestDownloadApp.this, 262 downloadDelayPicker.getValue())); 263 264 final Spinner downloadRequestSpinner = (Spinner) findViewById(R.id.active_downloads); 265 downloadRequestSpinner.setAdapter(mDownloadRequestAdapter); 266 267 Button cancelDownloadButton = (Button) findViewById(R.id.cancel_download_button); 268 cancelDownloadButton.setOnClickListener((view) -> { 269 if (mDownloadManager == null) { 270 Toast.makeText(EmbmsTestDownloadApp.this, 271 "No download service bound", Toast.LENGTH_SHORT).show(); 272 return; 273 } 274 DownloadRequest request = 275 (DownloadRequest) downloadRequestSpinner.getSelectedItem(); 276 mDownloadManager.cancelDownload(request); 277 mDownloadRequestAdapter.remove(request); 278 }); 279 280 Button registerProgressCallback = 281 (Button) findViewById(R.id.register_progress_callback_button); 282 registerProgressCallback.setOnClickListener((view) -> { 283 if (mDownloadManager == null) { 284 Toast.makeText(EmbmsTestDownloadApp.this, 285 "No download service bound", Toast.LENGTH_SHORT).show(); 286 return; 287 } 288 DownloadRequest req = (DownloadRequest) downloadRequestSpinner.getSelectedItem(); 289 if (req == null) { 290 Toast.makeText(EmbmsTestDownloadApp.this, 291 "No DownloadRequest Pending for progress...", Toast.LENGTH_SHORT).show(); 292 return; 293 } 294 mDownloadManager.registerStateCallback(req, new DownloadStateCallback( 295 DownloadStateCallback.PROGRESS_UPDATES) { 296 @Override 297 public void onProgressUpdated(DownloadRequest request, FileInfo fileInfo, 298 int currentDownloadSize, int fullDownloadSize, int currentDecodedSize, 299 int fullDecodedSize) { 300 Toast.makeText(EmbmsTestDownloadApp.this, 301 "Progress Updated (" + fileInfo + ") cd: " + currentDecodedSize 302 + " fd: " + fullDownloadSize, Toast.LENGTH_SHORT).show(); 303 } 304 305 @Override 306 public void onStateUpdated(DownloadRequest request, FileInfo fileInfo, int state) { 307 // only registered for state callback, this shouldn't happen! 308 Toast.makeText(EmbmsTestDownloadApp.this, 309 "State ERROR: received state update for callback that didn't filter it", 310 Toast.LENGTH_SHORT).show(); 311 } 312 }, sInstance.getMainThreadHandler()); 313 }); 314 315 Button registerStateCallback = 316 (Button) findViewById(R.id.register_state_callback_button); 317 registerStateCallback.setOnClickListener((view) -> { 318 if (mDownloadManager == null) { 319 Toast.makeText(EmbmsTestDownloadApp.this, 320 "No download service bound", Toast.LENGTH_SHORT).show(); 321 return; 322 } 323 DownloadRequest req = (DownloadRequest) downloadRequestSpinner.getSelectedItem(); 324 if (req == null) { 325 Toast.makeText(EmbmsTestDownloadApp.this, 326 "No DownloadRequest Pending for state...", Toast.LENGTH_SHORT).show(); 327 return; 328 } 329 mDownloadManager.registerStateCallback(req, new DownloadStateCallback( 330 DownloadStateCallback.STATE_UPDATES) { 331 @Override 332 public void onProgressUpdated(DownloadRequest request, FileInfo fileInfo, 333 int currentDownloadSize, int fullDownloadSize, int currentDecodedSize, 334 int fullDecodedSize) { 335 // only registered for state callback, this shouldn't happen! 336 Toast.makeText(EmbmsTestDownloadApp.this, 337 "Progress ERROR: received progress update for callback that didn't " 338 + "filter it", Toast.LENGTH_SHORT).show(); 339 } 340 341 @Override 342 public void onStateUpdated(DownloadRequest request, FileInfo fileInfo, int state) { 343 Toast.makeText(EmbmsTestDownloadApp.this, 344 "State Updated (" + fileInfo + ") state: " + state, 345 Toast.LENGTH_SHORT).show(); 346 } 347 }, sInstance.getMainThreadHandler()); 348 }); 349 350 Button registerAllCallbacks = 351 (Button) findViewById(R.id.register_all_callback_button); 352 registerAllCallbacks.setOnClickListener((view) -> { 353 if (mDownloadManager == null) { 354 Toast.makeText(EmbmsTestDownloadApp.this, 355 "No download service bound", Toast.LENGTH_SHORT).show(); 356 return; 357 } 358 DownloadRequest req = (DownloadRequest) downloadRequestSpinner.getSelectedItem(); 359 if (req == null) { 360 Toast.makeText(EmbmsTestDownloadApp.this, 361 "No DownloadRequest Pending for state...", Toast.LENGTH_SHORT).show(); 362 return; 363 } 364 mDownloadManager.registerStateCallback(req, new DownloadStateCallback() { 365 @Override 366 public void onProgressUpdated(DownloadRequest request, FileInfo fileInfo, 367 int currentDownloadSize, int fullDownloadSize, int currentDecodedSize, 368 int fullDecodedSize) { 369 Toast.makeText(EmbmsTestDownloadApp.this, 370 "Progress Updated (" + fileInfo + ") cd: " + currentDecodedSize 371 + " fd: " + fullDownloadSize, Toast.LENGTH_SHORT).show(); 372 } 373 374 @Override 375 public void onStateUpdated(DownloadRequest request, FileInfo fileInfo, int state) { 376 Toast.makeText(EmbmsTestDownloadApp.this, 377 "State Updated (" + fileInfo + ") state: " + state, 378 Toast.LENGTH_SHORT).show(); 379 } 380 }, sInstance.getMainThreadHandler()); 381 }); 382 } 383 384 @Override 385 protected void onDestroy() { 386 super.onDestroy(); 387 mHandlerThread.quit(); 388 sInstance = null; 389 } 390 391 public static EmbmsTestDownloadApp getInstance() { 392 return sInstance; 393 } 394 395 public void onDownloadFailed(int result) { 396 runOnUiThread(() -> 397 Toast.makeText(this, "Download failed: " + result, Toast.LENGTH_SHORT).show()); 398 } 399 400 // TODO: assumes that process does not get killed. Replace with more robust alternative 401 public void onDownloadDone(Uri fileLocation) { 402 Log.i(LOG_TAG, "File completed: " + fileLocation); 403 File imageFile = new File(fileLocation.getPath()); 404 if (!imageFile.exists()) { 405 Toast.makeText(this, "Download done but destination doesn't exist", Toast.LENGTH_SHORT) 406 .show(); 407 return; 408 } 409 mImageAdapter.addImage(fileLocation); 410 } 411 412 private void updateFileServicesList(List<FileServiceInfo> services) { 413 runOnUiThread(() -> mFileServiceInfoAdapter.update(services)); 414 } 415 416 private void performDownload(FileServiceInfo info) { 417 Uri.Builder sourceUriBuilder = new Uri.Builder() 418 .scheme(FILE_DOWNLOAD_SCHEME) 419 .authority(FILE_AUTHORITY); 420 if (info.getServiceId().contains("2")) { 421 sourceUriBuilder.path("/*"); 422 } else { 423 sourceUriBuilder.path("/image.png"); 424 } 425 426 Intent completionIntent = new Intent(DOWNLOAD_DONE_ACTION); 427 completionIntent.setClass(this, DownloadCompletionReceiver.class); 428 429 DownloadRequest request = new DownloadRequest.Builder(sourceUriBuilder.build()) 430 .setServiceInfo(info) 431 .setAppIntent(completionIntent) 432 .setSubscriptionId(SubscriptionManager.getDefaultSubscriptionId()) 433 .build(); 434 435 mDownloadManager.download(request); 436 mDownloadRequestAdapter.add(request); 437 } 438 } 439