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