package us.shandian.giga.service; import android.Manifest; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; import android.util.Log; import android.util.SparseArray; import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.util.ArrayList; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.service.DownloadManager.NetworkState; import static org.schabi.newpipe.BuildConfig.APPLICATION_ID; import static org.schabi.newpipe.BuildConfig.DEBUG; public class DownloadManagerService extends Service { private static final String TAG = "DownloadManagerService"; public static final int MESSAGE_PAUSED = 1; public static final int MESSAGE_FINISHED = 2; public static final int MESSAGE_PROGRESS = 3; public static final int MESSAGE_ERROR = 4; public static final int MESSAGE_DELETED = 5; private static final int FOREGROUND_NOTIFICATION_ID = 1000; private static final int DOWNLOADS_NOTIFICATION_ID = 1001; private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; private static final String EXTRA_NAME = "DownloadManagerService.extra.name"; private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location"; private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; private DMBinder mBinder; private DownloadManager mManager; private Notification mNotification; private Handler mHandler; private boolean mForeground = false; private NotificationManager mNotificationManager = null; private boolean mDownloadNotificationEnable = true; private int downloadDoneCount = 0; private Builder downloadDoneNotification = null; private StringBuilder downloadDoneList = null; private final ArrayList mEchoObservers = new ArrayList<>(1); private ConnectivityManager mConnectivityManager; private BroadcastReceiver mNetworkStateListener = null; private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null; private SharedPreferences mPrefs = null; private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange; private boolean mLockAcquired = false; private LockManager mLock = null; private int downloadFailedNotificationID = DOWNLOADS_NOTIFICATION_ID + 1; private Builder downloadFailedNotification = null; private SparseArray mFailedDownloads = new SparseArray<>(5); private Bitmap icLauncher; private Bitmap icDownloadDone; private Bitmap icDownloadFailed; private PendingIntent mOpenDownloadList; /** * notify media scanner on downloaded media file ... * * @param file the downloaded file */ private void notifyMediaScanner(File file) { sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); } @Override public void onCreate() { super.onCreate(); if (DEBUG) { Log.d(TAG, "onCreate"); } mBinder = new DMBinder(); mHandler = new Handler(Looper.myLooper()) { @Override public void handleMessage(Message msg) { DownloadManagerService.this.handleMessage(msg); } }; mManager = new DownloadManager(this, mHandler); Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN); mOpenDownloadList = PendingIntent.getActivity(this, 0, openDownloadListIntent, PendingIntent.FLAG_UPDATE_CURRENT); icLauncher = BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher); Builder builder = new Builder(this, getString(R.string.notification_channel_id)) .setContentIntent(mOpenDownloadList) .setSmallIcon(android.R.drawable.stat_sys_download) .setLargeIcon(icLauncher) .setContentTitle(getString(R.string.msg_running)) .setContentText(getString(R.string.msg_running_detail)); mNotification = builder.build(); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { handleConnectivityState(false); } @Override public void onLost(Network network) { handleConnectivityState(false); } }; mConnectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkStateListenerL); } else { mNetworkStateListener = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { handleConnectivityState(false); } }; registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry)); handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); mLock = new LockManager(this); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (DEBUG) { Log.d(TAG, intent == null ? "Restarting" : "Starting"); } if (intent == null) return START_NOT_STICKY; Log.i(TAG, "Got intent: " + intent); String action = intent.getAction(); if (action != null) { if (action.equals(Intent.ACTION_RUN)) { String[] urls = intent.getStringArrayExtra(EXTRA_URLS); String name = intent.getStringExtra(EXTRA_NAME); String location = intent.getStringExtra(EXTRA_LOCATION); int threads = intent.getIntExtra(EXTRA_THREADS, 1); char kind = intent.getCharExtra(EXTRA_KIND, '?'); String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); String source = intent.getStringExtra(EXTRA_SOURCE); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); handleConnectivityState(true);// first check the actual network status mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); } else if (downloadDoneNotification != null) { if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { downloadDoneCount = 0; downloadDoneList.setLength(0); } if (action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { startActivity(new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ); } } } return START_NOT_STICKY; } @Override public void onDestroy() { super.onDestroy(); if (DEBUG) { Log.d(TAG, "Destroying"); } stopForeground(true); if (mNotificationManager != null && downloadDoneNotification != null) { downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); } manageLock(false); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL); else unregisterReceiver(mNetworkStateListener); mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener); if (icDownloadDone != null) icDownloadDone.recycle(); if (icDownloadFailed != null) icDownloadFailed.recycle(); if (icLauncher != null) icLauncher.recycle(); mManager.pauseAllMissions(true); } @Override public IBinder onBind(Intent intent) { int permissionCheck; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); } } permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); } return mBinder; } public void handleMessage(Message msg) { DownloadMission mission = (DownloadMission) msg.obj; switch (msg.what) { case MESSAGE_FINISHED: notifyMediaScanner(mission.getDownloadedFile()); notifyFinishedDownload(mission.name); mManager.setFinished(mission); handleConnectivityState(false); updateForegroundState(mManager.runMissions()); break; case MESSAGE_PROGRESS: updateForegroundState(true); break; case MESSAGE_ERROR: notifyFailedDownload(mission); handleConnectivityState(false); updateForegroundState(mManager.runMissions()); break; case MESSAGE_PAUSED: updateForegroundState(mManager.getRunningMissionsCount() > 0); break; } if (msg.what != MESSAGE_ERROR) mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission)); synchronized (mEchoObservers) { for (Handler handler : mEchoObservers) { Message echo = new Message(); echo.what = msg.what; echo.obj = msg.obj; handler.sendMessage(echo); } } } private void handleConnectivityState(boolean updateOnly) { NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); NetworkState status; if (info == null) { status = NetworkState.Unavailable; Log.i(TAG, "Active network [connectivity is unavailable]"); } else { boolean connected = info.isConnected(); boolean metered = mConnectivityManager.isActiveNetworkMetered(); if (connected) status = metered ? NetworkState.MeteredOperating : NetworkState.Operating; else status = NetworkState.Unavailable; Log.i(TAG, "Active network [connected=" + connected + " metered=" + metered + "] " + info.toString()); } if (mManager == null) return;// avoid race-conditions while the service is starting mManager.handleConnectivityState(status, updateOnly); } private void handlePreferenceChange(SharedPreferences prefs, @NonNull String key) { if (key.equals(getString(R.string.downloads_maximum_retry))) { try { String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default)); mManager.mPrefMaxRetry = Integer.parseInt(value); } catch (Exception e) { mManager.mPrefMaxRetry = 0; } mManager.updateMaximumAttempts(); } else if (key.equals(getString(R.string.downloads_cross_network))) { mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false); } else if (key.equals(getString(R.string.downloads_queue_limit))) { mManager.mPrefQueueLimit = prefs.getBoolean(key, true); } } public void updateForegroundState(boolean state) { if (state == mForeground) return; if (state) { startForeground(FOREGROUND_NOTIFICATION_ID, mNotification); } else { stopForeground(true); } manageLock(state); mForeground = state; } public static void startMission(Context context, String urls[], String location, String name, char kind, int threads, String source, String psName, String[] psArgs, long nearLength) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); intent.putExtra(EXTRA_URLS, urls); intent.putExtra(EXTRA_NAME, name); intent.putExtra(EXTRA_LOCATION, location); intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_THREADS, threads); intent.putExtra(EXTRA_SOURCE, source); intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); context.startService(intent); } public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) { Intent intent = new Intent(); intent.setClass(context, DownloadManagerService.class); context.startService(intent); context.bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(ComponentName cname, IBinder service) { try { ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker); } catch (Exception err) { Log.w(TAG, "checkForRunningMission() callback is defective", err); } context.unbindService(this); } @Override public void onServiceDisconnected(ComponentName name) { } }, Context.BIND_AUTO_CREATE); } public void notifyFinishedDownload(String name) { if (!mDownloadNotificationEnable || mNotificationManager == null) { return; } if (downloadDoneNotification == null) { downloadDoneList = new StringBuilder(name.length()); icDownloadDone = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done); downloadDoneNotification = new Builder(this, getString(R.string.notification_channel_id)) .setAutoCancel(true) .setLargeIcon(icDownloadDone) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setDeleteIntent(makePendingIntent(ACTION_RESET_DOWNLOAD_FINISHED)) .setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED)); } if (downloadDoneCount < 1) { downloadDoneList.append(name); if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadDoneNotification.setContentTitle(getString(R.string.app_name)); } else { downloadDoneNotification.setContentTitle(null); } downloadDoneNotification.setContentText(getString(R.string.download_finished)); downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle() .setBigContentTitle(getString(R.string.download_finished)) .bigText(name) ); } else { downloadDoneList.append('\n'); downloadDoneList.append(name); downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle().bigText(downloadDoneList)); downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1))); downloadDoneNotification.setContentText(downloadDoneList); } mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); downloadDoneCount++; } public void notifyFailedDownload(DownloadMission mission) { if (!mDownloadNotificationEnable || mFailedDownloads.indexOfValue(mission) >= 0) return; int id = downloadFailedNotificationID++; mFailedDownloads.put(id, mission); if (downloadFailedNotification == null) { icDownloadFailed = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_warning); downloadFailedNotification = new Builder(this, getString(R.string.notification_channel_id)) .setAutoCancel(true) .setLargeIcon(icDownloadFailed) .setSmallIcon(android.R.drawable.stat_sys_warning) .setContentIntent(mOpenDownloadList); } if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadFailedNotification.setContentTitle(getString(R.string.app_name)); downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() .bigText(getString(R.string.download_failed).concat(": ").concat(mission.name))); } else { downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); downloadFailedNotification.setContentText(mission.name); downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() .bigText(mission.name)); } mNotificationManager.notify(id, downloadFailedNotification.build()); } private PendingIntent makePendingIntent(String action) { Intent intent = new Intent(this, DownloadManagerService.class).setAction(action); return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); } private void manageObservers(Handler handler, boolean add) { synchronized (mEchoObservers) { if (add) { mEchoObservers.add(handler); } else { mEchoObservers.remove(handler); } } } private void manageLock(boolean acquire) { if (acquire == mLockAcquired) return; if (acquire) mLock.acquireWifiAndCpu(); else mLock.releaseWifiAndCpu(); mLockAcquired = acquire; } //////////////////////////////////////////////////////////////////////////////////////////////// // Wrappers for DownloadManager //////////////////////////////////////////////////////////////////////////////////////////////// public class DMBinder extends Binder { public DownloadManager getDownloadManager() { return mManager; } public void addMissionEventListener(Handler handler) { manageObservers(handler, true); } public void removeMissionEventListener(Handler handler) { manageObservers(handler, false); } public void clearDownloadNotifications() { if (mNotificationManager == null) return; if (downloadDoneNotification != null) { mNotificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); downloadDoneList.setLength(0); downloadDoneCount = 0; } if (downloadFailedNotification != null) { for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) { mNotificationManager.cancel(downloadFailedNotificationID); } mFailedDownloads.clear(); downloadFailedNotificationID++; } } public void enableNotifications(boolean enable) { mDownloadNotificationEnable = enable; } } public interface DMChecker { void callback(MissionCheck result); } public enum MissionCheck {None, Pending, PendingRunning, Finished} }