Java tutorial
/* * Copyright 2016 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hippo.ehviewer.download; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.IBinder; import android.os.SystemClock; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.dao.DownloadInfo; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.ehviewer.ui.scene.DownloadsScene; import com.hippo.scene.StageActivity; import com.hippo.util.ReadableTime; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.SimpleHandler; import com.hippo.yorozuya.collect.LongList; import com.hippo.yorozuya.collect.SparseJBArray; import com.hippo.yorozuya.collect.SparseJLArray; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class DownloadService extends Service implements DownloadManager.DownloadListener { public static final String ACTION_START = "start"; public static final String ACTION_START_RANGE = "start_range"; public static final String ACTION_START_ALL = "start_all"; public static final String ACTION_STOP = "stop"; public static final String ACTION_STOP_RANGE = "stop_range"; public static final String ACTION_STOP_CURRENT = "stop_current"; public static final String ACTION_STOP_ALL = "stop_all"; public static final String ACTION_DELETE = "delete"; public static final String ACTION_DELETE_RANGE = "delete_range"; public static final String ACTION_CLEAR = "clear"; public static final String KEY_GALLERY_INFO = "gallery_info"; public static final String KEY_LABEL = "label"; public static final String KEY_GID = "gid"; public static final String KEY_GID_LIST = "gid_list"; private static final int ID_DOWNLOADING = 1; private static final int ID_DOWNLOADED = 2; private static final int ID_509 = 3; @Nullable private NotificationManager mNotifyManager; @Nullable private DownloadManager mDownloadManager; private NotificationCompat.Builder mDownloadingBuilder; private NotificationCompat.Builder mDownloadedBuilder; private NotificationCompat.Builder m509dBuilder; private NotificationDelay mDownloadingDelay; private NotificationDelay mDownloadedDelay; private NotificationDelay m509Delay; private final static SparseJBArray sItemStateArray = new SparseJBArray(); private final static SparseJLArray<String> sItemTitleArray = new SparseJLArray<>(); private static int sFailedCount; private static int sFinishedCount; private static int sDownloadedCount; public static void clear() { sFailedCount = 0; sFinishedCount = 0; sDownloadedCount = 0; sItemStateArray.clear(); sItemTitleArray.clear(); } @Override public void onCreate() { super.onCreate(); mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mDownloadManager = EhApplication.getDownloadManager(getApplicationContext()); mDownloadManager.setDownloadListener(this); } @Override public void onDestroy() { super.onDestroy(); mNotifyManager = null; if (mDownloadManager != null) { mDownloadManager.setDownloadListener(null); mDownloadManager = null; } mDownloadingBuilder = null; mDownloadedBuilder = null; m509dBuilder = null; if (mDownloadingDelay != null) { mDownloadingDelay.release(); } if (mDownloadedDelay != null) { mDownloadedDelay.release(); } if (m509Delay != null) { m509Delay.release(); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { handleIntent(intent); return START_STICKY; } private void handleIntent(Intent intent) { String action = null; if (intent != null) { action = intent.getAction(); } if (ACTION_START.equals(action)) { GalleryInfo gi = intent.getParcelableExtra(KEY_GALLERY_INFO); String label = intent.getStringExtra(KEY_LABEL); if (gi != null && mDownloadManager != null) { mDownloadManager.startDownload(gi, label); } } else if (ACTION_START_RANGE.equals(action)) { LongList gidList = intent.getParcelableExtra(KEY_GID_LIST); if (gidList != null && mDownloadManager != null) { mDownloadManager.startRangeDownload(gidList); } } else if (ACTION_START_ALL.equals(action)) { if (mDownloadManager != null) { mDownloadManager.startAllDownload(); } } else if (ACTION_STOP.equals(action)) { long gid = intent.getLongExtra(KEY_GID, -1); if (gid != -1 && mDownloadManager != null) { mDownloadManager.stopDownload(gid); } } else if (ACTION_STOP_CURRENT.equals(action)) { if (mDownloadManager != null) { mDownloadManager.stopCurrentDownload(); } } else if (ACTION_STOP_RANGE.equals(action)) { LongList gidList = intent.getParcelableExtra(KEY_GID_LIST); if (gidList != null && mDownloadManager != null) { mDownloadManager.stopRangeDownload(gidList); } } else if (ACTION_STOP_ALL.equals(action)) { if (mDownloadManager != null) { mDownloadManager.stopAllDownload(); } } else if (ACTION_DELETE.equals(action)) { long gid = intent.getLongExtra(KEY_GID, -1); if (gid != -1 && mDownloadManager != null) { mDownloadManager.deleteDownload(gid); } } else if (ACTION_DELETE_RANGE.equals(action)) { LongList gidList = intent.getParcelableExtra(KEY_GID_LIST); if (gidList != null && mDownloadManager != null) { mDownloadManager.deleteRangeDownload(gidList); } } else if (ACTION_CLEAR.equals(action)) { clear(); } checkStopSelf(); } @Nullable @Override public IBinder onBind(Intent intent) { throw new IllegalStateException("No bindService"); } @SuppressWarnings("deprecation") private void ensureDownloadingBuilder() { if (mDownloadingBuilder != null) { return; } Intent stopAllIntent = new Intent(this, DownloadService.class); stopAllIntent.setAction(ACTION_STOP_ALL); PendingIntent piStopAll = PendingIntent.getService(this, 0, stopAllIntent, 0); mDownloadingBuilder = new NotificationCompat.Builder(getApplicationContext()) .setSmallIcon(android.R.drawable.stat_sys_download).setOngoing(true).setAutoCancel(false) .setCategory(NotificationCompat.CATEGORY_PROGRESS) .setColor(getResources().getColor(R.color.colorPrimary)) .addAction(R.drawable.ic_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll) .setShowWhen(false); mDownloadingDelay = new NotificationDelay(this, mNotifyManager, mDownloadingBuilder, ID_DOWNLOADING); } private void ensureDownloadedBuilder() { if (mDownloadedBuilder != null) { return; } Intent clearIntent = new Intent(this, DownloadService.class); clearIntent.setAction(ACTION_CLEAR); PendingIntent piClear = PendingIntent.getService(this, 0, clearIntent, 0); Bundle bundle = new Bundle(); bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE); Intent activityIntent = new Intent(this, MainActivity.class); activityIntent.setAction(StageActivity.ACTION_START_SCENE); activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName()); activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle); PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext()) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentTitle(getString(R.string.stat_download_done_title)).setDeleteIntent(piClear) .setOngoing(false).setAutoCancel(true).setContentIntent(piActivity); mDownloadedDelay = new NotificationDelay(this, mNotifyManager, mDownloadedBuilder, ID_DOWNLOADED); } private void ensure509Builder() { if (m509dBuilder != null) { return; } m509dBuilder = new NotificationCompat.Builder(getApplicationContext()) .setSmallIcon(R.drawable.ic_stat_alert) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentText(getString(R.string.stat_509_alert_title)) .setContentText(getString(R.string.stat_509_alert_text)).setAutoCancel(true).setOngoing(false) .setCategory(NotificationCompat.CATEGORY_ERROR); m509Delay = new NotificationDelay(this, mNotifyManager, m509dBuilder, ID_509); } @Override public void onGet509() { if (mNotifyManager == null) { return; } ensure509Builder(); m509dBuilder.setWhen(System.currentTimeMillis()); m509Delay.show(); } @Override public void onStart(DownloadInfo info) { if (mNotifyManager == null) { return; } ensureDownloadingBuilder(); Bundle bundle = new Bundle(); bundle.putLong(DownloadsScene.KEY_GID, info.gid); Intent activityIntent = new Intent(this, MainActivity.class); activityIntent.setAction(StageActivity.ACTION_START_SCENE); activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName()); activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle); PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info)).setContentText(null) .setContentInfo(null).setProgress(0, 0, true).setContentIntent(piActivity); mDownloadingDelay.startForeground(); } private void onUpdate(DownloadInfo info) { if (mNotifyManager == null) { return; } ensureDownloadingBuilder(); long speed = info.speed; if (speed < 0) { speed = 0; } String text = FileUtils.humanReadableByteCount(speed, false) + "/S"; long remaining = info.remaining; if (remaining >= 0) { text = getString(R.string.download_speed_text_2, text, ReadableTime.getShortTimeInterval(remaining)); } else { text = getString(R.string.download_speed_text, text); } mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info)).setContentText(text) .setContentInfo(info.total == -1 || info.finished == -1 ? null : info.finished + "/" + info.total) .setProgress(info.total, info.finished, false); mDownloadingDelay.startForeground(); } @Override public void onDownload(DownloadInfo info) { onUpdate(info); } @Override public void onGetPage(DownloadInfo info) { onUpdate(info); } @Override public void onFinish(DownloadInfo info) { if (mNotifyManager == null) { return; } if (null != mDownloadingDelay) { mDownloadingDelay.cancel(); } ensureDownloadedBuilder(); boolean finish = info.state == DownloadInfo.STATE_FINISH; long gid = info.gid; int index = sItemStateArray.indexOfKey(gid); if (index < 0) { // Not contain sItemStateArray.put(gid, finish); sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info)); sDownloadedCount++; if (finish) { sFinishedCount++; } else { sFailedCount++; } } else { // Contain boolean oldFinish = sItemStateArray.valueAt(index); sItemStateArray.put(gid, finish); sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info)); if (oldFinish && !finish) { sFinishedCount--; sFailedCount++; } else if (!oldFinish && finish) { sFinishedCount++; sFailedCount--; } } String text; boolean needStyle; if (sFinishedCount != 0 && sFailedCount == 0) { if (sFinishedCount == 1) { if (sItemTitleArray.size() >= 1) { text = getString(R.string.stat_download_done_line_succeeded, sItemTitleArray.valueAt(0)); } else { Log.d("TAG", "WTF, sItemTitleArray is null"); text = getString(R.string.error_unknown); } needStyle = false; } else { text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount); needStyle = true; } } else if (sFinishedCount == 0 && sFailedCount != 0) { if (sFailedCount == 1) { if (sItemTitleArray.size() >= 1) { text = getString(R.string.stat_download_done_line_failed, sItemTitleArray.valueAt(0)); } else { Log.d("TAG", "WTF, sItemTitleArray is null"); text = getString(R.string.error_unknown); } needStyle = false; } else { text = getString(R.string.stat_download_done_text_failed, sFailedCount); needStyle = true; } } else { text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount); needStyle = true; } NotificationCompat.InboxStyle style; if (needStyle) { style = new NotificationCompat.InboxStyle(); style.setBigContentTitle(getString(R.string.stat_download_done_title)); SparseJBArray stateArray = sItemStateArray; SparseJLArray<String> titleArray = sItemTitleArray; for (int i = 0, n = stateArray.size(); i < n; i++) { long id = stateArray.keyAt(i); boolean fin = stateArray.valueAt(i); String title = titleArray.get(id); if (title == null) { continue; } style.addLine(getString( fin ? R.string.stat_download_done_line_succeeded : R.string.stat_download_done_line_failed, title)); } } else { style = null; } mDownloadedBuilder.setContentText(text).setStyle(style).setWhen(System.currentTimeMillis()) .setNumber(sDownloadedCount); mDownloadedDelay.show(); checkStopSelf(); } @Override public void onCancel(DownloadInfo info) { if (mNotifyManager == null) { return; } if (null != mDownloadingDelay) { mDownloadingDelay.cancel(); } checkStopSelf(); } private void checkStopSelf() { if (mDownloadManager == null || mDownloadManager.isIdle()) { stopForeground(true); stopSelf(); } } // TODO Include all notification in one delay // Avoid frequent notification private static class NotificationDelay implements Runnable { @IntDef({ OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND }) @Retention(RetentionPolicy.SOURCE) private @interface Ops { } private static final int OPS_NOTIFY = 0; private static final int OPS_CANCEL = 1; private static final int OPS_START_FOREGROUND = 2; private static final long DELAY = 1000; // 1s private Service mService; private final NotificationManager mNotifyManager; private final NotificationCompat.Builder mBuilder; private final int mId; private long mLastTime; private boolean mPosted; // false for show, true for cancel @Ops private int mOps; public NotificationDelay(Service service, NotificationManager notifyManager, NotificationCompat.Builder builder, int id) { mService = service; mNotifyManager = notifyManager; mBuilder = builder; mId = id; } public void release() { mService = null; } public void show() { if (mPosted) { mOps = OPS_NOTIFY; } else { long now = SystemClock.currentThreadTimeMillis(); if (now - mLastTime > DELAY) { // Wait long enough, do it now mNotifyManager.notify(mId, mBuilder.build()); } else { // Too quick, post delay mOps = OPS_NOTIFY; mPosted = true; SimpleHandler.getInstance().postDelayed(this, DELAY); } mLastTime = now; } } public void cancel() { if (mPosted) { mOps = OPS_CANCEL; } else { long now = SystemClock.currentThreadTimeMillis(); if (now - mLastTime > DELAY) { // Wait long enough, do it now mNotifyManager.cancel(mId); } else { // Too quick, post delay mOps = OPS_CANCEL; mPosted = true; SimpleHandler.getInstance().postDelayed(this, DELAY); } } } public void startForeground() { if (mPosted) { mOps = OPS_START_FOREGROUND; } else { long now = SystemClock.currentThreadTimeMillis(); if (now - mLastTime > DELAY) { // Wait long enough, do it now if (mService != null) { mService.startForeground(mId, mBuilder.build()); } } else { // Too quick, post delay mOps = OPS_START_FOREGROUND; mPosted = true; SimpleHandler.getInstance().postDelayed(this, DELAY); } } } @Override public void run() { mPosted = false; switch (mOps) { case OPS_NOTIFY: mNotifyManager.notify(mId, mBuilder.build()); break; case OPS_CANCEL: mNotifyManager.cancel(mId); break; case OPS_START_FOREGROUND: if (mService != null) { mService.startForeground(mId, mBuilder.build()); } break; } } } }