Skip to content
This repository was archived by the owner on Dec 3, 2019. It is now read-only.

Commit 41d9049

Browse files
authored
Merge pull request #19 from yringler/8-prompt-for-data-usage
8: make service for net permission/ability.
2 parents f42ce27 + 34fbbc9 commit 41d9049

11 files changed

+370
-27
lines changed

.vscode/launch.json

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Launch on iOS",
9+
"type": "nativescript",
10+
"request": "launch",
11+
"platform": "ios",
12+
"appRoot": "${workspaceRoot}",
13+
"sourceMaps": true,
14+
"watch": true
15+
},
16+
{
17+
"name": "Test on iOS",
18+
"type": "nativescript",
19+
"request": "launch",
20+
"platform": "ios",
21+
"appRoot": "${workspaceRoot}",
22+
"sourceMaps": true,
23+
"watch": false,
24+
"stopOnEntry": true,
25+
"launchTests": true,
26+
"tnsArgs": [
27+
"--justlaunch"
28+
]
29+
},
30+
{
31+
"name": "Attach on iOS",
32+
"type": "nativescript",
33+
"request": "attach",
34+
"platform": "ios",
35+
"appRoot": "${workspaceRoot}",
36+
"sourceMaps": true,
37+
"watch": false
38+
},
39+
{
40+
"name": "Launch on Android",
41+
"type": "nativescript",
42+
"request": "launch",
43+
"platform": "android",
44+
"appRoot": "${workspaceRoot}",
45+
"sourceMaps": true,
46+
"watch": true,
47+
"tnsArgs": ["--debug-brk", "--bundle"]
48+
},
49+
{
50+
"name": "Test on Android",
51+
"type": "nativescript",
52+
"request": "launch",
53+
"platform": "android",
54+
"appRoot": "${workspaceRoot}",
55+
"sourceMaps": true,
56+
"watch": false,
57+
"stopOnEntry": true,
58+
"launchTests": true,
59+
"tnsArgs": [
60+
"--justlaunch"
61+
]
62+
},
63+
{
64+
"name": "Attach on Android",
65+
"type": "nativescript",
66+
"request": "attach",
67+
"platform": "android",
68+
"appRoot": "${workspaceRoot}",
69+
"sourceMaps": true,
70+
"watch": false,
71+
}
72+
]
73+
}

App_Resources/Android/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
1414
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
1515
<uses-permission android:name="android.permission.INTERNET"/>
16+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
1617

1718
<application
1819
android:name="com.tns.NativeScriptApplication"

src/app/home/home.component.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
<ActionBar class="action-bar">
2-
<Label class="action-bar-title" text="Home"></Label>
2+
<ActionItem ios.systemicon="2" android.systemicon="ic_menu_preferences" (tap)="updatePermissionSetting()"></ActionItem>
33
</ActionBar>
44

55
<StackLayout class="page">
66
<daily-lesson
77
[track]="track"
8-
*ngFor="let track of todaysLessons$ | async"></daily-lesson>
8+
*ngFor="let track of todaysLessons$ | async"></daily-lesson>
9+
<Progress
10+
*ngIf="currentProgress > 0 && currentProgress < 100"
11+
[value]="currentProgress"
12+
maxvalue="100"
13+
></Progress>
914
</StackLayout>

src/app/home/home.component.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,51 @@
1-
import { Component, OnInit } from "@angular/core";
1+
import { Component, OnInit, NgZone } from "@angular/core";
22
import { DailyLessonTrack } from "../shared/models/dailyLessons";
33
import { Observable } from "rxjs";
44
import { DailyLessonService } from "../shared/services/daily-lesson.service";
5-
import { map, tap } from "rxjs/operators";
5+
import { map } from "rxjs/operators";
6+
import { RequestNetworkPermissionService } from "../shared/services/request-network-permission.service";
7+
import { NetworkPermissionService } from "../shared/services/network-permission.service";
8+
import { DownloadProgressService, DownloadProgress } from "../shared/services/download-progress.service";
69

710
@Component({
8-
selector: "Home",
9-
moduleId: module.id,
10-
templateUrl: "./home.component.html"
11+
selector: "Home",
12+
moduleId: module.id,
13+
templateUrl: "./home.component.html"
1114
})
1215
export class HomeComponent implements OnInit {
1316

14-
todaysLessons$: Observable<DailyLessonTrack[]>;
17+
todaysLessons$: Observable<DailyLessonTrack[]>;
1518

16-
constructor(private lessonService: DailyLessonService) { }
19+
/** @description How far along an ongoing download is. */
20+
currentProgress: number;
21+
22+
constructor(
23+
private lessonService: DailyLessonService,
24+
private networkPermission: NetworkPermissionService,
25+
private requestPermission: RequestNetworkPermissionService,
26+
private downloadProgress: DownloadProgressService,
27+
private zone: NgZone
28+
) { }
1729

1830
ngOnInit(): void {
1931
this.todaysLessons$ = this.lessonService.getLibrary().pipe(
2032
map(library => {
21-
return library.query({ date: 0 })
22-
})
23-
);
24-
}
33+
return library.query({ date: 0 })
34+
})
35+
);
36+
37+
// If our app ever wants to download something but can't because we don't know if user
38+
// allows mobile data downloads, ask.
39+
this.networkPermission.getPermissionRequestCheck().subscribe(
40+
() => this.requestPermission.requestPermission()
41+
)
42+
43+
this.downloadProgress.getProgress().subscribe(progress => {
44+
this.zone.run(() => this.currentProgress = progress.progress);
45+
})
46+
}
47+
48+
updatePermissionSetting() {
49+
this.requestPermission.requestPermission();
50+
}
2551
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { DownloadProgressService } from './download-progress.service';
4+
5+
describe('DownloadProgressService', () => {
6+
beforeEach(() => TestBed.configureTestingModule({}));
7+
8+
it('should be created', () => {
9+
const service: DownloadProgressService = TestBed.get(DownloadProgressService);
10+
expect(service).toBeTruthy();
11+
});
12+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Injectable } from '@angular/core';
2+
import { Subject, Subscribable } from 'rxjs';
3+
import { distinct, debounceTime } from 'rxjs/operators';
4+
5+
export class DownloadProgress {
6+
url: string;
7+
progress: number;
8+
}
9+
10+
@Injectable({
11+
providedIn: 'root'
12+
})
13+
export class DownloadProgressService {
14+
private progress$: Subject<DownloadProgress> = new Subject;
15+
16+
constructor() { }
17+
18+
getProgress(): Subscribable<DownloadProgress> {
19+
return this.progress$.asObservable();
20+
}
21+
22+
setProgress(progress: DownloadProgress) {
23+
this.progress$.next(progress);
24+
}
25+
}

src/app/shared/services/lesson-media.service.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Injectable } from '@angular/core';
22
import { Lesson, LessonQuery } from '../models/dailyLessons';
3-
import { Observable, from, ReplaySubject, of, Subject, timer, throwError, defer } from 'rxjs';
4-
import { map, catchError, tap, mergeMap, concatMap, retryWhen, take, delay, retry } from 'rxjs/operators';
5-
import { path, knownFolders, File } from 'tns-core-modules/file-system/file-system';
3+
import { Observable, ReplaySubject, of, Subject, throwError, defer, concat } from 'rxjs';
4+
import { map, catchError, tap, mergeMap, concatMap, retry, switchMap, skipWhile, first } from 'rxjs/operators';
5+
import { path, knownFolders, File, } from 'tns-core-modules/file-system/file-system';
66
import { DownloadProgress } from "nativescript-download-progress"
77
import { DailyLessonService } from './daily-lesson.service';
88
import { MediaManifestService } from './media-manifest.service';
9+
import { NetworkPermissionService, PermissionReason } from './network-permission.service';
10+
import { DownloadProgressService } from './download-progress.service';
911

1012
/**
1113
* @description The folder where media is downloaded to.
@@ -21,17 +23,17 @@ export class LessonMediaService {
2123

2224
constructor(
2325
private dailyLessonService: DailyLessonService,
24-
private mediaManifestService: MediaManifestService
26+
private mediaManifestService: MediaManifestService,
27+
private networkPermissionService: NetworkPermissionService,
28+
private downloadProgress: DownloadProgressService
2529
) {
2630
// #11, #12: The current downloader seems to have issues with concurrent downloads.
2731
// so wait until all pending downloads are completed before doing the next one.
2832
this.loadRequest$.pipe(
2933
concatMap(([lesson, subject]) => {
30-
this.loadMedia(lesson).subscribe(
31-
path => subject.next(path)
34+
return this.loadMedia(lesson).pipe(
35+
tap(path => path && subject.next(path))
3236
);
33-
34-
return of(null);
3537
})
3638
).subscribe();
3739
}
@@ -78,22 +80,59 @@ export class LessonMediaService {
7880
if (downloadItem != null) {
7981
return of(downloadItem.path);
8082
} else {
81-
return this.downloadLesson(lesson);
83+
return this.downloadLessonWithPermission(lesson);
8284
}
8385
})
8486
);
8587
}
8688

89+
/** @description Waits for permission, then downloads the lesson. */
90+
private downloadLessonWithPermission(lesson: Lesson): Observable<string> {
91+
return this.networkPermissionService.getPermission().pipe(
92+
// Ask for permission from user, if that's the only reason can't download.
93+
tap(permission => {
94+
if (!permission.canDownload && permission.reason == PermissionReason.unknown) {
95+
this.networkPermissionService.requestPermission();
96+
}
97+
}),
98+
// Don't do anything until can download.
99+
skipWhile(permission => !permission.canDownload),
100+
first(),
101+
switchMap(() => this.downloadLesson(lesson))
102+
);
103+
}
104+
105+
/** @description Downloads the lesson. */
87106
private downloadLesson(lesson: Lesson): Observable<string> {
88-
const filePath = path.join(downloadFolder, `${lesson.id}.mp3`);
89-
90-
return defer(() => new DownloadProgress().downloadFile(lesson.source, filePath)).pipe(
91-
tap(() => console.log(`Attempting download: ${filePath} from ${lesson.source}`)),
107+
const filePath = path.join(downloadFolder, `${lesson.id}`);
108+
109+
return defer(() => {
110+
console.log(`Attempting download: ${lesson.source}`);
111+
112+
let downloader = new DownloadProgress();
113+
downloader.addProgressCallback(progress => {
114+
this.downloadProgress.setProgress({
115+
progress: progress * 100,
116+
url: lesson.source
117+
});
118+
});
119+
120+
return downloader.downloadFile(lesson.source, filePath);
121+
}).pipe(
92122
// Known bug: sometimes download fails.
93123
catchError(err => {
94124
// I observed that err is -always- usually an empty object.
95125
console.log(`Download error (from ${lesson.source}): ${JSON.stringify(err)}`);
96-
return throwError(err);
126+
127+
if (File.exists(filePath)) {
128+
return concat(
129+
// If there's a partial, invalid file, delete it.
130+
File.fromPath(filePath).remove(),
131+
throwError(err)
132+
)
133+
} else {
134+
return throwError(err);
135+
}
97136
}),
98137
retry(3),
99138
tap(file => console.log(`downloaded to: ${file && file.path}`)),
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { NetworkPermissionService } from './network-permission.service';
4+
5+
describe('NetworkPermissionService', () => {
6+
beforeEach(() => TestBed.configureTestingModule({}));
7+
8+
it('should be created', () => {
9+
const service: NetworkPermissionService = TestBed.get(NetworkPermissionService);
10+
expect(service).toBeTruthy();
11+
});
12+
});

0 commit comments

Comments
 (0)