- Translate the interface to Galician (tools/translate_gl.py), including Galician month abbreviations for dates, which Android 2.3 lacks - Resize for the Galaxy Mini's 240x320 screen: larger text, rows, tabs and buttons; all sizes now live in res/values/dimens.xml - Redraw play/pause/refresh icons for every density; slightly brighter secondary text colours Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
231 lines
8.3 KiB
Java
231 lines
8.3 KiB
Java
package com.miniabs;
|
|
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.net.HttpURLConnection;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
import android.app.IntentService;
|
|
import android.app.Notification;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.net.wifi.WifiManager;
|
|
import android.os.PowerManager;
|
|
|
|
/** Downloads episodes one at a time to the SD card, resuming partial files. */
|
|
public class DownloadService extends IntentService {
|
|
private static final int NOTIF_ID = 2;
|
|
|
|
private static final List<Episode> queue = new ArrayList<Episode>();
|
|
static volatile Episode current;
|
|
static volatile int percent;
|
|
static volatile String lastError;
|
|
private static volatile String cancelId;
|
|
|
|
public DownloadService() {
|
|
super("MiniABS downloads");
|
|
}
|
|
|
|
static void enqueue(Context c, Episode e) {
|
|
synchronized (queue) {
|
|
if (current != null && current.id.equals(e.id)) return;
|
|
for (Episode q : queue) if (q.id.equals(e.id)) return;
|
|
queue.add(e);
|
|
}
|
|
lastError = null;
|
|
c.startService(new Intent(c, DownloadService.class).putExtra("ep", e.toJson().toString()));
|
|
}
|
|
|
|
static void cancel(String epId) {
|
|
synchronized (queue) {
|
|
for (int i = queue.size() - 1; i >= 0; i--) if (queue.get(i).id.equals(epId)) queue.remove(i);
|
|
}
|
|
Episode cur = current;
|
|
if (cur != null && cur.id.equals(epId)) cancelId = epId;
|
|
}
|
|
|
|
static List<Episode> queued() {
|
|
synchronized (queue) {
|
|
return new ArrayList<Episode>(queue);
|
|
}
|
|
}
|
|
|
|
static boolean busyWith(String epId) {
|
|
Episode cur = current;
|
|
if (cur != null && cur.id.equals(epId)) return true;
|
|
synchronized (queue) {
|
|
for (Episode q : queue) if (q.id.equals(epId)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected void onHandleIntent(Intent intent) {
|
|
Episode e;
|
|
try {
|
|
e = Episode.fromStored(new JSONObject(intent.getStringExtra("ep")));
|
|
} catch (Exception ex) {
|
|
return;
|
|
}
|
|
synchronized (queue) {
|
|
boolean found = false;
|
|
for (int i = 0; i < queue.size(); i++) {
|
|
if (queue.get(i).id.equals(e.id)) {
|
|
queue.remove(i);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) return; // cancelled while waiting
|
|
current = e;
|
|
}
|
|
percent = 0;
|
|
cancelId = null;
|
|
PowerManager.WakeLock wake = ((PowerManager) getSystemService(POWER_SERVICE))
|
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MiniABS download");
|
|
WifiManager.WifiLock wifi = ((WifiManager) getSystemService(WIFI_SERVICE))
|
|
.createWifiLock(WifiManager.WIFI_MODE_FULL, "MiniABS download");
|
|
wake.acquire();
|
|
wifi.acquire();
|
|
try {
|
|
download(e);
|
|
if (e.id.equals(cancelId)) {
|
|
Downloads.delete(this, e);
|
|
}
|
|
} catch (Exception ex) {
|
|
if (!e.id.equals(cancelId)) {
|
|
lastError = "Fallou a descarga: " + Util.describe(ex);
|
|
notifyDone(e.title, lastError);
|
|
}
|
|
} finally {
|
|
current = null;
|
|
cancelId = null;
|
|
stopForeground(true);
|
|
wifi.release();
|
|
wake.release();
|
|
}
|
|
}
|
|
|
|
private void download(Episode e) throws IOException {
|
|
if (Prefs.get(this).wifiOnly() && !Util.onWifi(this)) {
|
|
throw new IOException("non hai wifi (Axustes: Descargar só con wifi)");
|
|
}
|
|
File dir = Downloads.dir(this);
|
|
if (Downloads.audio(this, e) != null) {
|
|
Downloads.saved(this, e);
|
|
return;
|
|
}
|
|
if (e.ino.length() == 0) throw new IOException("este episodio non ten ficheiro de audio");
|
|
File part = new File(dir, e.id + ".part");
|
|
Notification n = progressNotification(e, 0);
|
|
startForeground(NOTIF_ID, n);
|
|
|
|
Api.ensureFresh(this, 600);
|
|
HttpURLConnection conn = null;
|
|
int code = 0;
|
|
long have = part.exists() ? part.length() : 0;
|
|
boolean refreshed = false;
|
|
String[] urls = Api.downloadUrls(this, e);
|
|
for (int i = 0; i < urls.length; i++) {
|
|
conn = Api.open(this, urls[i], "GET");
|
|
conn.setReadTimeout(60000);
|
|
if (have > 0) conn.setRequestProperty("Range", "bytes=" + have + "-");
|
|
code = Api.responseCode(conn);
|
|
if (code == 401 && !refreshed && Prefs.get(this).canRefresh()) {
|
|
conn.disconnect();
|
|
Api.refresh(this, true);
|
|
refreshed = true;
|
|
urls = Api.downloadUrls(this, e);
|
|
i--;
|
|
continue;
|
|
}
|
|
if (code == 200 || code == 206) break;
|
|
conn.disconnect();
|
|
if (code == 416 && have > 0) { // stale partial file: start over
|
|
part.delete();
|
|
have = 0;
|
|
i--;
|
|
continue;
|
|
}
|
|
if (code != 404 && code != 403) break;
|
|
}
|
|
if (code != 200 && code != 206) {
|
|
if (code == 401) throw new Api.AuthException("a sesión caducou; inicia sesión de novo");
|
|
if (code == 403) throw new IOException("a túa conta non ten permiso para descargar");
|
|
throw new Api.ApiException(code, "");
|
|
}
|
|
try {
|
|
boolean append = code == 206 && have > 0;
|
|
if (!append) have = 0;
|
|
long len = conn.getContentLength();
|
|
long total = len > 0 ? have + len : (e.size > 0 ? e.size : -1);
|
|
InputStream in = conn.getInputStream();
|
|
OutputStream out = new FileOutputStream(part, append);
|
|
try {
|
|
byte[] buf = new byte[16384];
|
|
long done = have, lastUi = 0;
|
|
int r;
|
|
while ((r = in.read(buf)) > 0) {
|
|
if (e.id.equals(cancelId)) return;
|
|
out.write(buf, 0, r);
|
|
done += r;
|
|
long now = System.currentTimeMillis();
|
|
if (now - lastUi > 1500) {
|
|
lastUi = now;
|
|
percent = total > 0 ? (int) (done * 100 / total) : 0;
|
|
n = progressNotification(e, percent);
|
|
nm().notify(NOTIF_ID, n);
|
|
}
|
|
}
|
|
} finally {
|
|
out.close();
|
|
in.close();
|
|
}
|
|
} finally {
|
|
conn.disconnect();
|
|
}
|
|
if (e.id.equals(cancelId)) return;
|
|
File dest = new File(dir, e.id + e.ext);
|
|
if (!part.renameTo(dest)) throw new IOException("non se puido gardar o ficheiro");
|
|
Downloads.saved(this, e);
|
|
percent = 100;
|
|
}
|
|
|
|
private NotificationManager nm() {
|
|
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
}
|
|
|
|
private PendingIntent openApp() {
|
|
Intent i = new Intent(this, HomeActivity.class).putExtra("tab", HomeActivity.TAB_SAVED)
|
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
return PendingIntent.getActivity(this, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
}
|
|
|
|
private Notification progressNotification(Episode e, int pct) {
|
|
Notification n = new Notification(android.R.drawable.stat_sys_download, null, System.currentTimeMillis());
|
|
n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_ONLY_ALERT_ONCE;
|
|
int left;
|
|
synchronized (queue) {
|
|
left = queue.size();
|
|
}
|
|
String text = "Descargando " + pct + " %" + (left > 0 ? " · " + left + " máis na cola" : "");
|
|
n.setLatestEventInfo(this, e.title, text, openApp());
|
|
return n;
|
|
}
|
|
|
|
private void notifyDone(String title, String text) {
|
|
Notification n = new Notification(android.R.drawable.stat_notify_error, text, System.currentTimeMillis());
|
|
n.flags |= Notification.FLAG_AUTO_CANCEL;
|
|
n.setLatestEventInfo(this, title, text, openApp());
|
|
nm().notify(3, n);
|
|
}
|
|
}
|