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 @@ + + + + + + + + + + + + + + + + + + + + + +