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