diff --git a/app/build.gradle b/app/build.gradle
index 0e1552e2d..0e4929f88 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -43,4 +43,6 @@ dependencies {
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
compile 'com.github.nirhart:parallaxscroll:1.0'
compile 'com.google.android.exoplayer:exoplayer:r1.5.5'
+ compile 'com.google.code.gson:gson:2.3.+'
+ compile 'com.nononsenseapps:filepicker:2.0.5'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a4ffff035..fb017102b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -125,5 +125,28 @@
android:label="@string/general_error"
android:theme="@android:style/Theme.NoDisplay" />
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index 8791f5a3d..c53b4abb1 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -2,9 +2,9 @@ package org.schabi.newpipe.download;
import android.Manifest;
import android.app.Dialog;
-import android.app.DownloadManager;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
@@ -14,7 +14,6 @@ import android.support.v4.app.DialogFragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.util.Log;
-import android.widget.Toast;
import org.schabi.newpipe.App;
import org.schabi.newpipe.NewPipeSettings;
@@ -152,49 +151,23 @@ public class DownloadDialog extends DialogFragment {
private void download(String url, String title,
String fileSuffix, File downloadDir, Context context) {
- if(!downloadDir.exists()) {
- //attempt to create directory
- boolean mkdir = downloadDir.mkdirs();
- if(!mkdir && !downloadDir.isDirectory()) {
- String message = context.getString(R.string.err_dir_create,downloadDir.toString());
- Log.e(TAG, message);
- Toast.makeText(context,message , Toast.LENGTH_LONG).show();
-
- return;
- }
- String message = context.getString(R.string.info_dir_created,downloadDir.toString());
- Log.e(TAG, message);
- Toast.makeText(context,message , Toast.LENGTH_LONG).show();
- }
-
File saveFilePath = new File(downloadDir,createFileName(title) + fileSuffix);
long id = 0;
-
- if (App.isUsingTor()) {
- // if using Tor, do not use DownloadManager because the proxy cannot be set
- FileDownloader.downloadFile(getContext(), url, saveFilePath, title);
- } else {
- DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
- DownloadManager.Request request = new DownloadManager.Request(
- Uri.parse(url));
- request.setDestinationUri(Uri.fromFile(saveFilePath));
- request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
-
- request.setTitle(title);
- request.setDescription("'" + url +
- "' => '" + saveFilePath + "'");
- request.allowScanningByMediaScanner();
-
- try {
- id = dm.enqueue(request);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
Log.i(TAG,"Started downloading '" + url +
"' => '" + saveFilePath + "' #" + id);
+
+ if (App.isUsingTor()) {
+ //if using Tor, do not use DownloadManager because the proxy cannot be set
+ //we'll see later
+ FileDownloader.downloadFile(getContext(), url, saveFilePath, title);
+ } else {
+ Intent intent = new Intent(getContext(), MainActivity.class);
+ intent.setAction(MainActivity.INTENT_DOWNLOAD);
+ intent.setData(Uri.parse(url));
+ intent.putExtra("fileName", createFileName(title) + fileSuffix);
+ startActivity(intent);
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/download/MainActivity.java b/app/src/main/java/org/schabi/newpipe/download/MainActivity.java
new file mode 100644
index 000000000..72578ea47
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/download/MainActivity.java
@@ -0,0 +1,280 @@
+package org.schabi.newpipe.download;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.schabi.newpipe.R;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import us.shandian.giga.get.DownloadManager;
+import us.shandian.giga.service.DownloadManagerService;
+import us.shandian.giga.ui.fragment.AllMissionsFragment;
+import us.shandian.giga.ui.fragment.MissionsFragment;
+import us.shandian.giga.util.CrashHandler;
+import us.shandian.giga.util.Utility;
+
+public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener{
+
+ public static final String INTENT_DOWNLOAD = "us.shandian.giga.intent.DOWNLOAD";
+
+ public static final String INTENT_LIST = "us.shandian.giga.intent.LIST";
+
+ private MissionsFragment mFragment;
+ private DownloadManager mManager;
+ private DownloadManagerService.DMBinder mBinder;
+
+ private String mPendingUrl;
+ private SharedPreferences mPrefs;
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName p1, IBinder binder) {
+ mBinder = (DownloadManagerService.DMBinder) binder;
+ mManager = mBinder.getDownloadManager();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName p1) {
+
+ }
+ };
+
+ @Override
+ @TargetApi(21)
+ protected void onCreate(Bundle savedInstanceState) {
+ CrashHandler.init(this);
+ CrashHandler.register();
+
+ // Service
+ Intent i = new Intent();
+ i.setClass(this, DownloadManagerService.class);
+ startService(i);
+ bindService(i, mConnection, Context.BIND_AUTO_CREATE);
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_downloader);
+
+
+ mPrefs = getSharedPreferences("threads", Context.MODE_WORLD_READABLE);
+
+
+ // Fragment
+ getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ updateFragments();
+ getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ }
+ });
+
+ // Intent
+ if (getIntent().getAction().equals(INTENT_DOWNLOAD)) {
+ mPendingUrl = getIntent().getData().toString();
+ }
+ }
+
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ if (intent.getAction().equals(INTENT_DOWNLOAD)) {
+ mPendingUrl = intent.getData().toString();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (mPendingUrl != null) {
+ showUrlDialog();
+ mPendingUrl = null;
+ }
+ }
+
+ private void updateFragments() {
+
+ mFragment = new AllMissionsFragment();
+
+ getFragmentManager().beginTransaction()
+ .replace(R.id.frame, mFragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ .commit();
+
+ }
+
+ private void showUrlDialog() {
+ // Create the view
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View v = inflater.inflate(R.layout.dialog_url, null);
+ final EditText text = Utility.findViewById(v, R.id.url);
+ final EditText name = Utility.findViewById(v, R.id.file_name);
+ final TextView tCount = Utility.findViewById(v, R.id.threads_count);
+ final SeekBar threads = Utility.findViewById(v, R.id.threads);
+ final Toolbar toolbar = Utility.findViewById(v, R.id.toolbar);
+ final Button fetch = Utility.findViewById(v, R.id.fetch_name);
+
+ threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+
+ @Override
+ public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
+ tCount.setText(String.valueOf(progress + 1));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar p1) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar p1) {
+
+ }
+
+ });
+
+ int def = mPrefs.getInt("threads", 4);
+ threads.setProgress(def - 1);
+ tCount.setText(String.valueOf(def));
+
+ if (mPendingUrl != null) {
+ text.setText(mPendingUrl);
+ }
+
+ name.setText(getIntent().getStringExtra("fileName"));
+
+ toolbar.setTitle(R.string.add);
+ toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
+ toolbar.inflateMenu(R.menu.dialog_url);
+
+ // Show the dialog
+ final AlertDialog dialog = new AlertDialog.Builder(this)
+ .setCancelable(true)
+ .setView(v)
+ .create();
+
+ dialog.show();
+
+ fetch.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new NameFetcherTask().execute(text, name);
+ }
+ });
+
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+
+ toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (item.getItemId() == R.id.okay) {
+ String url = text.getText().toString().trim();
+ String fName = name.getText().toString().trim();
+
+ File f = new File(mManager.getLocation() + "/" + fName);
+
+ if (f.exists()) {
+ Toast.makeText(MainActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show();
+ } else if (!checkURL(url)) {
+ Toast.makeText(MainActivity.this, R.string.msg_url_malform, Toast.LENGTH_SHORT).show();
+ } else {
+
+ while (mBinder == null);
+
+ int res = mManager.startMission(url, fName, threads.getProgress() + 1);
+ mBinder.onMissionAdded(mManager.getMission(res));
+ mFragment.notifyChange();
+
+ mPrefs.edit().putInt("threads", threads.getProgress() + 1).commit();
+ mPendingUrl = null;
+ dialog.dismiss();
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+
+ }
+
+ private boolean checkURL(String url) {
+ try {
+ URL u = new URL(url);
+ u.openConnection();
+ return true;
+ } catch (MalformedURLException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private class NameFetcherTask extends AsyncTask {
+
+ @Override
+ protected Object[] doInBackground(View[] params) {
+ try {
+ URL url = new URL(((EditText) params[0]).getText().toString());
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ String header = conn.getHeaderField("Content-Disposition");
+
+ if (header != null && header.indexOf("=") != -1) {
+ return new Object[]{params[1], header.split("=")[1].replace("\"", "")};
+ }
+ } catch (Exception e) {
+
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Object[] result) {
+ super.onPostExecute(result);
+
+ if (result != null) {
+ ((EditText) result[0]).setText(result[1].toString());
+ }
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+
+ }
+
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManager.java b/app/src/main/java/us/shandian/giga/get/DownloadManager.java
new file mode 100644
index 000000000..e1ff8e297
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/DownloadManager.java
@@ -0,0 +1,14 @@
+package us.shandian.giga.get;
+
+public interface DownloadManager
+{
+ public static final int BLOCK_SIZE = 512 * 1024;
+
+ public int startMission(String url, String name, int threads);
+ public void resumeMission(int id);
+ public void pauseMission(int id);
+ public void deleteMission(int id);
+ public DownloadMission getMission(int id);
+ public int getCount();
+ public String getLocation();
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
new file mode 100755
index 000000000..b7067c1a4
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
@@ -0,0 +1,214 @@
+package us.shandian.giga.get;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.google.gson.Gson;
+
+import java.io.File;
+import java.io.RandomAccessFile;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+
+import us.shandian.giga.util.Utility;
+import static org.schabi.newpipe.BuildConfig.DEBUG;
+
+public class DownloadManagerImpl implements DownloadManager
+{
+ private static final String TAG = DownloadManagerImpl.class.getSimpleName();
+
+ private Context mContext;
+ private String mLocation;
+ protected ArrayList mMissions = new ArrayList();
+
+ public DownloadManagerImpl(Context context, String location) {
+ mContext = context;
+ mLocation = location;
+ loadMissions();
+ }
+
+ @Override
+ public int startMission(String url, String name, int threads) {
+ DownloadMission mission = new DownloadMission();
+ mission.url = url;
+ mission.name = name;
+ mission.location = mLocation;
+ mission.timestamp = System.currentTimeMillis();
+ mission.threadCount = threads;
+ new Initializer(mContext, mission).start();
+ return insertMission(mission);
+ }
+
+ @Override
+ public void resumeMission(int i) {
+ DownloadMission d = getMission(i);
+ if (!d.running && d.errCode == -1) {
+ d.start();
+ }
+ }
+
+ @Override
+ public void pauseMission(int i) {
+ DownloadMission d = getMission(i);
+ if (d.running) {
+ d.pause();
+ }
+ }
+
+ @Override
+ public void deleteMission(int i) {
+ getMission(i).delete();
+ mMissions.remove(i);
+ }
+
+ private void loadMissions() {
+ File f = new File(mLocation);
+
+ if (f.exists() && f.isDirectory()) {
+ File[] subs = f.listFiles();
+
+ for (File sub : subs) {
+ if (sub.isDirectory()) {
+ continue;
+ }
+
+ if (sub.getName().endsWith(".giga")) {
+ String str = Utility.readFromFile(sub.getAbsolutePath());
+ if (str != null && !str.trim().equals("")) {
+
+ if (DEBUG) {
+ Log.d(TAG, "loading mission " + sub.getName());
+ Log.d(TAG, str);
+ }
+
+ DownloadMission mis = new Gson().fromJson(str, DownloadMission.class);
+
+ if (mis.finished) {
+ sub.delete();
+ continue;
+ }
+
+ mis.running = false;
+ mis.recovered = true;
+ insertMission(mis);
+ }
+ } else if (!sub.getName().startsWith(".") && !new File(sub.getPath() + ".giga").exists()) {
+ // Add a dummy mission for downloaded files
+ DownloadMission mis = new DownloadMission();
+ mis.length = sub.length();
+ mis.done = mis.length;
+ mis.finished = true;
+ mis.running = false;
+ mis.name = sub.getName();
+ mis.location = mLocation;
+ mis.timestamp = sub.lastModified();
+ insertMission(mis);
+ }
+ }
+ }
+ }
+
+ @Override
+ public DownloadMission getMission(int i) {
+ return mMissions.get(i);
+ }
+
+ @Override
+ public int getCount() {
+ return mMissions.size();
+ }
+
+ private int insertMission(DownloadMission mission) {
+ int i = -1;
+
+ DownloadMission m = null;
+
+ if (mMissions.size() > 0) {
+ do {
+ m = mMissions.get(++i);
+ } while (m.timestamp > mission.timestamp && i < mMissions.size() - 1);
+
+ //if (i > 0) i--;
+ } else {
+ i = 0;
+ }
+
+ mMissions.add(i, mission);
+
+ return i;
+ }
+
+ @Override
+ public String getLocation() {
+ return mLocation;
+ }
+
+ private class Initializer extends Thread {
+ private Context context;
+ private DownloadMission mission;
+
+ public Initializer(Context context, DownloadMission mission) {
+ this.context = context;
+ this.mission = mission;
+ }
+
+ @Override
+ public void run() {
+ try {
+ URL url = new URL(mission.url);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ mission.length = conn.getContentLength();
+
+ if (mission.length <= 0) {
+ mission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
+ //mission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
+ return;
+ }
+
+ // Open again
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestProperty("Range", "bytes=" + (mission.length - 10) + "-" + mission.length);
+
+ if (conn.getResponseCode() != 206) {
+ // Fallback to single thread if no partial content support
+ mission.fallback = true;
+
+ if (DEBUG) {
+ Log.d(TAG, "falling back");
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "response = " + conn.getResponseCode());
+ }
+
+ mission.blocks = mission.length / BLOCK_SIZE;
+
+ if (mission.threadCount > mission.blocks) {
+ mission.threadCount = (int) mission.blocks;
+ }
+
+ if (mission.threadCount <= 0) {
+ mission.threadCount = 1;
+ }
+
+ if (mission.blocks * BLOCK_SIZE < mission.length) {
+ mission.blocks++;
+ }
+
+
+ new File(mission.location).mkdirs();
+ new File(mission.location + "/" + mission.name).createNewFile();
+ RandomAccessFile af = new RandomAccessFile(mission.location + "/" + mission.name, "rw");
+ af.setLength(mission.length);
+ af.close();
+
+ mission.start();
+ } catch (Exception e) {
+ // TODO Notify
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java
new file mode 100644
index 000000000..bbcee7e94
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java
@@ -0,0 +1,229 @@
+package us.shandian.giga.get;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.google.gson.Gson;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.HashMap;
+
+import us.shandian.giga.util.Utility;
+import static org.schabi.newpipe.BuildConfig.DEBUG;
+
+public class DownloadMission
+{
+ private static final String TAG = DownloadMission.class.getSimpleName();
+
+ public static interface MissionListener {
+ HashMap handlerStore = new HashMap<>();
+
+ public void onProgressUpdate(long done, long total);
+ public void onFinish();
+ public void onError(int errCode);
+ }
+
+ public static final int ERROR_SERVER_UNSUPPORTED = 206;
+ public static final int ERROR_UNKNOWN = 233;
+
+ public String name = "";
+ public String url = "";
+ public String location = "";
+ public long blocks = 0;
+ public long length = 0;
+ public long done = 0;
+ public int threadCount = 3;
+ public int finishCount = 0;
+ public ArrayList threadPositions = new ArrayList();
+ public HashMap blockState = new HashMap();
+ public boolean running = false;
+ public boolean finished = false;
+ public boolean fallback = false;
+ public int errCode = -1;
+ public long timestamp = 0;
+
+ public transient boolean recovered = false;
+
+ private transient ArrayList> mListeners = new ArrayList>();
+ private transient boolean mWritingToFile = false;
+
+ public boolean isBlockPreserved(long block) {
+ return blockState.containsKey(block) ? blockState.get(block) : false;
+ }
+
+ public void preserveBlock(long block) {
+ synchronized (blockState) {
+ blockState.put(block, true);
+ }
+ }
+
+ public void setPosition(int id, long position) {
+ threadPositions.set(id, position);
+ }
+
+ public long getPosition(int id) {
+ return threadPositions.get(id);
+ }
+
+ public synchronized void notifyProgress(long deltaLen) {
+ if (!running) return;
+
+ if (recovered) {
+ recovered = false;
+ }
+
+ done += deltaLen;
+
+ if (done > length) {
+ done = length;
+ }
+
+ if (done != length) {
+ writeThisToFile();
+ }
+
+ for (WeakReference ref: mListeners) {
+ final MissionListener listener = ref.get();
+ if (listener != null) {
+ MissionListener.handlerStore.get(listener).post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onProgressUpdate(done, length);
+ }
+ });
+ }
+ }
+ }
+
+ public synchronized void notifyFinished() {
+ if (errCode > 0) return;
+
+ finishCount++;
+
+ if (finishCount == threadCount) {
+ onFinish();
+ }
+ }
+
+ private void onFinish() {
+ if (errCode > 0) return;
+
+ if (DEBUG) {
+ Log.d(TAG, "onFinish");
+ }
+
+ running = false;
+ finished = true;
+
+ deleteThisFromFile();
+
+ for (WeakReference ref : mListeners) {
+ final MissionListener listener = ref.get();
+ if (listener != null) {
+ MissionListener.handlerStore.get(listener).post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onFinish();
+ }
+ });
+ }
+ }
+ }
+
+ public synchronized void notifyError(int err) {
+ errCode = err;
+
+ writeThisToFile();
+
+ for (WeakReference ref : mListeners) {
+ final MissionListener listener = ref.get();
+ MissionListener.handlerStore.get(listener).post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onError(errCode);
+ }
+ });
+ }
+ }
+
+ public synchronized void addListener(MissionListener listener) {
+ Handler handler = new Handler(Looper.getMainLooper());
+ MissionListener.handlerStore.put(listener, handler);
+ mListeners.add(new WeakReference(listener));
+ }
+
+ public synchronized void removeListener(MissionListener listener) {
+ for (Iterator> iterator = mListeners.iterator();
+ iterator.hasNext(); ) {
+ WeakReference weakRef = iterator.next();
+ if (listener!=null && listener == weakRef.get())
+ {
+ iterator.remove();
+ }
+ }
+ }
+
+ public void start() {
+ if (!running && !finished) {
+ running = true;
+
+ if (!fallback) {
+ for (int i = 0; i < threadCount; i++) {
+ if (threadPositions.size() <= i && !recovered) {
+ threadPositions.add((long) i);
+ }
+ new Thread(new DownloadRunnable(this, i)).start();
+ }
+ } else {
+ // In fallback mode, resuming is not supported.
+ threadCount = 1;
+ done = 0;
+ blocks = 0;
+ new Thread(new DownloadRunnableFallback(this)).start();
+ }
+ }
+ }
+
+ public void pause() {
+ if (running) {
+ running = false;
+ recovered = true;
+
+ // TODO: Notify & Write state to info file
+ // if (err)
+ }
+ }
+
+ public void delete() {
+ deleteThisFromFile();
+ new File(location + "/" + name).delete();
+ }
+
+ public void writeThisToFile() {
+ if (!mWritingToFile) {
+ mWritingToFile = true;
+ new Thread() {
+ @Override
+ public void run() {
+ doWriteThisToFile();
+ mWritingToFile = false;
+ }
+ }.start();
+ }
+ }
+
+ private void doWriteThisToFile() {
+ synchronized (blockState) {
+ Utility.writeToFile(location + "/" + name + ".giga", new Gson().toJson(this));
+ }
+ }
+
+ private void deleteThisFromFile() {
+ new File(location + "/" + name + ".giga").delete();
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
new file mode 100644
index 000000000..48990bde6
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
@@ -0,0 +1,174 @@
+package us.shandian.giga.get;
+
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.RandomAccessFile;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static org.schabi.newpipe.BuildConfig.DEBUG;
+
+public class DownloadRunnable implements Runnable
+{
+ private static final String TAG = DownloadRunnable.class.getSimpleName();
+
+ private DownloadMission mMission;
+ private int mId;
+
+ public DownloadRunnable(DownloadMission mission, int id) {
+ mMission = mission;
+ mId = id;
+ }
+
+ @Override
+ public void run() {
+ boolean retry = mMission.recovered;
+ long position = mMission.getPosition(mId);
+
+ if (DEBUG) {
+ Log.d(TAG, mId + ":default pos " + position);
+ Log.d(TAG, mId + ":recovered: " + mMission.recovered);
+ }
+
+ while (mMission.errCode == -1 && mMission.running && position < mMission.blocks) {
+
+ if (Thread.currentThread().isInterrupted()) {
+ mMission.pause();
+ return;
+ }
+
+ if (DEBUG && retry) {
+ Log.d(TAG, mId + ":retry is true. Resuming at " + position);
+ }
+
+ // Wait for an unblocked position
+ while (!retry && position < mMission.blocks && mMission.isBlockPreserved(position)) {
+
+ if (DEBUG) {
+ Log.d(TAG, mId + ":position " + position + " preserved, passing");
+ }
+
+ position++;
+ }
+
+ retry = false;
+
+ if (position >= mMission.blocks) {
+ break;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, mId + ":preserving position " + position);
+ }
+
+ mMission.preserveBlock(position);
+ mMission.setPosition(mId, position);
+
+ long start = position * DownloadManager.BLOCK_SIZE;
+ long end = start + DownloadManager.BLOCK_SIZE - 1;
+
+ if (end >= mMission.length) {
+ end = mMission.length - 1;
+ }
+
+ HttpURLConnection conn = null;
+
+ int total = 0;
+
+ try {
+ URL url = new URL(mMission.url);
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
+
+ if (DEBUG) {
+ Log.d(TAG, mId + ":" + conn.getRequestProperty("Range"));
+ Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode());
+ }
+
+ // A server may be ignoring the range requet
+ if (conn.getResponseCode() != 206) {
+ mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
+ notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
+
+ if (DEBUG) {
+ Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode());
+ }
+
+ break;
+ }
+
+ RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
+ f.seek(start);
+ BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream());
+ byte[] buf = new byte[512];
+
+ while (start < end && mMission.running) {
+ int len = ipt.read(buf, 0, 512);
+
+ if (len == -1) {
+ break;
+ } else {
+ start += len;
+ total += len;
+ f.write(buf, 0, len);
+ notifyProgress(len);
+ }
+ }
+
+ if (DEBUG && mMission.running) {
+ Log.d(TAG, mId + ":position " + position + " finished, total length " + total);
+ }
+
+ f.close();
+ ipt.close();
+
+ // TODO We should save progress for each thread
+ } catch (Exception e) {
+ // TODO Retry count limit & notify error
+ retry = true;
+
+ notifyProgress(-total);
+
+ if (DEBUG) {
+ Log.d(TAG, mId + ":position " + position + " retrying");
+ }
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "thread " + mId + " exited main loop");
+ }
+
+ if (mMission.errCode == -1 && mMission.running) {
+ if (DEBUG) {
+ Log.d(TAG, "no error has happened, notifying");
+ }
+ notifyFinished();
+ }
+
+ if (DEBUG && !mMission.running) {
+ Log.d(TAG, "The mission has been paused. Passing.");
+ }
+ }
+
+ private void notifyProgress(final long len) {
+ synchronized (mMission) {
+ mMission.notifyProgress(len);
+ }
+ }
+
+ private void notifyError(final int err) {
+ synchronized (mMission) {
+ mMission.notifyError(err);
+ mMission.pause();
+ }
+ }
+
+ private void notifyFinished() {
+ synchronized (mMission) {
+ mMission.notifyFinished();
+ }
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
new file mode 100644
index 000000000..50bdce858
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
@@ -0,0 +1,74 @@
+package us.shandian.giga.get;
+
+import java.io.BufferedInputStream;
+import java.io.RandomAccessFile;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+// Single-threaded fallback mode
+public class DownloadRunnableFallback implements Runnable
+{
+ private DownloadMission mMission;
+ //private int mId;
+
+ public DownloadRunnableFallback(DownloadMission mission) {
+ //mId = id;
+ mMission = mission;
+ }
+
+ @Override
+ public void run() {
+ try {
+ URL url = new URL(mMission.url);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+ if (conn.getResponseCode() != 200 && conn.getResponseCode() != 206) {
+ notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
+ } else {
+ RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
+ f.seek(0);
+ BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream());
+ byte[] buf = new byte[512];
+ int len = 0;
+
+ while ((len = ipt.read(buf, 0, 512)) != -1 && mMission.running) {
+ f.write(buf, 0, len);
+ notifyProgress(len);
+
+ if (Thread.currentThread().interrupted()) {
+ break;
+ }
+
+ }
+
+ f.close();
+ ipt.close();
+ }
+ } catch (Exception e) {
+ notifyError(DownloadMission.ERROR_UNKNOWN);
+ }
+
+ if (mMission.errCode == -1 && mMission.running) {
+ notifyFinished();
+ }
+ }
+
+ private void notifyProgress(final long len) {
+ synchronized (mMission) {
+ mMission.notifyProgress(len);
+ }
+ }
+
+ private void notifyError(final int err) {
+ synchronized (mMission) {
+ mMission.notifyError(err);
+ mMission.pause();
+ }
+ }
+
+ private void notifyFinished() {
+ synchronized (mMission) {
+ mMission.notifyFinished();
+ }
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/FilteredDownloadManagerWrapper.java b/app/src/main/java/us/shandian/giga/get/FilteredDownloadManagerWrapper.java
new file mode 100644
index 000000000..c417def15
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/FilteredDownloadManagerWrapper.java
@@ -0,0 +1,88 @@
+package us.shandian.giga.get;
+
+import android.content.Context;
+
+import java.util.Map.Entry;
+import java.util.HashMap;
+
+public class FilteredDownloadManagerWrapper implements DownloadManager
+{
+
+ private boolean mDownloaded = false; // T=Filter downloaded files; F=Filter downloading files
+ private DownloadManager mManager;
+ private HashMap mElementsMap = new HashMap();
+
+ public FilteredDownloadManagerWrapper(DownloadManager manager, boolean filterDownloaded) {
+ mManager = manager;
+ mDownloaded = filterDownloaded;
+ refreshMap();
+ }
+
+ private void refreshMap() {
+ mElementsMap.clear();
+
+ int size = 0;
+ for (int i = 0; i < mManager.getCount(); i++) {
+ if (mManager.getMission(i).finished == mDownloaded) {
+ mElementsMap.put(size++, i);
+ }
+ }
+ }
+
+ private int toRealPosition(int pos) {
+ if (mElementsMap.containsKey(pos)) {
+ return mElementsMap.get(pos);
+ } else {
+ return -1;
+ }
+ }
+
+ private int toFakePosition(int pos) {
+ for (Entry entry : mElementsMap.entrySet()) {
+ if (entry.getValue() == pos) {
+ return entry.getKey();
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public int startMission(String url, String name, int threads) {
+ int ret = mManager.startMission(url, name, threads);
+ refreshMap();
+ return toFakePosition(ret);
+ }
+
+ @Override
+ public void resumeMission(int id) {
+ mManager.resumeMission(toRealPosition(id));
+ }
+
+ @Override
+ public void pauseMission(int id) {
+ mManager.pauseMission(toRealPosition(id));
+ }
+
+ @Override
+ public void deleteMission(int id) {
+ mManager.deleteMission(toRealPosition(id));
+ }
+
+ @Override
+ public DownloadMission getMission(int id) {
+ return mManager.getMission(toRealPosition(id));
+ }
+
+
+ @Override
+ public int getCount() {
+ return mElementsMap.size();
+ }
+
+ @Override
+ public String getLocation() {
+ return mManager.getLocation();
+ }
+
+}
diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
new file mode 100755
index 000000000..52be503a6
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
@@ -0,0 +1,186 @@
+package us.shandian.giga.service;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Message;
+import android.support.v4.app.NotificationCompat.Builder;
+import android.util.Log;
+
+import org.schabi.newpipe.R;
+import us.shandian.giga.get.DownloadManager;
+import us.shandian.giga.get.DownloadManagerImpl;
+import us.shandian.giga.get.DownloadMission;
+import org.schabi.newpipe.download.MainActivity;
+import us.shandian.giga.util.Settings;
+import static org.schabi.newpipe.BuildConfig.DEBUG;
+
+public class DownloadManagerService extends Service implements DownloadMission.MissionListener
+{
+
+ private static final String TAG = DownloadManagerService.class.getSimpleName();
+
+ private DMBinder mBinder;
+ private DownloadManager mManager;
+ private Notification mNotification;
+ private Handler mHandler;
+ private long mLastTimeStamp = System.currentTimeMillis();
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ if (DEBUG) {
+ Log.d(TAG, "onCreate");
+ }
+
+ mBinder = new DMBinder();
+ if (mManager == null) {
+ String path = Settings.getInstance(this).getString(Settings.DOWNLOAD_DIRECTORY, Settings.DEFAULT_PATH);
+ mManager = new DownloadManagerImpl(this, path);
+ if (DEBUG) {
+ Log.d(TAG, "mManager == null");
+ Log.d(TAG, "Download directory: " + path);
+ }
+ }
+
+ Intent i = new Intent();
+ i.setAction(Intent.ACTION_MAIN);
+ i.setClass(this, MainActivity.class);
+
+ Drawable icon = this.getResources().getDrawable(R.mipmap.ic_launcher);
+
+ Builder builder = new Builder(this)
+ .setContentIntent(PendingIntent.getActivity(this, 0, i, 0))
+ .setSmallIcon(android.R.drawable.stat_sys_download)
+ .setLargeIcon(((BitmapDrawable) icon).getBitmap())
+ .setContentTitle(getString(R.string.msg_running))
+ .setContentText(getString(R.string.msg_running_detail));
+
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(
+ this,
+ 0,
+ new Intent(this, MainActivity.class)
+ .setAction(MainActivity.INTENT_LIST),
+ PendingIntent.FLAG_UPDATE_CURRENT
+ );
+
+ builder.setContentIntent(pendingIntent);
+
+ mNotification = builder.build();
+
+ HandlerThread thread = new HandlerThread("ServiceMessenger");
+ thread.start();
+
+ mHandler = new Handler(thread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == 0) {
+ int runningCount = 0;
+
+ for (int i = 0; i < mManager.getCount(); i++) {
+ if (mManager.getMission(i).running) {
+ runningCount++;
+ }
+ }
+
+ updateState(runningCount);
+ }
+ }
+ };
+
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (DEBUG) {
+ Log.d(TAG, "Starting");
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (DEBUG) {
+ Log.d(TAG, "Destroying");
+ }
+
+ for (int i = 0; i < mManager.getCount(); i++) {
+ mManager.pauseMission(i);
+ }
+
+ stopForeground(true);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+
+ @Override
+ public void onProgressUpdate(long done, long total) {
+
+ long now = System.currentTimeMillis();
+
+ long delta = now - mLastTimeStamp;
+
+ if (delta > 2000) {
+ postUpdateMessage();
+ mLastTimeStamp = now;
+ }
+ }
+
+ @Override
+ public void onFinish() {
+ postUpdateMessage();
+ }
+
+ @Override
+ public void onError(int errCode) {
+ postUpdateMessage();
+ }
+
+ private void postUpdateMessage() {
+ mHandler.sendEmptyMessage(0);
+ }
+
+ private void updateState(int runningCount) {
+ if (runningCount == 0) {
+ stopForeground(true);
+ } else {
+ startForeground(1000, mNotification);
+ }
+ }
+
+
+ // Wrapper of DownloadManager
+ public class DMBinder extends Binder {
+ public DownloadManager getDownloadManager() {
+ return mManager;
+ }
+
+ public void onMissionAdded(DownloadMission mission) {
+ mission.addListener(DownloadManagerService.this);
+ postUpdateMessage();
+ }
+
+ public void onMissionRemoved(DownloadMission mission) {
+ mission.removeListener(DownloadManagerService.this);
+ postUpdateMessage();
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
new file mode 100644
index 000000000..4fdbf4f2a
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -0,0 +1,340 @@
+package us.shandian.giga.ui.adapter;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.MimeTypeMap;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.schabi.newpipe.R;
+import us.shandian.giga.get.DownloadManager;
+import us.shandian.giga.get.DownloadMission;
+import us.shandian.giga.service.DownloadManagerService;
+import us.shandian.giga.ui.common.ProgressDrawable;
+import us.shandian.giga.util.Utility;
+
+public class MissionAdapter extends RecyclerView.Adapter
+{
+ private static final Map ALGORITHMS = new HashMap<>();
+
+ static {
+ ALGORITHMS.put(R.id.md5, "MD5");
+ ALGORITHMS.put(R.id.sha1, "SHA1");
+ }
+
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private DownloadManager mManager;
+ private DownloadManagerService.DMBinder mBinder;
+ private int mLayout;
+
+ public MissionAdapter(Context context, DownloadManagerService.DMBinder binder, DownloadManager manager, boolean isLinear) {
+ mContext = context;
+ mManager = manager;
+ mBinder = binder;
+
+ mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item;
+ }
+
+ @Override
+ public MissionAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ final ViewHolder h = new ViewHolder(mInflater.inflate(mLayout, parent, false));
+
+ h.menu.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ buildPopup(h);
+ }
+ });
+
+ /*h.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showDetail(h);
+ }
+ });*/
+
+ return h;
+ }
+
+ @Override
+ public void onViewRecycled(MissionAdapter.ViewHolder h) {
+ super.onViewRecycled(h);
+ h.mission.removeListener(h.observer);
+ h.mission = null;
+ h.observer = null;
+ h.progress = null;
+ h.position = -1;
+ h.lastTimeStamp = -1;
+ h.lastDone = -1;
+ h.colorId = 0;
+ }
+
+ @Override
+ public void onBindViewHolder(MissionAdapter.ViewHolder h, int pos) {
+ DownloadMission ms = mManager.getMission(pos);
+ h.mission = ms;
+ h.position = pos;
+
+ Utility.FileType type = Utility.getFileType(ms.name);
+
+ //h.icon.setImageResource(Utility.getIconForFileType(type));
+ h.name.setText(ms.name);
+ h.size.setText(Utility.formatBytes(ms.length));
+
+ h.progress = new ProgressDrawable(mContext, Utility.getBackgroundForFileType(type), Utility.getForegroundForFileType(type));
+ h.bkg.setBackgroundDrawable(h.progress);
+
+ h.observer = new MissionObserver(this, h);
+ ms.addListener(h.observer);
+
+ updateProgress(h);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mManager.getCount();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ private void updateProgress(ViewHolder h) {
+ updateProgress(h, false);
+ }
+
+ private void updateProgress(ViewHolder h, boolean finished) {
+ if (h.mission == null) return;
+
+ long now = System.currentTimeMillis();
+
+ if (h.lastTimeStamp == -1) {
+ h.lastTimeStamp = now;
+ }
+
+ if (h.lastDone == -1) {
+ h.lastDone = h.mission.done;
+ }
+
+ long deltaTime = now - h.lastTimeStamp;
+ long deltaDone = h.mission.done - h.lastDone;
+
+ if (deltaTime == 0 || deltaTime > 1000 || finished) {
+ if (h.mission.errCode > 0) {
+ h.status.setText(R.string.msg_error);
+ } else {
+ float progress = (float) h.mission.done / h.mission.length;
+ h.status.setText(String.format("%.2f%%", progress * 100));
+ h.progress.setProgress(progress);
+
+ }
+ }
+
+ if (deltaTime > 1000 && deltaDone > 0) {
+ float speed = (float) deltaDone / deltaTime;
+ String speedStr = Utility.formatSpeed(speed * 1000);
+ String sizeStr = Utility.formatBytes(h.mission.length);
+
+ h.size.setText(sizeStr + " " + speedStr);
+
+ h.lastTimeStamp = now;
+ h.lastDone = h.mission.done;
+ }
+ }
+
+
+ private void buildPopup(final ViewHolder h) {
+ PopupMenu popup = new PopupMenu(mContext, h.menu);
+ popup.inflate(R.menu.mission);
+
+ Menu menu = popup.getMenu();
+ MenuItem start = menu.findItem(R.id.start);
+ MenuItem pause = menu.findItem(R.id.pause);
+ MenuItem view = menu.findItem(R.id.view);
+ MenuItem delete = menu.findItem(R.id.delete);
+ MenuItem checksum = menu.findItem(R.id.checksum);
+
+ // Set to false first
+ start.setVisible(false);
+ pause.setVisible(false);
+ view.setVisible(false);
+ delete.setVisible(false);
+ checksum.setVisible(false);
+
+ if (!h.mission.finished) {
+ if (!h.mission.running) {
+ if (h.mission.errCode == -1) {
+ start.setVisible(true);
+ }
+
+ delete.setVisible(true);
+ } else {
+ pause.setVisible(true);
+ }
+ } else {
+ view.setVisible(true);
+ delete.setVisible(true);
+ checksum.setVisible(true);
+ }
+
+ popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ int id = item.getItemId();
+ switch (id) {
+ case R.id.start:
+ mManager.resumeMission(h.position);
+ mBinder.onMissionAdded(mManager.getMission(h.position));
+ return true;
+ case R.id.pause:
+ mManager.pauseMission(h.position);
+ mBinder.onMissionRemoved(mManager.getMission(h.position));
+ h.lastTimeStamp = -1;
+ h.lastDone = -1;
+ return true;
+ case R.id.view:
+ Intent i = new Intent();
+ i.setAction(Intent.ACTION_VIEW);
+ File f = new File(h.mission.location + "/" + h.mission.name);
+ String ext = Utility.getFileExt(h.mission.name);
+
+ if (ext == null) return false;
+
+ String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
+
+ if (f.exists()) {
+ i.setDataAndType(Uri.fromFile(f), mime);
+
+ try {
+ mContext.startActivity(i);
+ } catch (Exception e) {
+
+ }
+ }
+
+ return true;
+ case R.id.delete:
+ mManager.deleteMission(h.position);
+ notifyDataSetChanged();
+ return true;
+ case R.id.md5:
+ case R.id.sha1:
+ DownloadMission mission = mManager.getMission(h.position);
+ new ChecksumTask().execute(mission.location + "/" + mission.name, ALGORITHMS.get(id));
+ return true;
+ default:
+ return false;
+ }
+ }
+ });
+
+ popup.show();
+ }
+
+ private class ChecksumTask extends AsyncTask {
+ ProgressDialog prog;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ // Create dialog
+ prog = new ProgressDialog(mContext);
+ prog.setCancelable(false);
+ prog.setMessage(mContext.getString(R.string.msg_wait));
+ prog.show();
+ }
+
+ @Override
+ protected String doInBackground(String... params) {
+ return Utility.checksum(params[0], params[1]);
+ }
+
+ @Override
+ protected void onPostExecute(String result) {
+ super.onPostExecute(result);
+ prog.dismiss();
+ Utility.copyToClipboard(mContext, result);
+ }
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ public DownloadMission mission;
+ public int position;
+
+ public TextView status;
+ public ImageView icon;
+ public TextView name;
+ public TextView size;
+ public View bkg;
+ public ImageView menu;
+ public ProgressDrawable progress;
+ public MissionObserver observer;
+
+ public long lastTimeStamp = -1;
+ public long lastDone = -1;
+ public int colorId = 0;
+
+ public ViewHolder(View v) {
+ super(v);
+
+ status = Utility.findViewById(v, R.id.item_status);
+ icon = Utility.findViewById(v, R.id.item_icon);
+ name = Utility.findViewById(v, R.id.item_name);
+ size = Utility.findViewById(v, R.id.item_size);
+ bkg = Utility.findViewById(v, R.id.item_bkg);
+ menu = Utility.findViewById(v, R.id.item_more);
+ }
+ }
+
+ static class MissionObserver implements DownloadMission.MissionListener {
+ private MissionAdapter mAdapter;
+ private ViewHolder mHolder;
+
+ public MissionObserver(MissionAdapter adapter, ViewHolder holder) {
+ mAdapter = adapter;
+ mHolder = holder;
+ }
+
+ @Override
+ public void onProgressUpdate(long done, long total) {
+ mAdapter.updateProgress(mHolder);
+ }
+
+ @Override
+ public void onFinish() {
+ //mAdapter.mManager.deleteMission(mHolder.position);
+ // TODO Notification
+ //mAdapter.notifyDataSetChanged();
+ if (mHolder.mission != null) {
+ mHolder.size.setText(Utility.formatBytes(mHolder.mission.length));
+ mAdapter.updateProgress(mHolder, true);
+ }
+ }
+
+ @Override
+ public void onError(int errCode) {
+ mAdapter.updateProgress(mHolder);
+ }
+
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/common/BlockGraphView.java b/app/src/main/java/us/shandian/giga/ui/common/BlockGraphView.java
new file mode 100755
index 000000000..9e2706678
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/common/BlockGraphView.java
@@ -0,0 +1,84 @@
+package us.shandian.giga.ui.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import org.schabi.newpipe.R;
+import us.shandian.giga.get.DownloadMission;
+
+public class BlockGraphView extends View
+{
+ private static int BLOCKS_PER_LINE = 15;
+
+ private int mForeground, mBackground;
+ private int mBlockSize, mLineCount;
+ private DownloadMission mMission;
+
+ public BlockGraphView(Context context) {
+ this(context, null);
+ }
+
+ public BlockGraphView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BlockGraphView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ try {
+ TypedArray array = context.obtainStyledAttributes(R.styleable.AppCompatTheme);
+ mBackground = array.getColor(R.styleable.AppCompatTheme_colorPrimary, 0);
+ mForeground = array.getColor(R.styleable.AppCompatTheme_colorPrimaryDark, 0);
+ array.recycle();
+ } catch (Exception e) {
+
+ }
+ }
+
+ public void setMission(DownloadMission mission) {
+ mMission = mission;
+ setWillNotDraw(false);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ mBlockSize = width / BLOCKS_PER_LINE - 1;
+ mLineCount = (int) Math.ceil((double) mMission.blocks / BLOCKS_PER_LINE);
+ int height = mLineCount * (mBlockSize + 1);
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ Paint p = new Paint();
+ p.setFlags(Paint.ANTI_ALIAS_FLAG);
+
+ for (int i = 0; i < mLineCount; i++) {
+ for (int j = 0; j < BLOCKS_PER_LINE; j++) {
+ long pos = i * BLOCKS_PER_LINE + j;
+ if (pos >= mMission.blocks) {
+ break;
+ }
+
+ if (mMission.isBlockPreserved(pos)) {
+ p.setColor(mForeground);
+ } else {
+ p.setColor(mBackground);
+ }
+
+ int left = (mBlockSize + 1) * j;
+ int right = left + mBlockSize;
+ int top = (mBlockSize + 1) * i;
+ int bottom = top + mBlockSize;
+ canvas.drawRect(left, top, right, bottom, p);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/common/FloatingActionButton.java b/app/src/main/java/us/shandian/giga/ui/common/FloatingActionButton.java
new file mode 100644
index 000000000..424ce388a
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/common/FloatingActionButton.java
@@ -0,0 +1,231 @@
+package us.shandian.giga.ui.common;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.FrameLayout;
+
+/*
+ * From GitHub Gist: https://gist.github.com/Jogan/9def6110edf3247825c9
+ */
+public class FloatingActionButton extends View implements Animator.AnimatorListener {
+
+ final static OvershootInterpolator overshootInterpolator = new OvershootInterpolator();
+ final static AccelerateInterpolator accelerateInterpolator = new AccelerateInterpolator();
+
+ Context context;
+ Paint mButtonPaint;
+ Paint mDrawablePaint;
+ Bitmap mBitmap;
+ boolean mHidden = false;
+
+ public FloatingActionButton(Context context) {
+ super(context);
+ this.context = context;
+ init(Color.WHITE);
+ }
+
+ public void setFloatingActionButtonColor(int FloatingActionButtonColor) {
+ init(FloatingActionButtonColor);
+ }
+
+ public void setFloatingActionButtonDrawable(Drawable FloatingActionButtonDrawable) {
+ mBitmap = ((BitmapDrawable) FloatingActionButtonDrawable).getBitmap();
+ invalidate();
+ }
+
+ public void init(int FloatingActionButtonColor) {
+ setWillNotDraw(false);
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+ mButtonPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mButtonPaint.setColor(FloatingActionButtonColor);
+ mButtonPaint.setStyle(Paint.Style.FILL);
+ mButtonPaint.setShadowLayer(10.0f, 0.0f, 3.5f, Color.argb(100, 0, 0, 0));
+ mDrawablePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ setClickable(true);
+ canvas.drawCircle(getPaddingLeft() + getRealWidth() / 2,
+ getPaddingTop() + getRealHeight() / 2,
+ (float) getRealWidth() / 2.6f, mButtonPaint);
+ canvas.drawBitmap(mBitmap, getPaddingLeft() + (getRealWidth() - mBitmap.getWidth()) / 2,
+ getPaddingTop() + (getRealHeight() - mBitmap.getHeight()) / 2, mDrawablePaint);
+ }
+
+ private int getRealWidth() {
+ return getWidth() - getPaddingLeft() - getPaddingRight();
+ }
+
+ private int getRealHeight() {
+ return getHeight() - getPaddingTop() - getPaddingBottom();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ setAlpha(1.0f);
+ } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ setAlpha(0.6f);
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator anim) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ if (mHidden) {
+ setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator anim) {
+
+ }
+
+ @Override
+ public void onAnimationStart(Animator anim) {
+
+ }
+
+
+ public void hideFloatingActionButton() {
+ if (!mHidden) {
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(this, "scaleX", 1, 0);
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(this, "scaleY", 1, 0);
+ AnimatorSet animSetXY = new AnimatorSet();
+ animSetXY.playTogether(scaleX, scaleY);
+ animSetXY.setInterpolator(accelerateInterpolator);
+ animSetXY.setDuration(100);
+ animSetXY.start();
+ animSetXY.addListener(this);
+ mHidden = true;
+ }
+ }
+
+ public void showFloatingActionButton() {
+ if (mHidden) {
+ setVisibility(View.VISIBLE);
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(this, "scaleX", 0, 1);
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(this, "scaleY", 0, 1);
+ AnimatorSet animSetXY = new AnimatorSet();
+ animSetXY.playTogether(scaleX, scaleY);
+ animSetXY.setInterpolator(overshootInterpolator);
+ animSetXY.setDuration(200);
+ animSetXY.start();
+ mHidden = false;
+ }
+ }
+
+ public boolean isHidden() {
+ return mHidden;
+ }
+
+ static public class Builder {
+ private FrameLayout.LayoutParams params;
+ private final Activity activity;
+ int gravity = Gravity.BOTTOM | Gravity.RIGHT; // default bottom right
+ Drawable drawable;
+ int color = Color.WHITE;
+ int size = 0;
+ float scale = 0;
+ int paddingLeft = 0,
+ paddingTop = 0,
+ paddingBottom = 0,
+ paddingRight = 0;
+
+ public Builder(Activity context) {
+ scale = context.getResources().getDisplayMetrics().density;
+ size = convertToPixels(72, scale); // default size is 72dp by 72dp
+ params = new FrameLayout.LayoutParams(size, size);
+ params.gravity = gravity;
+
+ this.activity = context;
+ }
+
+ /**
+ * Sets the gravity for the FAB
+ */
+ public Builder withGravity(int gravity) {
+ this.gravity = gravity;
+ return this;
+ }
+
+ /**
+ * Sets the margins for the FAB in dp
+ */
+ public Builder withPaddings(int left, int top, int right, int bottom) {
+ paddingLeft = convertToPixels(left, scale);
+ paddingTop = convertToPixels(top, scale);
+ paddingRight = convertToPixels(right, scale);
+ paddingBottom = convertToPixels(bottom, scale);
+ return this;
+ }
+
+ /**
+ * Sets the FAB drawable
+ */
+ public Builder withDrawable(final Drawable drawable) {
+ this.drawable = drawable;
+ return this;
+ }
+
+ /**
+ * Sets the FAB color
+ */
+ public Builder withButtonColor(final int color) {
+ this.color = color;
+ return this;
+ }
+
+ /**
+ * Sets the FAB size in dp
+ */
+ public Builder withButtonSize(int size) {
+ size = convertToPixels(size, scale);
+ params = new FrameLayout.LayoutParams(size, size);
+ return this;
+ }
+
+ public FloatingActionButton create() {
+ final FloatingActionButton button = new FloatingActionButton(activity);
+ button.setFloatingActionButtonColor(this.color);
+ button.setFloatingActionButtonDrawable(this.drawable);
+ button.setPadding(paddingLeft, paddingTop, paddingBottom, paddingRight);
+ params.gravity = this.gravity;
+ ViewGroup root = (ViewGroup) activity.findViewById(android.R.id.content);
+ root.addView(button, params);
+ return button;
+ }
+
+ // The calculation (value * scale + 0.5f) is a widely used to convert to dps to pixel units
+ // based on density scale
+ // see developer.android.com (Supporting Multiple Screen Sizes)
+ private int convertToPixels(int dp, float scale){
+ return (int) (dp * scale + 0.5f) ;
+ }
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java
new file mode 100644
index 000000000..a37532249
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java
@@ -0,0 +1,58 @@
+package us.shandian.giga.ui.common;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+public class ProgressDrawable extends Drawable
+{
+ private float mProgress = 0.0f;
+ private int mBackgroundColor, mForegroundColor;
+
+ public ProgressDrawable(Context context, int background, int foreground) {
+ this(context.getResources().getColor(background), context.getResources().getColor(foreground));
+ }
+
+ public ProgressDrawable(int background, int foreground) {
+ mBackgroundColor = background;
+ mForegroundColor = foreground;
+ }
+
+ public void setProgress(float progress) {
+ mProgress = progress;
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ int width = canvas.getWidth();
+ int height = canvas.getHeight();
+
+ Paint paint = new Paint();
+
+ paint.setColor(mBackgroundColor);
+ canvas.drawRect(0, 0, width, height, paint);
+
+ paint.setColor(mForegroundColor);
+ canvas.drawRect(0, 0, (int) (mProgress * width), height, paint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // Unsupported
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter filter) {
+ // Unsupported
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/common/ToolbarActivity.java b/app/src/main/java/us/shandian/giga/ui/common/ToolbarActivity.java
new file mode 100644
index 000000000..7200bb522
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/common/ToolbarActivity.java
@@ -0,0 +1,26 @@
+package us.shandian.giga.ui.common;
+
+import android.os.Bundle;
+
+import android.support.v7.app.ActionBarActivity;
+import android.support.v7.widget.Toolbar;
+
+import org.schabi.newpipe.R;
+import us.shandian.giga.util.Utility;
+
+public abstract class ToolbarActivity extends ActionBarActivity
+{
+ protected Toolbar mToolbar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(getLayoutResource());
+
+ mToolbar = Utility.findViewById(this, R.id.toolbar);
+
+ setSupportActionBar(mToolbar);
+ }
+
+ protected abstract int getLayoutResource();
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/AllMissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/AllMissionsFragment.java
new file mode 100644
index 000000000..452024223
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/fragment/AllMissionsFragment.java
@@ -0,0 +1,13 @@
+package us.shandian.giga.ui.fragment;
+
+import us.shandian.giga.get.DownloadManager;
+import us.shandian.giga.service.DownloadManagerService;
+
+public class AllMissionsFragment extends MissionsFragment
+{
+
+ @Override
+ protected DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder) {
+ return binder.getDownloadManager();
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/DownloadedMissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/DownloadedMissionsFragment.java
new file mode 100644
index 000000000..1c8c56a36
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/fragment/DownloadedMissionsFragment.java
@@ -0,0 +1,13 @@
+package us.shandian.giga.ui.fragment;
+
+import us.shandian.giga.get.DownloadManager;
+import us.shandian.giga.get.FilteredDownloadManagerWrapper;
+import us.shandian.giga.service.DownloadManagerService;
+
+public class DownloadedMissionsFragment extends MissionsFragment
+{
+ @Override
+ protected DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder) {
+ return new FilteredDownloadManagerWrapper(binder.getDownloadManager(), true);
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/DownloadingMissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/DownloadingMissionsFragment.java
new file mode 100644
index 000000000..79d50904e
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/fragment/DownloadingMissionsFragment.java
@@ -0,0 +1,13 @@
+package us.shandian.giga.ui.fragment;
+
+import us.shandian.giga.get.DownloadManager;
+import us.shandian.giga.get.FilteredDownloadManagerWrapper;
+import us.shandian.giga.service.DownloadManagerService;
+
+public class DownloadingMissionsFragment extends MissionsFragment
+{
+ @Override
+ protected DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder) {
+ return new FilteredDownloadManagerWrapper(binder.getDownloadManager(), false);
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
new file mode 100644
index 000000000..51927051b
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
@@ -0,0 +1,134 @@
+package us.shandian.giga.ui.fragment;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+
+import org.schabi.newpipe.R;
+import us.shandian.giga.get.DownloadManager;
+import us.shandian.giga.service.DownloadManagerService;
+import us.shandian.giga.ui.adapter.MissionAdapter;
+import us.shandian.giga.util.Utility;
+
+public abstract class MissionsFragment extends Fragment
+{
+ private DownloadManager mManager;
+ private DownloadManagerService.DMBinder mBinder;
+
+ private SharedPreferences mPrefs;
+ private boolean mLinear = false;
+ private MenuItem mSwitch;
+
+ private RecyclerView mList;
+ private MissionAdapter mAdapter;
+ private GridLayoutManager mGridManager;
+ private LinearLayoutManager mLinearManager;
+ private Activity mActivity;
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ mBinder = (DownloadManagerService.DMBinder) binder;
+ mManager = setupDownloadManager(mBinder);
+ updateList();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ // What to do?
+ }
+
+
+ };
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.missions, container, false);
+
+ mPrefs = getActivity().getSharedPreferences("mode", Context.MODE_WORLD_READABLE);
+ mLinear = mPrefs.getBoolean("linear", false);
+
+ // Bind the service
+ Intent i = new Intent();
+ i.setClass(getActivity(), DownloadManagerService.class);
+ getActivity().bindService(i, mConnection, Context.BIND_AUTO_CREATE);
+
+ // Views
+ mList = Utility.findViewById(v, R.id.mission_recycler);
+
+ // Init
+ mGridManager = new GridLayoutManager(getActivity(), 2);
+ mLinearManager = new LinearLayoutManager(getActivity());
+ mList.setLayoutManager(mGridManager);
+
+ setHasOptionsMenu(true);
+
+ return v;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mActivity = activity;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.frag_mission, menu);
+ mSwitch = menu.findItem(R.id.switch_mode);
+ mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.switch_mode:
+ mLinear = !mLinear;
+ updateList();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void notifyChange() {
+ mAdapter.notifyDataSetChanged();
+ }
+
+ private void updateList() {
+ mAdapter = new MissionAdapter(mActivity, mBinder, mManager, mLinear);
+
+ if (mLinear) {
+ mList.setLayoutManager(mLinearManager);
+ } else {
+ mList.setLayoutManager(mGridManager);
+ }
+
+ mList.setAdapter(mAdapter);
+
+ if (mSwitch != null) {
+ mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
+ }
+
+ mPrefs.edit().putBoolean("linear", mLinear).commit();
+ }
+
+ protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder);
+}
diff --git a/app/src/main/java/us/shandian/giga/util/CrashHandler.java b/app/src/main/java/us/shandian/giga/util/CrashHandler.java
new file mode 100644
index 000000000..5404e0d39
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/util/CrashHandler.java
@@ -0,0 +1,84 @@
+package us.shandian.giga.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.Build;
+import android.os.Environment;
+
+import java.io.File;
+import java.io.PrintWriter;
+
+public class CrashHandler implements Thread.UncaughtExceptionHandler
+{
+ public static String CRASH_DIR = Environment.getExternalStorageDirectory().getPath() + "/GigaCrash/";
+ public static String CRASH_LOG = CRASH_DIR + "last_crash.log";
+ public static String CRASH_TAG = CRASH_DIR + ".crashed";
+
+ private static String ANDROID = Build.VERSION.RELEASE;
+ private static String MODEL = Build.MODEL;
+ private static String MANUFACTURER = Build.MANUFACTURER;
+
+ public static String VERSION = "Unknown";
+
+ private Thread.UncaughtExceptionHandler mPrevious;
+
+ public static void init(Context context) {
+ try {
+ PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ VERSION = info.versionName + info.versionCode;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void register() {
+ new CrashHandler();
+ }
+
+ private CrashHandler() {
+ mPrevious = Thread.currentThread().getUncaughtExceptionHandler();
+ Thread.currentThread().setUncaughtExceptionHandler(this);
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ File f = new File(CRASH_LOG);
+ if (f.exists()) {
+ f.delete();
+ } else {
+ try {
+ new File(CRASH_DIR).mkdirs();
+ f.createNewFile();
+ } catch (Exception e) {
+ return;
+ }
+ }
+
+ PrintWriter p;
+ try {
+ p = new PrintWriter(f);
+ } catch (Exception e) {
+ return;
+ }
+
+ p.write("Android Version: " + ANDROID + "\n");
+ p.write("Device Model: " + MODEL + "\n");
+ p.write("Device Manufacturer: " + MANUFACTURER + "\n");
+ p.write("App Version: " + VERSION + "\n");
+ p.write("*********************\n");
+ throwable.printStackTrace(p);
+
+ p.close();
+
+ try {
+ new File(CRASH_TAG).createNewFile();
+ } catch (Exception e) {
+ return;
+ }
+
+ if (mPrevious != null) {
+ mPrevious.uncaughtException(thread, throwable);
+ }
+ }
+}
+
diff --git a/app/src/main/java/us/shandian/giga/util/Settings.java b/app/src/main/java/us/shandian/giga/util/Settings.java
new file mode 100644
index 000000000..82ceb2826
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/util/Settings.java
@@ -0,0 +1,60 @@
+package us.shandian.giga.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/*
+ Settings Provider
+*/
+public class Settings
+{
+ public static final String XML_NAME = "settings";
+
+ public static final String DOWNLOAD_DIRECTORY = "download_directory";
+
+ public static final String DEFAULT_PATH = "/storage/sdcard0/GigaGet";
+
+ private static Settings sInstance;
+
+ private SharedPreferences mPrefs;
+
+ public static Settings getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new Settings(context);
+ }
+
+ return sInstance;
+ }
+
+ private Settings(Context context) {
+ mPrefs = context.getSharedPreferences(XML_NAME, Context.MODE_PRIVATE);
+ }
+
+ public Settings putBoolean(String key, boolean value) {
+ mPrefs.edit().putBoolean(key, value).commit();
+ return this;
+ }
+
+ public boolean getBoolean(String key, boolean def) {
+ return mPrefs.getBoolean(key, def);
+ }
+
+ public Settings putInt(String key, int value) {
+ mPrefs.edit().putInt(key, value).commit();
+ return this;
+ }
+
+ public int getInt(String key, int defValue) {
+ return mPrefs.getInt(key, defValue);
+ }
+
+ public Settings putString(String key, String value) {
+ mPrefs.edit().putString(key, value).commit();
+ return this;
+ }
+
+ public String getString(String key, String defValue) {
+ return mPrefs.getString(key, defValue);
+ }
+
+}
diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java
new file mode 100644
index 000000000..acad26209
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/util/Utility.java
@@ -0,0 +1,334 @@
+package us.shandian.giga.util;
+
+import android.app.Activity;
+import android.content.ClipboardManager;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.view.View;
+import android.widget.Toast;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.schabi.newpipe.R;
+import us.shandian.giga.get.DownloadMission;
+import us.shandian.giga.util.Settings;
+
+import com.nononsenseapps.filepicker.FilePickerActivity;
+import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
+
+public class Utility
+{
+
+ public static enum FileType {
+ APP,
+ VIDEO,
+ EXCEL,
+ WORD,
+ POWERPOINT,
+ MUSIC,
+ ARCHIVE,
+ UNKNOWN
+ }
+
+ public static String formatBytes(long bytes) {
+ if (bytes < 1024) {
+ return String.format("%d B", bytes);
+ } else if (bytes < 1024 * 1024) {
+ return String.format("%.2f kB", (float) bytes / 1024);
+ } else if (bytes < 1024 * 1024 * 1024) {
+ return String.format("%.2f MB", (float) bytes / 1024 / 1024);
+ } else {
+ return String.format("%.2f GB", (float) bytes / 1024 / 1024 / 1024);
+ }
+ }
+
+ public static String formatSpeed(float speed) {
+ if (speed < 1024) {
+ return String.format("%.2f B/s", speed);
+ } else if (speed < 1024 * 1024) {
+ return String.format("%.2f kB/s", speed / 1024);
+ } else if (speed < 1024 * 1024 * 1024) {
+ return String.format("%.2f MB/s", speed / 1024 / 1024);
+ } else {
+ return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024);
+ }
+ }
+
+ public static void writeToFile(String fileName, String content) {
+ try {
+ writeToFile(fileName, content.getBytes("UTF-8"));
+ } catch (Exception e) {
+
+ }
+ }
+
+ public static void writeToFile(String fileName, byte[] content) {
+ File f = new File(fileName);
+
+ if (!f.exists()) {
+ try {
+ f.createNewFile();
+ } catch (Exception e) {
+
+ }
+ }
+
+ try {
+ FileOutputStream opt = new FileOutputStream(f, false);
+ opt.write(content, 0, content.length);
+ opt.close();
+ } catch (Exception e) {
+
+ }
+ }
+
+ public static String readFromFile(String file) {
+ try {
+ File f = new File(file);
+
+ if (!f.exists() || !f.canRead()) {
+ return null;
+ }
+
+ BufferedInputStream ipt = new BufferedInputStream(new FileInputStream(f));
+
+ byte[] buf = new byte[512];
+ StringBuilder sb = new StringBuilder();
+
+ while (ipt.available() > 0) {
+ int len = ipt.read(buf, 0, 512);
+ sb.append(new String(buf, 0, len, "UTF-8"));
+ }
+
+ ipt.close();
+ return sb.toString();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static T findViewById(View v, int id) {
+ return (T) v.findViewById(id);
+ }
+
+ public static T findViewById(Activity activity, int id) {
+ return (T) activity.findViewById(id);
+ }
+
+ public static String getFileExt(String url) {
+ if (url.indexOf("?")>-1) {
+ url = url.substring(0,url.indexOf("?"));
+ }
+ if (url.lastIndexOf(".") == -1) {
+ return null;
+ } else {
+ String ext = url.substring(url.lastIndexOf(".") );
+ if (ext.indexOf("%")>-1) {
+ ext = ext.substring(0,ext.indexOf("%"));
+ }
+ if (ext.indexOf("/")>-1) {
+ ext = ext.substring(0,ext.indexOf("/"));
+ }
+ return ext.toLowerCase();
+
+ }
+ }
+
+ public static String getErrorString(Context context, int code) {
+ switch (code) {
+ case DownloadMission.ERROR_SERVER_UNSUPPORTED:
+ return context.getString(R.string.msg_server_unsupported);
+ default:
+ return "";
+ }
+ }
+
+ public static FileType getFileType(String file) {
+ if (file.endsWith(".apk")) {
+ return FileType.APP;
+ } else if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac")) {
+ return FileType.MUSIC;
+ } else if (file.endsWith(".mp4") || file.endsWith(".mpeg") || file.endsWith(".rm") || file.endsWith(".rmvb")
+ || file.endsWith(".flv") || file.endsWith(".webp")) {
+ return FileType.VIDEO;
+ } else if (file.endsWith(".doc") || file.endsWith(".docx")) {
+ return FileType.WORD;
+ } else if (file.endsWith(".xls") || file.endsWith(".xlsx")) {
+ return FileType.EXCEL;
+ } else if (file.endsWith(".ppt") || file.endsWith(".pptx")) {
+ return FileType.POWERPOINT;
+ } else if (file.endsWith(".zip") || file.endsWith(".rar") || file.endsWith(".7z") || file.endsWith(".gz")
+ || file.endsWith("tar") || file.endsWith(".bz")) {
+ return FileType.ARCHIVE;
+ } else {
+ return FileType.UNKNOWN;
+ }
+ }
+
+ public static int getBackgroundForFileType(FileType type) {
+ switch (type) {
+ case APP:
+ return R.color.orange;
+ case MUSIC:
+ return R.color.cyan;
+ case ARCHIVE:
+ return R.color.blue;
+ case VIDEO:
+ return R.color.green;
+ case WORD:
+ case EXCEL:
+ case POWERPOINT:
+ return R.color.brown;
+ case UNKNOWN:
+ default:
+ return R.color.bluegray;
+ }
+ }
+
+ public static int getForegroundForFileType(FileType type) {
+ switch (type) {
+ case APP:
+ return R.color.orange_dark;
+ case MUSIC:
+ return R.color.cyan_dark;
+ case ARCHIVE:
+ return R.color.blue_dark;
+ case VIDEO:
+ return R.color.green_dark;
+ case WORD:
+ case EXCEL:
+ case POWERPOINT:
+ return R.color.brown_dark;
+ case UNKNOWN:
+ default:
+ return R.color.bluegray_dark;
+ }
+ }
+
+ public static int getThemeForFileType(FileType type) {
+ /*switch (type) {
+ case APP:
+ return R.style.Theme_App_Orange;
+ case MUSIC:
+ return R.style.Theme_App_Cyan;
+ case ARCHIVE:
+ return R.style.Theme_App_Blue;
+ case VIDEO:
+ return R.style.Theme_App_Green;
+ case WORD:
+ case EXCEL:
+ case POWERPOINT:
+ return R.style.Theme_App_Brown;
+ case UNKNOWN:
+ default:
+ return R.style.Theme_App_BlueGray;
+ }*/
+ return 0;
+ }
+
+ public static int getIconForFileType(FileType type) {
+ switch (type) {
+ case APP:
+ return R.drawable.apps;
+ case MUSIC:
+ return R.drawable.music;
+ case ARCHIVE:
+ return R.drawable.archive;
+ case VIDEO:
+ return R.drawable.video;
+ case WORD:
+ return R.drawable.word;
+ case EXCEL:
+ return R.drawable.excel;
+ case POWERPOINT:
+ return R.drawable.powerpoint;
+ case UNKNOWN:
+ default:
+ return R.drawable.unknown;
+ }
+ }
+
+ public static boolean isDirectoryAvailble(String path) {
+ File dir = new File(path);
+ return dir.exists() && dir.isDirectory();
+ }
+
+ public static boolean isDownloadDirectoryAvailble(Context context) {
+ return isDirectoryAvailble(Settings.getInstance(context).getString(Settings.DOWNLOAD_DIRECTORY, Settings.DEFAULT_PATH));
+ }
+
+ public static void changeDownloadDirectory(Context context, String path) {
+ Settings.getInstance(context).putString(Settings.DOWNLOAD_DIRECTORY, path);
+ }
+
+ public static void showDirectoryChooser(Activity activity) {
+ Intent i = new Intent(activity, FilePickerActivity.class);
+ i.setAction(Intent.ACTION_GET_CONTENT);
+ i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
+ i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true);
+ i.putExtra(FilePickerActivity.EXTRA_MODE, AbstractFilePickerFragment.MODE_DIR);
+ activity.startActivityForResult(i, 233);
+ }
+
+ public static void checkAndReshow(Activity activity){
+ if (!isDownloadDirectoryAvailble(activity)){
+ Toast.makeText(activity.getApplicationContext(),
+ R.string.no_available_dir, Toast.LENGTH_LONG).show();
+ showDirectoryChooser(activity);
+ }
+ }
+
+ public static void copyToClipboard(Context context, String str) {
+ ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(ClipData.newPlainText("text", str));
+ Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
+ }
+
+ public static String checksum(String path, String algorithm) {
+ MessageDigest md = null;
+
+ try {
+ md = MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+
+ FileInputStream i = null;
+
+ try {
+ i = new FileInputStream(path);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+
+ byte[] buf = new byte[1024];
+ int len = 0;
+
+ try {
+ while ((len = i.read(buf)) != -1) {
+ md.update(buf, 0, len);
+ }
+ } catch (IOException e) {
+
+ }
+
+ byte[] digest = md.digest();
+
+ // HEX
+ StringBuilder sb = new StringBuilder();
+ for (byte b : digest) {
+ sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
+ }
+
+ return sb.toString();
+
+ }
+}
diff --git a/app/src/main/res/drawable-hdpi/drawer_shadow.9.png b/app/src/main/res/drawable-hdpi/drawer_shadow.9.png
new file mode 100644
index 000000000..236bff558
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/drawer_shadow.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/grid.png b/app/src/main/res/drawable-hdpi/grid.png
new file mode 100644
index 000000000..254f1d300
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/grid.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png
new file mode 100644
index 000000000..481643ecd
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_more.png b/app/src/main/res/drawable-hdpi/ic_menu_more.png
new file mode 100644
index 000000000..928fcab8f
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_more.png differ
diff --git a/app/src/main/res/drawable-hdpi/list.png b/app/src/main/res/drawable-hdpi/list.png
new file mode 100644
index 000000000..0b3f54c20
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/list.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/apps.png b/app/src/main/res/drawable-ldrtl-xhdpi/apps.png
new file mode 100644
index 000000000..27a31e90f
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/apps.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/archive.png b/app/src/main/res/drawable-ldrtl-xhdpi/archive.png
new file mode 100644
index 000000000..70ba10c53
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/archive.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/drawer_shadow.9.png b/app/src/main/res/drawable-ldrtl-xhdpi/drawer_shadow.9.png
new file mode 100644
index 000000000..fabe9d965
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/drawer_shadow.9.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/excel.png b/app/src/main/res/drawable-ldrtl-xhdpi/excel.png
new file mode 100644
index 000000000..fe19a6500
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/excel.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/grid.png b/app/src/main/res/drawable-ldrtl-xhdpi/grid.png
new file mode 100644
index 000000000..94bb67f93
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/grid.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-ldrtl-xhdpi/ic_add_white_24dp.png
new file mode 100644
index 000000000..67042105d
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/ic_add_white_24dp.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/ic_menu_more.png b/app/src/main/res/drawable-ldrtl-xhdpi/ic_menu_more.png
new file mode 100644
index 000000000..13596f594
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/ic_menu_more.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/list.png b/app/src/main/res/drawable-ldrtl-xhdpi/list.png
new file mode 100644
index 000000000..905e17c8a
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/list.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/music.png b/app/src/main/res/drawable-ldrtl-xhdpi/music.png
new file mode 100644
index 000000000..130f5da30
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/music.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/pdf.png b/app/src/main/res/drawable-ldrtl-xhdpi/pdf.png
new file mode 100644
index 000000000..9b8005b1f
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/pdf.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/powerpoint.png b/app/src/main/res/drawable-ldrtl-xhdpi/powerpoint.png
new file mode 100644
index 000000000..7d6ca29bc
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/powerpoint.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/unknown.png b/app/src/main/res/drawable-ldrtl-xhdpi/unknown.png
new file mode 100644
index 000000000..5e535f265
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/unknown.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/video.png b/app/src/main/res/drawable-ldrtl-xhdpi/video.png
new file mode 100644
index 000000000..fefb72d9c
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/video.png differ
diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/word.png b/app/src/main/res/drawable-ldrtl-xhdpi/word.png
new file mode 100644
index 000000000..34f3685d8
Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/word.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/grid.png b/app/src/main/res/drawable-xxhdpi/grid.png
new file mode 100644
index 000000000..db7497981
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/grid.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png
new file mode 100644
index 000000000..72cedcad4
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/list.png b/app/src/main/res/drawable-xxhdpi/list.png
new file mode 100644
index 000000000..fbb7c1072
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/list.png differ
diff --git a/app/src/main/res/drawable/action_shadow.xml b/app/src/main/res/drawable/action_shadow.xml
new file mode 100644
index 000000000..e76c1566f
--- /dev/null
+++ b/app/src/main/res/drawable/action_shadow.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_downloader.xml b/app/src/main/res/layout/activity_downloader.xml
new file mode 100644
index 000000000..db8aea4d9
--- /dev/null
+++ b/app/src/main/res/layout/activity_downloader.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_url.xml b/app/src/main/res/layout/dialog_url.xml
new file mode 100644
index 000000000..b797325c1
--- /dev/null
+++ b/app/src/main/res/layout/dialog_url.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/mission_item.xml b/app/src/main/res/layout/mission_item.xml
new file mode 100644
index 000000000..054ebb4eb
--- /dev/null
+++ b/app/src/main/res/layout/mission_item.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/mission_item_linear.xml b/app/src/main/res/layout/mission_item_linear.xml
new file mode 100644
index 000000000..7bbdeecb4
--- /dev/null
+++ b/app/src/main/res/layout/mission_item_linear.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/missions.xml b/app/src/main/res/layout/missions.xml
new file mode 100644
index 000000000..3abfecd8e
--- /dev/null
+++ b/app/src/main/res/layout/missions.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/menu/dialog_url.xml b/app/src/main/res/menu/dialog_url.xml
new file mode 100644
index 000000000..919ddf368
--- /dev/null
+++ b/app/src/main/res/menu/dialog_url.xml
@@ -0,0 +1,10 @@
+
diff --git a/app/src/main/res/menu/frag_mission.xml b/app/src/main/res/menu/frag_mission.xml
new file mode 100644
index 000000000..ec21c6389
--- /dev/null
+++ b/app/src/main/res/menu/frag_mission.xml
@@ -0,0 +1,11 @@
+
diff --git a/app/src/main/res/menu/mission.xml b/app/src/main/res/menu/mission.xml
new file mode 100644
index 000000000..b76d1a923
--- /dev/null
+++ b/app/src/main/res/menu/mission.xml
@@ -0,0 +1,37 @@
+
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
index 75d058e50..2a278569f 100644
--- a/app/src/main/res/values-v21/styles.xml
+++ b/app/src/main/res/values-v21/styles.xml
@@ -34,4 +34,5 @@
- @color/video_overlay_color
- @color/video_overlay_color
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 963a49bcc..9cc8ba2a1 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -17,4 +17,26 @@
#EEFFFFFF
#66000000
#323232
+
+
+ #2979FF
+ #1565C0
+ #607D8B
+ #546E7A
+ #00BCD4
+ #00ACC1
+ #FF9800
+ #EF6C00
+ #4CAF50
+ #388E3C
+ #795548
+ #5D4037
+
+
+ #FFFFFF
+ #EFEFEF
+ #E0E0E0
+ #616161
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7ec9de9bf..c3e0fc12f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -140,4 +140,39 @@
Permission to access storage was denied
Use ExoPlayer
Experimental
+
+
+ Start
+ Pause
+ View
+ Delete
+ Checksum
+
+
+ New mission
+ Okay
+ Switch between list and grid
+
+
+
+ Download URL
+ File name
+ Threads
+ Fetch file name
+ Error
+ Server unsupported
+ File already exists
+ Malformed URL or Internet not available
+ NewPipe Downloading
+ Click for details
+ Please wait...
+ Copied to clipboard.
+ Please select an available download directory.
+
+
+ MD5
+ SHA1
+
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index cb0233f00..e7549b7f7 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -48,4 +48,5 @@
- @color/video_overlay_color
+