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
17package com.android.phone.testapps.embmsdownload;
18
19import android.app.Activity;
20import android.content.Context;
21import android.content.Intent;
22import android.net.Uri;
23import android.os.Bundle;
24import android.os.Handler;
25import android.os.HandlerThread;
26import android.support.v7.widget.LinearLayoutManager;
27import android.support.v7.widget.RecyclerView;
28import android.telephony.MbmsDownloadSession;
29import android.telephony.SubscriptionManager;
30import android.telephony.mbms.DownloadRequest;
31import android.telephony.mbms.DownloadStateCallback;
32import android.telephony.mbms.FileInfo;
33import android.telephony.mbms.FileServiceInfo;
34import android.telephony.mbms.MbmsDownloadSessionCallback;
35import android.util.Log;
36import android.view.View;
37import android.view.ViewGroup;
38import android.widget.ArrayAdapter;
39import android.widget.Button;
40import android.widget.ImageView;
41import android.widget.NumberPicker;
42import android.widget.Spinner;
43import android.widget.TextView;
44import android.widget.Toast;
45
46import java.io.File;
47import java.util.ArrayList;
48import java.util.Collections;
49import java.util.List;
50
51public 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