├── .gitignore ├── .travis.yml ├── app ├── build.gradle ├── distr │ ├── ss0.png │ └── ss1.png ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── redwid │ │ └── android │ │ └── youtube │ │ └── dl │ │ └── app │ │ ├── MainApplication.java │ │ ├── gui │ │ ├── FormatAdapter.java │ │ ├── MainActivity.java │ │ └── PopUpDialog.java │ │ ├── model │ │ └── Format.java │ │ └── utils │ │ └── JsonHelper.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ ├── list_longpressed.9.png │ ├── list_pressed.9.png │ ├── list_selector_background.xml │ ├── list_selector_background_disabled.9.png │ ├── list_selector_background_focus.9.png │ ├── list_selector_background_transition.xml │ └── rounded_background.xml │ ├── layout │ ├── activity_main.xml │ ├── pop_up_dialog.xml │ └── row_layout.xml │ ├── menu │ └── main_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ ├── kamranzafar │ │ └── jtar │ │ │ ├── Octal.java │ │ │ ├── TarConstants.java │ │ │ ├── TarEntry.java │ │ │ ├── TarHeader.java │ │ │ ├── TarInputStream.java │ │ │ ├── TarOutputStream.java │ │ │ └── TarUtils.java │ │ └── redwid │ │ └── android │ │ └── youtube │ │ └── dl │ │ ├── TaskWorkerThread.java │ │ ├── YoutubeDlService.java │ │ ├── YoutubeDlWorker.java │ │ └── unpack │ │ ├── AssetExtract.java │ │ ├── GZIPUtils.java │ │ ├── ResourceManager.java │ │ ├── UnpackFilesTask.java │ │ ├── UnpackFilesTaskCallback.java │ │ └── UnpackTask.java │ └── res │ └── values │ └── strings.xml ├── readme.md └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.nar 19 | *.ear 20 | *.zip 21 | *.tar.gz 22 | *.rar 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | ### Android template 27 | # Built application files 28 | *.apk 29 | *.ap_ 30 | 31 | # Files for the ART/Dalvik VM 32 | *.dex 33 | 34 | # Java class files 35 | 36 | # Generated files 37 | bin/ 38 | gen/ 39 | out/ 40 | 41 | # Gradle files 42 | .gradle/ 43 | build/ 44 | 45 | # Local configuration file (sdk path, etc) 46 | local.properties 47 | 48 | # Proguard folder generated by Eclipse 49 | proguard/ 50 | 51 | # Log Files 52 | 53 | # Android Studio Navigation editor temp files 54 | .navigation/ 55 | 56 | # Android Studio captures folder 57 | captures/ 58 | 59 | # IntelliJ 60 | *.iml 61 | .idea 62 | 63 | # Keystore files 64 | # Uncomment the following line if you do not want to check your keystore files in. 65 | #*.jks 66 | 67 | # External native build folder generated in Android Studio 2.2 and later 68 | .externalNativeBuild 69 | 70 | # Google Services (e.g. APIs or Firebase) 71 | google-services.json 72 | 73 | # Freeline 74 | freeline.py 75 | freeline/ 76 | freeline_project_description.json 77 | 78 | # fastlane 79 | fastlane/report.xml 80 | fastlane/Preview.html 81 | fastlane/screenshots 82 | fastlane/test_output 83 | fastlane/readme.md 84 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | # Uncomment the lines below if you want to 5 | # use the latest revision of Android SDK Tools 6 | # - tools 7 | # - platform-tools 8 | 9 | # The BuildTools version used by your project 10 | - build-tools-28.0.3 11 | 12 | # The SDK version used to compile your project 13 | - android-28 14 | 15 | # Additional components 16 | - extra-google-m2repository 17 | - extra-android-m2repository 18 | 19 | # Specify at least one system image, 20 | # if you need to run emulator(s) during your tests 21 | # - sys-img-x86-android-26 22 | # - sys-img-armeabi-v7a-android-17 23 | 24 | licenses: 25 | - 'android-sdk-license-.+' 26 | - 'google-gdk-license-.+' 27 | 28 | 29 | before_deploy: 30 | # Set up git user name and tag this commit 31 | - git config --local user.name Redwid 32 | - git config --local user.email $GIT_USER_EMAIL 33 | - export TRAVIS_TAG=${TRAVIS_TAG:-$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)} 34 | - git tag $TRAVIS_TAG 35 | 36 | deploy: 37 | #dry_run: true 38 | provider: releases 39 | api_key: $GITHUB_OAUTH_TOKEN 40 | file: app/build/outputs/apk/debug/app-debug.apk 41 | skip_cleanup: true 42 | name: '1.0.3' -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "org.redwid.android.youtube.dl.app" 7 | minSdkVersion 14 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0.3" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation "androidx.core:core:1.2.0" 23 | implementation 'androidx.appcompat:appcompat:1.1.0' 24 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 25 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' 26 | 27 | implementation 'com.jakewharton.timber:timber:4.7.1' 28 | 29 | implementation 'com.google.code.gson:gson:2.8.5' 30 | implementation 'com.github.bumptech.glide:glide:4.2.0' 31 | 32 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1' 33 | releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' 34 | 35 | implementation project(path: ':lib') 36 | 37 | testImplementation 'junit:junit:4.12' 38 | androidTestImplementation 'androidx.test:runner:1.3.0-beta05' 39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-alpha05' 40 | } 41 | -------------------------------------------------------------------------------- /app/distr/ss0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redwid/android-youtube-dl/960e7ea545b73c26afda6c91bd19b7d853f7216b/app/distr/ss0.png -------------------------------------------------------------------------------- /app/distr/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redwid/android-youtube-dl/960e7ea545b73c26afda6c91bd19b7d853f7216b/app/distr/ss1.png -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/org/redwid/android/youtube/dl/app/MainApplication.java: -------------------------------------------------------------------------------- 1 | package org.redwid.android.youtube.dl.app; 2 | 3 | import android.app.Application; 4 | 5 | import org.redwid.youtube.dl.android.BuildConfig; 6 | 7 | import com.squareup.leakcanary.LeakCanary; 8 | 9 | import timber.log.Timber; 10 | 11 | /** 12 | * The MainApplication class. 13 | */ 14 | public class MainApplication extends Application { 15 | 16 | @Override 17 | public void onCreate() { 18 | super.onCreate(); 19 | if (LeakCanary.isInAnalyzerProcess(this)) { 20 | // This process is dedicated to LeakCanary for heap analysis. 21 | // You should not init your app in this process. 22 | return; 23 | } 24 | LeakCanary.install(this); 25 | 26 | setupTimber(); 27 | Timber.i("onCreate()"); 28 | } 29 | 30 | private void setupTimber() { 31 | if (BuildConfig.DEBUG) { 32 | Timber.plant(new Timber.DebugTree()); 33 | } else { 34 | //Timber.plant(new CrashlyticsTree()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/org/redwid/android/youtube/dl/app/gui/FormatAdapter.java: -------------------------------------------------------------------------------- 1 | package org.redwid.android.youtube.dl.app.gui; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.TextView; 10 | 11 | import org.redwid.android.youtube.dl.app.R; 12 | import org.redwid.android.youtube.dl.app.model.Format; 13 | 14 | import java.util.List; 15 | 16 | import androidx.annotation.NonNull; 17 | import androidx.recyclerview.widget.RecyclerView; 18 | import timber.log.Timber; 19 | 20 | /** 21 | * The FormatAdapter class. 22 | */ 23 | public class FormatAdapter extends RecyclerView.Adapter { 24 | 25 | private final List formatList; 26 | private final Context context; 27 | 28 | public FormatAdapter(final List formatList, final Context context) { 29 | this.formatList = formatList; 30 | this.context = context; 31 | } 32 | 33 | public Format get(int position) { 34 | return formatList.get(position); 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { 40 | final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_layout, parent, false); 41 | final ViewHolder viewHolder = new ViewHolder(view); 42 | return viewHolder; 43 | } 44 | 45 | @Override 46 | public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { 47 | final Format format = get(position); 48 | if(holder.titleView != null) { 49 | holder.titleView.setText(format.getName()); 50 | } 51 | if(holder.descriptionView != null) { 52 | holder.descriptionView.setText(String.format("acodec: %s, vcodec: %s, ext: %s, %sx%s, file size: %sb", 53 | format.getAcodec(), format.getVcodec(), format.getExt(), format.getWidth(), format.getHeight(), format.getFileSize())); 54 | } 55 | if(position % 2 == 0) { 56 | if(holder.titleView != null) { 57 | holder.titleView.setBackgroundColor(context.getResources().getColor(R.color.rowBackground)); 58 | } 59 | if(holder.descriptionView != null) { 60 | holder.descriptionView.setBackgroundColor(context.getResources().getColor(R.color.rowBackground)); 61 | } 62 | } 63 | else { 64 | if(holder.titleView != null) { 65 | holder.titleView.setBackgroundColor(context.getResources().getColor(R.color.transparent)); 66 | } 67 | if(holder.descriptionView != null) { 68 | holder.descriptionView.setBackgroundColor(context.getResources().getColor(R.color.transparent)); 69 | } 70 | } 71 | holder.itemView.setOnClickListener(new View.OnClickListener() { 72 | @Override 73 | public void onClick(final View v) { 74 | Timber.d("onClick(), url: %s", format.getUrl()); 75 | final Intent intent = new Intent(Intent.ACTION_VIEW); 76 | intent.setData(Uri.parse(format.getUrl())); 77 | try { 78 | context.startActivity(intent); 79 | } catch(Exception e) { 80 | Timber.e(e, "ERROR in onClick(), e: %s", e); 81 | } 82 | } 83 | }); 84 | } 85 | 86 | @Override 87 | public int getItemCount() { 88 | return formatList.size(); 89 | } 90 | 91 | public class ViewHolder extends RecyclerView.ViewHolder { 92 | 93 | public TextView titleView; 94 | public TextView descriptionView; 95 | 96 | public ViewHolder(View view) { 97 | super(view); 98 | titleView = view.findViewById(R.id.title); 99 | descriptionView = view.findViewById(R.id.description); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/org/redwid/android/youtube/dl/app/gui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package org.redwid.android.youtube.dl.app.gui; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.net.Uri; 8 | import android.os.Bundle; 9 | import android.text.TextUtils; 10 | import android.view.Menu; 11 | import android.view.MenuInflater; 12 | import android.view.MenuItem; 13 | import android.view.View; 14 | import android.widget.ImageView; 15 | import android.widget.TextView; 16 | 17 | import com.bumptech.glide.Glide; 18 | import com.bumptech.glide.load.engine.DiskCacheStrategy; 19 | import com.bumptech.glide.request.RequestOptions; 20 | import com.google.gson.Gson; 21 | import com.google.gson.JsonArray; 22 | import com.google.gson.JsonElement; 23 | import com.google.gson.JsonObject; 24 | 25 | import org.redwid.android.youtube.dl.YoutubeDlService; 26 | import org.redwid.android.youtube.dl.app.R; 27 | import org.redwid.android.youtube.dl.app.model.Format; 28 | import org.redwid.android.youtube.dl.app.utils.JsonHelper; 29 | import org.redwid.android.youtube.dl.unpack.GZIPUtils; 30 | 31 | import java.util.ArrayList; 32 | import java.util.Collections; 33 | import java.util.List; 34 | 35 | import androidx.appcompat.app.AppCompatActivity; 36 | import androidx.fragment.app.FragmentManager; 37 | import androidx.recyclerview.widget.LinearLayoutManager; 38 | import androidx.recyclerview.widget.RecyclerView; 39 | import timber.log.Timber; 40 | 41 | import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; 42 | import static org.redwid.android.youtube.dl.YoutubeDlService.VALUE_URL; 43 | 44 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 45 | 46 | public static final String TEXT_PLAIN = "text/plain"; 47 | 48 | private String stringTitle; 49 | private String stringSubTitle; 50 | private String stringDescription; 51 | 52 | private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 53 | @Override 54 | public void onReceive(final Context context, final Intent intent) { 55 | Timber.i("onReceive(), action: %s", intent.getAction()); 56 | final String jsonAsString = GZIPUtils.uncompress(intent.getByteArrayExtra(YoutubeDlService.VALUE_JSON)); 57 | Timber.i("onReceive(): is jsonAsString empty: %s", TextUtils.isEmpty(jsonAsString)); 58 | hideLoading(); 59 | if(YoutubeDlService.JSON_RESULT_SUCCESS.equals(intent.getAction())) { 60 | final Gson gson = new Gson(); 61 | final JsonObject jsonObject = gson.fromJson(jsonAsString, JsonObject.class); 62 | initData(jsonObject); 63 | } 64 | else 65 | if(YoutubeDlService.JSON_RESULT_ERROR.equals(intent.getAction())) { 66 | final Gson gson = new Gson(); 67 | final JsonObject jsonObject = gson.fromJson(jsonAsString, JsonObject.class); 68 | initData(null); 69 | showError(jsonObject, intent.getStringExtra(VALUE_URL)); 70 | } 71 | } 72 | }; 73 | 74 | @Override 75 | protected void onCreate(Bundle savedInstanceState) { 76 | Timber.i("onCreate(%s)", savedInstanceState); 77 | super.onCreate(savedInstanceState); 78 | setContentView(R.layout.activity_main); 79 | initData(null); 80 | registerBroadcastReceiver(); 81 | processSendIntent(getIntent()); 82 | } 83 | 84 | @Override 85 | public boolean onCreateOptionsMenu(Menu menu) { 86 | final MenuInflater inflater = getMenuInflater(); 87 | inflater.inflate(R.menu.main_menu, menu); 88 | return true; 89 | } 90 | 91 | @Override 92 | public boolean onOptionsItemSelected(MenuItem item) { 93 | switch (item.getItemId()) { 94 | case R.id.cancel_all_works: 95 | // final WorkManager workManager = WorkManager.getInstance(); 96 | // workManager.cancelAllWork(); 97 | return true; 98 | 99 | default: 100 | return super.onOptionsItemSelected(item); 101 | } 102 | } 103 | 104 | @Override 105 | protected void onNewIntent(final Intent intent) { 106 | Timber.i("onNewIntent()"); 107 | super.onNewIntent(intent); 108 | processSendIntent(intent); 109 | } 110 | 111 | @Override 112 | protected void onDestroy() { 113 | Timber.i("onDestroy()"); 114 | unregisterReceiver(broadcastReceiver); 115 | super.onDestroy(); 116 | } 117 | 118 | private void registerBroadcastReceiver() { 119 | Timber.i("registerBroadcastReceiver()"); 120 | final IntentFilter intentFilter = new IntentFilter(); 121 | intentFilter.addAction(YoutubeDlService.JSON_RESULT_SUCCESS); 122 | intentFilter.addAction(YoutubeDlService.JSON_RESULT_ERROR); 123 | registerReceiver(broadcastReceiver, intentFilter); 124 | } 125 | 126 | private void processSendIntent(final Intent intent) { 127 | Timber.d("processSendIntent()"); 128 | final String action = intent.getAction(); 129 | final String type = intent.getType(); 130 | if (Intent.ACTION_SEND.equals(action) && type != null) { 131 | if (TEXT_PLAIN.equals(type)) { 132 | final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); 133 | Timber.d("processSendIntent(), action: %s, type: %s, sharedText: %s", sharedText, action, type); 134 | 135 | final Intent serviceIntent = new Intent(YoutubeDlService.ACTION_DUMP_JSON); 136 | serviceIntent.setClass(this, YoutubeDlService.class); 137 | serviceIntent.putExtra(VALUE_URL, Uri.parse(sharedText).toString()); 138 | serviceIntent.putExtra(YoutubeDlService.VALUE_TIME_OUT, 30000l); 139 | startService(serviceIntent); 140 | showLoading(); 141 | } 142 | } 143 | } 144 | 145 | private void initData(final JsonObject jsonObject) { 146 | final View content = findViewById(R.id.content); 147 | final View empty = findViewById(R.id.empty); 148 | 149 | if(jsonObject == null) { 150 | content.setVisibility(View.GONE); 151 | empty.setVisibility(View.VISIBLE); 152 | return; 153 | } 154 | 155 | content.setVisibility(View.VISIBLE); 156 | empty.setVisibility(View.GONE); 157 | 158 | final String thumbnail = JsonHelper.getAsString(jsonObject,"thumbnail"); 159 | final ImageView thumbnailView = findViewById(R.id.thumbnail); 160 | if(thumbnailView != null && !TextUtils.isEmpty(thumbnail)) { 161 | final RequestOptions options = new RequestOptions(). 162 | centerCrop(). 163 | diskCacheStrategy(DiskCacheStrategy.ALL); 164 | Glide.with(this). 165 | load(thumbnail). 166 | apply(options). 167 | transition(withCrossFade()). 168 | into(thumbnailView); 169 | } 170 | 171 | final long duration = JsonHelper.getAsLong(jsonObject,"duration"); 172 | final TextView durationView = findViewById(R.id.duration); 173 | if(durationView != null) { 174 | durationView.setText(formatVideoFileDuration(duration)); 175 | } 176 | 177 | stringTitle = JsonHelper.getAsString(jsonObject,"title"); 178 | final TextView titleView = findViewById(R.id.title); 179 | if(titleView != null) { 180 | titleView.setText(stringTitle); 181 | titleView.setOnClickListener(this); 182 | } 183 | 184 | final String uploader = JsonHelper.getAsString(jsonObject,"uploader", "unknown"); 185 | final String view_count = JsonHelper.getAsString(jsonObject,"view_count", "0"); 186 | final String upload_date = JsonHelper.getAsString(jsonObject,"upload_date", "none"); 187 | stringSubTitle = String.format("%s - %s views - %s", uploader, view_count,upload_date); 188 | 189 | final TextView sub_titleView = findViewById(R.id.sub_title); 190 | if(sub_titleView != null) { 191 | sub_titleView.setText(stringSubTitle); 192 | sub_titleView.setOnClickListener(this); 193 | } 194 | 195 | stringDescription = JsonHelper.getAsString(jsonObject,"description"); 196 | final TextView descriptionView = findViewById(R.id.description); 197 | if(descriptionView != null) { 198 | descriptionView.setText(stringDescription); 199 | descriptionView.setOnClickListener(this); 200 | } 201 | 202 | final List formatList = parseFormats(jsonObject); 203 | if(!formatList.isEmpty()) { 204 | final RecyclerView recycler_view = findViewById(R.id.recycler_view); 205 | recycler_view.setLayoutManager(new LinearLayoutManager(this)); 206 | if(recycler_view != null) { 207 | Collections.sort(formatList); 208 | recycler_view.setAdapter(new FormatAdapter(formatList, this)); 209 | } 210 | } 211 | } 212 | 213 | public String formatVideoFileDuration(long timeInMillisec) { 214 | final String format = String.format("%%0%dd", 2); 215 | timeInMillisec = timeInMillisec / 1000; 216 | String seconds = String.format(format, timeInMillisec % 60); 217 | String minutes = String.format(format, (timeInMillisec % 3600) / 60); 218 | String hours = String.format(format, timeInMillisec / 3600); 219 | String time = ""; 220 | 221 | if(hours.equals("00")) { 222 | time = minutes + ":" + seconds; 223 | } 224 | else { 225 | time = hours + ":" + minutes + ":" + seconds; 226 | } 227 | return time; 228 | } 229 | 230 | public List parseFormats(final JsonObject jsonObject) { 231 | final JsonArray array = jsonObject.getAsJsonArray("formats"); 232 | if(array != null) { 233 | JsonObject item; 234 | final List formatList = new ArrayList<>(); 235 | for (JsonElement jsonElement : array) { 236 | item = ((JsonObject) jsonElement); 237 | formatList.add(new Format(item)); 238 | } 239 | return formatList; 240 | } 241 | return Collections.emptyList(); 242 | } 243 | 244 | protected void showLoading() { 245 | changeLoadingVisibility(View.VISIBLE); 246 | } 247 | 248 | protected void hideLoading() { 249 | changeLoadingVisibility(View.GONE); 250 | } 251 | 252 | protected void changeLoadingVisibility(int visibility) { 253 | final View loading = findViewById(R.id.loading); 254 | if(loading != null) { 255 | loading.setVisibility(visibility); 256 | } 257 | } 258 | 259 | @Override 260 | public void onClick(final View v) { 261 | showPopUpDialog(stringTitle, stringSubTitle, stringDescription); 262 | } 263 | 264 | private void showError(final JsonObject jsonObject, final String stringUrl) { 265 | Timber.d("showError(%s)", jsonObject); 266 | showPopUpDialog(getString(R.string.error_title), getString(R.string.error_text, stringUrl), jsonObject.toString()); 267 | } 268 | 269 | private void showPopUpDialog(final String stringTitle, final String stringSubTitle, final String stringDescription) { 270 | final FragmentManager fragmentManager = getSupportFragmentManager(); 271 | 272 | final Bundle bundle = new Bundle(); 273 | bundle.putString(PopUpDialog.STRING_TITLE, stringTitle); 274 | bundle.putString(PopUpDialog.STRING_SUB_TITLE, stringSubTitle); 275 | bundle.putString(PopUpDialog.STRING_DESCRIPTION, stringDescription); 276 | 277 | final PopUpDialog popUpDialog = new PopUpDialog(); 278 | popUpDialog.setArguments(bundle); 279 | 280 | try { 281 | popUpDialog.show(fragmentManager, "pop_up_dialog"); 282 | } catch(IllegalStateException e) { 283 | Timber.e(e, "ERROR showing popUpDialog.show()"); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /app/src/main/java/org/redwid/android/youtube/dl/app/gui/PopUpDialog.java: -------------------------------------------------------------------------------- 1 | package org.redwid.android.youtube.dl.app.gui; 2 | 3 | import android.app.Dialog; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.Button; 10 | import android.widget.TextView; 11 | 12 | import org.redwid.android.youtube.dl.app.R; 13 | 14 | import androidx.annotation.Nullable; 15 | import androidx.appcompat.app.AppCompatDialogFragment; 16 | 17 | /** 18 | * The PopUpDialog class. 19 | */ 20 | public class PopUpDialog extends AppCompatDialogFragment implements View.OnClickListener { 21 | 22 | private static final String LOG_TAG = "PopUpDialog"; 23 | 24 | public static final String STRING_TITLE = "title"; 25 | public static final String STRING_SUB_TITLE = "sub_title"; 26 | public static final String STRING_DESCRIPTION = "description"; 27 | 28 | private String stringTitle; 29 | private String stringSubTitle; 30 | private String stringDescription; 31 | 32 | @Override 33 | public void onCreate(@Nullable final Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setStyle(STYLE_NO_TITLE, 0); 36 | setRetainInstance(true); 37 | 38 | if(getArguments() != null) { 39 | stringTitle = getArguments().getString(STRING_TITLE); 40 | stringSubTitle = getArguments().getString(STRING_SUB_TITLE); 41 | stringDescription = getArguments().getString(STRING_DESCRIPTION); 42 | } 43 | } 44 | 45 | @Override 46 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 47 | return inflater.inflate(R.layout.pop_up_dialog, null); 48 | } 49 | 50 | @Override 51 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 52 | super.onViewCreated(view, savedInstanceState); 53 | 54 | TextView textView = view.findViewById(R.id.title); 55 | if(textView != null) { 56 | textView.setText(stringTitle); 57 | } 58 | textView = view.findViewById(R.id.sub_title); 59 | if(textView != null) { 60 | textView.setText(stringSubTitle); 61 | } 62 | textView = view.findViewById(R.id.description); 63 | if(textView != null) { 64 | textView.setText(stringDescription); 65 | } 66 | 67 | final Button okButton = view.findViewById(R.id.ok_button); 68 | okButton.setOnClickListener(this); 69 | 70 | final Dialog dialog = getDialog(); 71 | if(dialog != null) { 72 | } 73 | } 74 | 75 | @Override 76 | public void onClick(final View v) { 77 | Log.d(LOG_TAG, "onClick()"); 78 | if(v.getId() == R.id.ok_button) { 79 | final Dialog dialog = getDialog(); 80 | if (dialog != null) { 81 | dialog.dismiss(); 82 | } 83 | } 84 | } 85 | 86 | // private Spanned getCountString(final long value) { 87 | // final String string = getString(R.string.switching_list_dialog_count, value/1000); 88 | // return Html.fromHtml(string); 89 | // } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/org/redwid/android/youtube/dl/app/model/Format.java: -------------------------------------------------------------------------------- 1 | package org.redwid.android.youtube.dl.app.model; 2 | 3 | import com.google.gson.JsonObject; 4 | 5 | import org.redwid.android.youtube.dl.app.utils.JsonHelper; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | /** 10 | * The Format class. 11 | */ 12 | public class Format implements Comparable { 13 | 14 | String name; 15 | String url; 16 | String vcodec; 17 | String acodec; 18 | String width; 19 | String height; 20 | String ext; 21 | String fileSize; 22 | String formatId; 23 | 24 | public Format(final JsonObject item) { 25 | name = JsonHelper.getAsString(item, "format"); 26 | url = JsonHelper.getAsString(item, "url"); 27 | acodec = JsonHelper.getAsString(item, "acodec"); 28 | vcodec = JsonHelper.getAsString(item, "vcodec"); 29 | width = JsonHelper.getAsString(item, "width"); 30 | height = JsonHelper.getAsString(item, "height"); 31 | ext = JsonHelper.getAsString(item, "ext"); 32 | fileSize = JsonHelper.getAsString(item, "filesize"); 33 | formatId = JsonHelper.getAsString(item, "format_id"); 34 | } 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | 40 | public String getUrl() { 41 | return url; 42 | } 43 | 44 | public String getVcodec() { 45 | return vcodec; 46 | } 47 | 48 | public String getAcodec() { 49 | return acodec; 50 | } 51 | 52 | public String getWidth() { 53 | return width; 54 | } 55 | 56 | public String getHeight() { 57 | return height; 58 | } 59 | 60 | public String getExt() { 61 | return ext; 62 | } 63 | 64 | public String getFileSize() { 65 | return fileSize; 66 | } 67 | 68 | public String getFormatId() { 69 | return formatId; 70 | } 71 | 72 | @Override 73 | public int compareTo(@NonNull final Format o) { 74 | if(formatId != null) { 75 | return formatId.compareTo(o.getFormatId()); 76 | } 77 | return 1; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/org/redwid/android/youtube/dl/app/utils/JsonHelper.java: -------------------------------------------------------------------------------- 1 | package org.redwid.android.youtube.dl.app.utils; 2 | 3 | import com.google.gson.JsonObject; 4 | 5 | /** 6 | * The JsonHelper class. 7 | */ 8 | public final class JsonHelper { 9 | 10 | public static String getAsString(final JsonObject item, final String key) { 11 | return getAsString(item, key, ""); 12 | } 13 | 14 | public static String getAsString(final JsonObject item, final String key, final String defaultValue) { 15 | if(item != null && item.has(key)) { 16 | try { 17 | return item.get(key).getAsString(); 18 | } catch(UnsupportedOperationException ignore) { 19 | 20 | } 21 | } 22 | return defaultValue; 23 | } 24 | 25 | public static long getAsLong(final JsonObject item, final String key) { 26 | if(item != null && item.has(key)) { 27 | try { 28 | return item.get(key).getAsLong(); 29 | } catch(UnsupportedOperationException ignore) { 30 | 31 | } 32 | } 33 | return 0; 34 | } 35 | 36 | public static int getAsInt(final JsonObject item, final String key) { 37 | if(item != null && item.has(key)) { 38 | try { 39 | return item.get(key).getAsInt(); 40 | } catch(UnsupportedOperationException ignore) { 41 | 42 | } 43 | } 44 | return 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_longpressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redwid/android-youtube-dl/960e7ea545b73c26afda6c91bd19b7d853f7216b/app/src/main/res/drawable/list_longpressed.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redwid/android-youtube-dl/960e7ea545b73c26afda6c91bd19b7d853f7216b/app/src/main/res/drawable/list_pressed.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_selector_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_selector_background_disabled.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redwid/android-youtube-dl/960e7ea545b73c26afda6c91bd19b7d853f7216b/app/src/main/res/drawable/list_selector_background_disabled.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_selector_background_focus.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redwid/android-youtube-dl/960e7ea545b73c26afda6c91bd19b7d853f7216b/app/src/main/res/drawable/list_selector_background_focus.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_selector_background_transition.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 25 | 26 | 42 | 43 | 54 | 55 | 66 | 67 | 79 | 80 | 93 | 94 | 103 | 104 | 105 | 117 | 118 | 129 | 130 | 140 | 141 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /app/src/main/res/layout/pop_up_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 25 | 26 | 43 | 44 | 62 | 63 | 70 | 71 | 72 |