Merge remote-tracking branch 'newpipe/dev' into rebase
| @@ -62,7 +62,8 @@ dependencies { | |||||||
|         exclude module: 'support-annotations' |         exclude module: 'support-annotations' | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c420340ceb39' |     implementation 'com.github.TeamNewPipe:NewPipeExtractor:43b54cc' | ||||||
|  |  | ||||||
|     testImplementation 'junit:junit:4.12' |     testImplementation 'junit:junit:4.12' | ||||||
|     testImplementation 'org.mockito:mockito-core:2.23.0' |     testImplementation 'org.mockito:mockito-core:2.23.0' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,9 +6,9 @@ import android.app.NotificationChannel; | |||||||
| import android.app.NotificationManager; | import android.app.NotificationManager; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
|  | import android.util.Log; | ||||||
|  |  | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import android.util.Log; |  | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; | import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; | ||||||
| import com.nostra13.universalimageloader.core.ImageLoader; | import com.nostra13.universalimageloader.core.ImageLoader; | ||||||
| @@ -29,6 +29,7 @@ import org.schabi.newpipe.report.UserAction; | |||||||
| import org.schabi.newpipe.settings.SettingsActivity; | import org.schabi.newpipe.settings.SettingsActivity; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
|  | import org.schabi.newpipe.util.ServiceHelper; | ||||||
| import org.schabi.newpipe.util.StateSaver; | import org.schabi.newpipe.util.StateSaver; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| @@ -103,6 +104,8 @@ public class App extends Application { | |||||||
|         StateSaver.init(this); |         StateSaver.init(this); | ||||||
|         initNotificationChannel(); |         initNotificationChannel(); | ||||||
|  |  | ||||||
|  |         ServiceHelper.initServices(this); | ||||||
|  |  | ||||||
|         // Initialize image loader |         // Initialize image loader | ||||||
|         ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); |         ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,14 +32,18 @@ import android.preference.PreferenceManager; | |||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.Gravity; | import android.view.Gravity; | ||||||
| import android.view.KeyEvent; | import android.view.KeyEvent; | ||||||
|  | import android.view.LayoutInflater; | ||||||
| import android.view.Menu; | import android.view.Menu; | ||||||
| import android.view.MenuInflater; | import android.view.MenuInflater; | ||||||
| import android.view.MenuItem; | import android.view.MenuItem; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.Window; | import android.view.Window; | ||||||
| import android.view.WindowManager; | import android.view.WindowManager; | ||||||
|  | import android.widget.AdapterView; | ||||||
|  | import android.widget.ArrayAdapter; | ||||||
| import android.widget.Button; | import android.widget.Button; | ||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
|  | import android.widget.Spinner; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| @@ -50,12 +54,15 @@ import androidx.appcompat.widget.Toolbar; | |||||||
| import androidx.core.view.GravityCompat; | import androidx.core.view.GravityCompat; | ||||||
| import androidx.drawerlayout.widget.DrawerLayout; | import androidx.drawerlayout.widget.DrawerLayout; | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.fragment.app.FragmentManager; | ||||||
|  |  | ||||||
| import com.google.android.material.navigation.NavigationView; | import com.google.android.material.navigation.NavigationView; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
|  | import org.schabi.newpipe.extractor.ServiceList; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  | import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; | ||||||
| import org.schabi.newpipe.fragments.BackPressable; | import org.schabi.newpipe.fragments.BackPressable; | ||||||
| import org.schabi.newpipe.fragments.MainFragment; | import org.schabi.newpipe.fragments.MainFragment; | ||||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||||
| @@ -65,12 +72,16 @@ import org.schabi.newpipe.util.Constants; | |||||||
| import org.schabi.newpipe.util.FireTvUtils; | import org.schabi.newpipe.util.FireTvUtils; | ||||||
| import org.schabi.newpipe.util.KioskTranslator; | import org.schabi.newpipe.util.KioskTranslator; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
|  | import org.schabi.newpipe.util.PeertubeHelper; | ||||||
| import org.schabi.newpipe.util.PermissionHelper; | import org.schabi.newpipe.util.PermissionHelper; | ||||||
| import org.schabi.newpipe.util.ServiceHelper; | import org.schabi.newpipe.util.ServiceHelper; | ||||||
| import org.schabi.newpipe.util.StateSaver; | import org.schabi.newpipe.util.StateSaver; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
| import org.schabi.newpipe.views.FocusOverlayView; | import org.schabi.newpipe.views.FocusOverlayView; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
| public class MainActivity extends AppCompatActivity { | public class MainActivity extends AppCompatActivity { | ||||||
|     private static final String TAG = "MainActivity"; |     private static final String TAG = "MainActivity"; | ||||||
|     public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); |     public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); | ||||||
| @@ -309,13 +320,57 @@ public class MainActivity extends AppCompatActivity { | |||||||
|             final String title = s.getServiceInfo().getName() + |             final String title = s.getServiceInfo().getName() + | ||||||
|                     (ServiceHelper.isBeta(s) ? " (beta)" : ""); |                     (ServiceHelper.isBeta(s) ? " (beta)" : ""); | ||||||
|  |  | ||||||
|             drawerItems.getMenu() |             MenuItem menuItem = drawerItems.getMenu() | ||||||
|                     .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) |                     .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) | ||||||
|                     .setIcon(ServiceHelper.getIcon(s.getServiceId())); |                     .setIcon(ServiceHelper.getIcon(s.getServiceId())); | ||||||
|  |  | ||||||
|  |             // peertube specifics | ||||||
|  |             if(s.getServiceId() == 3){ | ||||||
|  |                 enhancePeertubeMenu(s, menuItem); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); |         drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) { | ||||||
|  |         PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance(); | ||||||
|  |         menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); | ||||||
|  |         Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null); | ||||||
|  |         List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this); | ||||||
|  |         List<String> items = new ArrayList<>(); | ||||||
|  |         int defaultSelect = 0; | ||||||
|  |         for(PeertubeInstance instance: instances){ | ||||||
|  |             items.add(instance.getName()); | ||||||
|  |             if(instance.getUrl().equals(currentInstace.getUrl())){ | ||||||
|  |                 defaultSelect = items.size()-1; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items); | ||||||
|  |         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); | ||||||
|  |         spinner.setAdapter(adapter); | ||||||
|  |         spinner.setSelection(defaultSelect, false); | ||||||
|  |         spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { | ||||||
|  |                 PeertubeInstance newInstance = instances.get(position); | ||||||
|  |                 if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return; | ||||||
|  |                 PeertubeHelper.selectInstance(newInstance, getApplicationContext()); | ||||||
|  |                 changeService(menuItem); | ||||||
|  |                 drawer.closeDrawers(); | ||||||
|  |                 new Handler(Looper.getMainLooper()).postDelayed(() -> { | ||||||
|  |                     getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); | ||||||
|  |                     recreate(); | ||||||
|  |                 }, 300); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onNothingSelected(AdapterView<?> parent) { | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         menuItem.setActionView(spinner); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private void showTabs() throws ExtractionException { |     private void showTabs() throws ExtractionException { | ||||||
|         serviceArrow.setImageResource(R.drawable.ic_arrow_down_white); |         serviceArrow.setImageResource(R.drawable.ic_arrow_down_white); | ||||||
|  |  | ||||||
| @@ -376,6 +431,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|             String selectedServiceName = NewPipe.getService( |             String selectedServiceName = NewPipe.getService( | ||||||
|                     ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); |                     ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); | ||||||
|             headerServiceView.setText(selectedServiceName); |             headerServiceView.setText(selectedServiceName); | ||||||
|  |             headerServiceView.post(() -> headerServiceView.setSelected(true)); | ||||||
|             toggleServiceButton.setContentDescription( |             toggleServiceButton.setContentDescription( | ||||||
|                     getString(R.string.drawer_header_description) + selectedServiceName); |                     getString(R.string.drawer_header_description) + selectedServiceName); | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|   | |||||||
| @@ -68,6 +68,7 @@ import java.util.Locale; | |||||||
| import icepick.Icepick; | import icepick.Icepick; | ||||||
| import icepick.State; | import icepick.State; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | import us.shandian.giga.get.MissionRecoveryInfo; | ||||||
| import us.shandian.giga.io.StoredDirectoryHelper; | import us.shandian.giga.io.StoredDirectoryHelper; | ||||||
| import us.shandian.giga.io.StoredFileHelper; | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.postprocessing.Postprocessing; | import us.shandian.giga.postprocessing.Postprocessing; | ||||||
| @@ -762,12 +763,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         Stream selectedStream; |         Stream selectedStream; | ||||||
|  |         Stream secondaryStream = null; | ||||||
|         char kind; |         char kind; | ||||||
|         int threads = threadsSeekBar.getProgress() + 1; |         int threads = threadsSeekBar.getProgress() + 1; | ||||||
|         String[] urls; |         String[] urls; | ||||||
|  |         MissionRecoveryInfo[] recoveryInfo; | ||||||
|         String psName = null; |         String psName = null; | ||||||
|         String[] psArgs = null; |         String[] psArgs = null; | ||||||
|         String secondaryStreamUrl = null; |  | ||||||
|         long nearLength = 0; |         long nearLength = 0; | ||||||
|  |  | ||||||
|         // more download logic: select muxer, subtitle converter, etc. |         // more download logic: select muxer, subtitle converter, etc. | ||||||
| @@ -778,18 +780,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|  |  | ||||||
|                 if (selectedStream.getFormat() == MediaFormat.M4A) { |                 if (selectedStream.getFormat() == MediaFormat.M4A) { | ||||||
|                     psName = Postprocessing.ALGORITHM_M4A_NO_DASH; |                     psName = Postprocessing.ALGORITHM_M4A_NO_DASH; | ||||||
|  |                 } else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) { | ||||||
|  |                     psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER; | ||||||
|                 } |                 } | ||||||
|                 break; |                 break; | ||||||
|             case R.id.video_button: |             case R.id.video_button: | ||||||
|                 kind = 'v'; |                 kind = 'v'; | ||||||
|                 selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); |                 selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); | ||||||
|  |  | ||||||
|                 SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter |                 SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter | ||||||
|                         .getAllSecondary() |                         .getAllSecondary() | ||||||
|                         .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); |                         .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); | ||||||
|  |  | ||||||
|                 if (secondaryStream != null) { |                 if (secondary != null) { | ||||||
|                     secondaryStreamUrl = secondaryStream.getStream().getUrl(); |                     secondaryStream = secondary.getStream(); | ||||||
|  |  | ||||||
|                     if (selectedStream.getFormat() == MediaFormat.MPEG_4) |                     if (selectedStream.getFormat() == MediaFormat.MPEG_4) | ||||||
|                         psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER; |                         psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER; | ||||||
| @@ -801,8 +805,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|  |  | ||||||
|                     // set nearLength, only, if both sizes are fetched or known. This probably |                     // set nearLength, only, if both sizes are fetched or known. This probably | ||||||
|                     // does not work on slow networks but is later updated in the downloader |                     // does not work on slow networks but is later updated in the downloader | ||||||
|                     if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) { |                     if (secondary.getSizeInBytes() > 0 && videoSize > 0) { | ||||||
|                         nearLength = secondaryStream.getSizeInBytes() + videoSize; |                         nearLength = secondary.getSizeInBytes() + videoSize; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 break; |                 break; | ||||||
| @@ -824,13 +828,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|                 return; |                 return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (secondaryStreamUrl == null) { |         if (secondaryStream == null) { | ||||||
|             urls = new String[]{selectedStream.getUrl()}; |             urls = new String[]{ | ||||||
|  |                     selectedStream.getUrl() | ||||||
|  |             }; | ||||||
|  |             recoveryInfo = new MissionRecoveryInfo[]{ | ||||||
|  |                     new MissionRecoveryInfo(selectedStream) | ||||||
|  |             }; | ||||||
|         } else { |         } else { | ||||||
|             urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; |             urls = new String[]{ | ||||||
|  |                     selectedStream.getUrl(), secondaryStream.getUrl() | ||||||
|  |             }; | ||||||
|  |             recoveryInfo = new MissionRecoveryInfo[]{ | ||||||
|  |                     new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream) | ||||||
|  |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); |         DownloadManagerService.startMission( | ||||||
|  |                 context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         dismiss(); |         dismiss(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.InfoItem; | |||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||||
| @@ -98,7 +99,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|     @Override |     @Override | ||||||
|     public void setUserVisibleHint(boolean isVisibleToUser) { |     public void setUserVisibleHint(boolean isVisibleToUser) { | ||||||
|         super.setUserVisibleHint(isVisibleToUser); |         super.setUserVisibleHint(isVisibleToUser); | ||||||
|         if(activity != null |         if (activity != null | ||||||
|                 && useAsFrontPage |                 && useAsFrontPage | ||||||
|                 && isVisibleToUser) { |                 && isVisibleToUser) { | ||||||
|             setTitle(currentInfo != null ? currentInfo.getName() : name); |             setTitle(currentInfo != null ? currentInfo.getName() : name); | ||||||
| @@ -152,7 +153,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||||
|         super.onCreateOptionsMenu(menu, inflater); |         super.onCreateOptionsMenu(menu, inflater); | ||||||
|         ActionBar supportActionBar = activity.getSupportActionBar(); |         ActionBar supportActionBar = activity.getSupportActionBar(); | ||||||
|         if(useAsFrontPage && supportActionBar != null) { |         if (useAsFrontPage && supportActionBar != null) { | ||||||
|             supportActionBar.setDisplayHomeAsUpEnabled(false); |             supportActionBar.setDisplayHomeAsUpEnabled(false); | ||||||
|         } else { |         } else { | ||||||
|             inflater.inflate(R.menu.menu_channel, menu); |             inflater.inflate(R.menu.menu_channel, menu); | ||||||
| @@ -165,7 +166,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|  |  | ||||||
|     private void openRssFeed() { |     private void openRssFeed() { | ||||||
|         final ChannelInfo info = currentInfo; |         final ChannelInfo info = currentInfo; | ||||||
|         if(info != null) { |         if (info != null) { | ||||||
|             Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); |             Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); | ||||||
|             startActivity(intent); |             startActivity(intent); | ||||||
|         } |         } | ||||||
| @@ -178,10 +179,14 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|                 openRssFeed(); |                 openRssFeed(); | ||||||
|                 break; |                 break; | ||||||
|             case R.id.menu_item_openInBrowser: |             case R.id.menu_item_openInBrowser: | ||||||
|                 ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl()); |                 if (currentInfo != null) { | ||||||
|  |                     ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl()); | ||||||
|  |                 } | ||||||
|                 break; |                 break; | ||||||
|             case R.id.menu_item_share: |             case R.id.menu_item_share: | ||||||
|                 ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl()); |                 if (currentInfo != null) { | ||||||
|  |                     ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl()); | ||||||
|  |                 } | ||||||
|                 break; |                 break; | ||||||
|             default: |             default: | ||||||
|                 return super.onOptionsItemSelected(item); |                 return super.onOptionsItemSelected(item); | ||||||
| @@ -218,7 +223,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|                 .debounce(100, TimeUnit.MILLISECONDS) |                 .debounce(100, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe((List<SubscriptionEntity> subscriptionEntities) -> |                 .subscribe((List<SubscriptionEntity> subscriptionEntities) -> | ||||||
|                         updateSubscribeButton(!subscriptionEntities.isEmpty()) |                                 updateSubscribeButton(!subscriptionEntities.isEmpty()) | ||||||
|                         , onError)); |                         , onError)); | ||||||
|  |  | ||||||
|     } |     } | ||||||
| @@ -359,9 +364,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|  |  | ||||||
|         headerRootLayout.setVisibility(View.VISIBLE); |         headerRootLayout.setVisibility(View.VISIBLE); | ||||||
|         imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, |         imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, | ||||||
|         		ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); |                 ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); | ||||||
|         imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, |         imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, | ||||||
|         		ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); |                 ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); | ||||||
|  |  | ||||||
|         headerSubscribersTextView.setVisibility(View.VISIBLE); |         headerSubscribersTextView.setVisibility(View.VISIBLE); | ||||||
|         if (result.getSubscriberCount() >= 0) { |         if (result.getSubscriberCount() >= 0) { | ||||||
| @@ -397,8 +402,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|  |  | ||||||
|     private PlayQueue getPlayQueue(final int index) { |     private PlayQueue getPlayQueue(final int index) { | ||||||
|         final List<StreamInfoItem> streamItems = new ArrayList<>(); |         final List<StreamInfoItem> streamItems = new ArrayList<>(); | ||||||
|         for(InfoItem i : infoListAdapter.getItemsList()) { |         for (InfoItem i : infoListAdapter.getItemsList()) { | ||||||
|             if(i instanceof StreamInfoItem) { |             if (i instanceof StreamInfoItem) { | ||||||
|                 streamItems.add((StreamInfoItem) i); |                 streamItems.add((StreamInfoItem) i); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -432,12 +437,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|     protected boolean onError(Throwable exception) { |     protected boolean onError(Throwable exception) { | ||||||
|         if (super.onError(exception)) return true; |         if (super.onError(exception)) return true; | ||||||
|  |  | ||||||
|         int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; |         if (exception instanceof ContentNotAvailableException) { | ||||||
|         onUnrecoverableError(exception, |             showError(getString(R.string.content_not_available), false); | ||||||
|                 UserAction.REQUESTED_CHANNEL, |         } else { | ||||||
|                 NewPipe.getNameOfService(serviceId), |             int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; | ||||||
|                 url, |             onUnrecoverableError(exception, | ||||||
|                 errorId); |                     UserAction.REQUESTED_CHANNEL, | ||||||
|  |                     NewPipe.getNameOfService(serviceId), | ||||||
|  |                     url, | ||||||
|  |                     errorId); | ||||||
|  |         } | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,14 +26,13 @@ import android.animation.PropertyValuesHolder; | |||||||
| import android.animation.ValueAnimator; | import android.animation.ValueAnimator; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
|  | import android.content.SharedPreferences; | ||||||
| import android.graphics.Bitmap; | import android.graphics.Bitmap; | ||||||
| import android.graphics.Color; | import android.graphics.Color; | ||||||
| import android.graphics.PorterDuff; | import android.graphics.PorterDuff; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import androidx.annotation.NonNull; | import android.preference.PreferenceManager; | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.core.content.ContextCompat; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.Menu; | import android.view.Menu; | ||||||
| import android.view.MenuItem; | import android.view.MenuItem; | ||||||
| @@ -45,6 +44,10 @@ import android.widget.ProgressBar; | |||||||
| import android.widget.SeekBar; | import android.widget.SeekBar; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  |  | ||||||
| import com.google.android.exoplayer2.C; | import com.google.android.exoplayer2.C; | ||||||
| import com.google.android.exoplayer2.PlaybackParameters; | import com.google.android.exoplayer2.PlaybackParameters; | ||||||
| import com.google.android.exoplayer2.Player; | import com.google.android.exoplayer2.Player; | ||||||
| @@ -286,6 +289,17 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         if (captionPopupMenu == null) return; |         if (captionPopupMenu == null) return; | ||||||
|         captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); |         captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); | ||||||
|  |  | ||||||
|  |         String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context) | ||||||
|  |                 .getString(context.getString(R.string.caption_user_set_key), null); | ||||||
|  |         /* | ||||||
|  |          * only search for autogenerated cc as fallback | ||||||
|  |          * if "(auto-generated)" was not already selected | ||||||
|  |          * we are only looking for "(" instead of "(auto-generated)" to hopefully get all | ||||||
|  |          * internationalized variants such as "(automatisch-erzeugt)" and so on | ||||||
|  |          */ | ||||||
|  |         boolean searchForAutogenerated = userPreferredLanguage != null && | ||||||
|  |                 !userPreferredLanguage.contains("("); | ||||||
|  |  | ||||||
|         // Add option for turning off caption |         // Add option for turning off caption | ||||||
|         MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, |         MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, | ||||||
|                 0, Menu.NONE, R.string.caption_none); |                 0, Menu.NONE, R.string.caption_none); | ||||||
| @@ -295,6 +309,8 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|                 trackSelector.setParameters(trackSelector.buildUponParameters() |                 trackSelector.setParameters(trackSelector.buildUponParameters() | ||||||
|                         .setRendererDisabled(textRendererIndex, true)); |                         .setRendererDisabled(textRendererIndex, true)); | ||||||
|             } |             } | ||||||
|  |             final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |             prefs.edit().remove(context.getString(R.string.caption_user_set_key)).commit(); | ||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
| @@ -309,9 +325,26 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|                     trackSelector.setPreferredTextLanguage(captionLanguage); |                     trackSelector.setPreferredTextLanguage(captionLanguage); | ||||||
|                     trackSelector.setParameters(trackSelector.buildUponParameters() |                     trackSelector.setParameters(trackSelector.buildUponParameters() | ||||||
|                             .setRendererDisabled(textRendererIndex, false)); |                             .setRendererDisabled(textRendererIndex, false)); | ||||||
|  |                     final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |                     prefs.edit().putString(context.getString(R.string.caption_user_set_key), | ||||||
|  |                             captionLanguage).commit(); | ||||||
|                 } |                 } | ||||||
|                 return true; |                 return true; | ||||||
|             }); |             }); | ||||||
|  |             // apply caption language from previous user preference | ||||||
|  |             if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) || | ||||||
|  |                     searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) || | ||||||
|  |                     userPreferredLanguage.contains("(") && | ||||||
|  |                             captionLanguage.startsWith(userPreferredLanguage.substring(0, | ||||||
|  |                                     userPreferredLanguage.indexOf('('))))) { | ||||||
|  |                 final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); | ||||||
|  |                 if (textRendererIndex != RENDERER_UNAVAILABLE) { | ||||||
|  |                     trackSelector.setPreferredTextLanguage(captionLanguage); | ||||||
|  |                     trackSelector.setParameters(trackSelector.buildUponParameters() | ||||||
|  |                             .setRendererDisabled(textRendererIndex, false)); | ||||||
|  |                 } | ||||||
|  |                 searchForAutogenerated = false; | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         captionPopupMenu.setOnDismissListener(this); |         captionPopupMenu.setOnDismissListener(this); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,417 @@ | |||||||
|  | package org.schabi.newpipe.settings; | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.Menu; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.view.MenuItem; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.EditText; | ||||||
|  | import android.widget.ImageView; | ||||||
|  | import android.widget.ProgressBar; | ||||||
|  | import android.widget.RadioButton; | ||||||
|  | import android.widget.TextView; | ||||||
|  | import android.widget.Toast; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.appcompat.app.ActionBar; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.appcompat.app.AppCompatActivity; | ||||||
|  | import androidx.appcompat.content.res.AppCompatResources; | ||||||
|  | import androidx.appcompat.widget.AppCompatImageView; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.recyclerview.widget.ItemTouchHelper; | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  |  | ||||||
|  | import com.google.android.material.floatingactionbutton.FloatingActionButton; | ||||||
|  | import com.grack.nanojson.JsonStringWriter; | ||||||
|  | import com.grack.nanojson.JsonWriter; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; | ||||||
|  | import org.schabi.newpipe.util.Constants; | ||||||
|  | import org.schabi.newpipe.util.PeertubeHelper; | ||||||
|  | import org.schabi.newpipe.util.ThemeHelper; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import io.reactivex.Single; | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
|  | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | import io.reactivex.disposables.Disposable; | ||||||
|  | import io.reactivex.schedulers.Schedulers; | ||||||
|  |  | ||||||
|  | public class PeertubeInstanceListFragment extends Fragment { | ||||||
|  |  | ||||||
|  |     private List<PeertubeInstance> instanceList = new ArrayList<>(); | ||||||
|  |     private PeertubeInstance selectedInstance; | ||||||
|  |     private String savedInstanceListKey; | ||||||
|  |     public InstanceListAdapter instanceListAdapter; | ||||||
|  |  | ||||||
|  |     private ProgressBar progressBar; | ||||||
|  |     private SharedPreferences sharedPreferences; | ||||||
|  |  | ||||||
|  |     private CompositeDisposable disposables = new CompositeDisposable(); | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Lifecycle | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |  | ||||||
|  |         sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); | ||||||
|  |         savedInstanceListKey = getString(R.string.peertube_instance_list_key); | ||||||
|  |         selectedInstance = PeertubeHelper.getCurrentInstance(); | ||||||
|  |         updateInstanceList(); | ||||||
|  |  | ||||||
|  |         setHasOptionsMenu(true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||||
|  |         return inflater.inflate(R.layout.fragment_instance_list, container, false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) { | ||||||
|  |         super.onViewCreated(rootView, savedInstanceState); | ||||||
|  |  | ||||||
|  |         initButton(rootView); | ||||||
|  |  | ||||||
|  |         RecyclerView listInstances = rootView.findViewById(R.id.instances); | ||||||
|  |         listInstances.setLayoutManager(new LinearLayoutManager(requireContext())); | ||||||
|  |  | ||||||
|  |         ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); | ||||||
|  |         itemTouchHelper.attachToRecyclerView(listInstances); | ||||||
|  |  | ||||||
|  |         instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper); | ||||||
|  |         listInstances.setAdapter(instanceListAdapter); | ||||||
|  |  | ||||||
|  |         progressBar = rootView.findViewById(R.id.loading_progress_bar); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  |         updateTitle(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPause() { | ||||||
|  |         super.onPause(); | ||||||
|  |         saveChanges(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onDestroy() { | ||||||
|  |         super.onDestroy(); | ||||||
|  |         if (disposables != null) disposables.clear(); | ||||||
|  |         disposables = null; | ||||||
|  |     } | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Menu | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private final int MENU_ITEM_RESTORE_ID = 123456; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||||
|  |         super.onCreateOptionsMenu(menu, inflater); | ||||||
|  |  | ||||||
|  |         final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults); | ||||||
|  |         restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); | ||||||
|  |  | ||||||
|  |         final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults); | ||||||
|  |         restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|  |         if (item.getItemId() == MENU_ITEM_RESTORE_ID) { | ||||||
|  |             restoreDefaults(); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return super.onOptionsItemSelected(item); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Utils | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private void updateInstanceList() { | ||||||
|  |         instanceList.clear(); | ||||||
|  |         instanceList.addAll(PeertubeHelper.getInstanceList(requireContext())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void selectInstance(PeertubeInstance instance) { | ||||||
|  |         selectedInstance = PeertubeHelper.selectInstance(instance, requireContext()); | ||||||
|  |         sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void updateTitle() { | ||||||
|  |         if (getActivity() instanceof AppCompatActivity) { | ||||||
|  |             ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); | ||||||
|  |             if (actionBar != null) actionBar.setTitle(R.string.peertube_instance_url_title); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void saveChanges() { | ||||||
|  |         JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); | ||||||
|  |         for (PeertubeInstance instance : instanceList) { | ||||||
|  |             jsonWriter.object(); | ||||||
|  |             jsonWriter.value("name", instance.getName()); | ||||||
|  |             jsonWriter.value("url", instance.getUrl()); | ||||||
|  |             jsonWriter.end(); | ||||||
|  |         } | ||||||
|  |         String jsonToSave = jsonWriter.end().end().done(); | ||||||
|  |         sharedPreferences.edit().putString(savedInstanceListKey, jsonToSave).apply(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void restoreDefaults() { | ||||||
|  |         new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext())) | ||||||
|  |                 .setTitle(R.string.restore_defaults) | ||||||
|  |                 .setMessage(R.string.restore_defaults_confirmation) | ||||||
|  |                 .setNegativeButton(R.string.cancel, null) | ||||||
|  |                 .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||||
|  |                     sharedPreferences.edit().remove(savedInstanceListKey).apply(); | ||||||
|  |                     selectInstance(PeertubeInstance.defaultInstance); | ||||||
|  |                     updateInstanceList(); | ||||||
|  |                     instanceListAdapter.notifyDataSetChanged(); | ||||||
|  |                 }) | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void initButton(View rootView) { | ||||||
|  |         final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton); | ||||||
|  |         fab.setOnClickListener(v -> { | ||||||
|  |             showAddItemDialog(requireContext()); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void showAddItemDialog(Context c) { | ||||||
|  |         final EditText urlET = new EditText(c); | ||||||
|  |         urlET.setHint(R.string.peertube_instance_add_help); | ||||||
|  |         AlertDialog dialog = new AlertDialog.Builder(c) | ||||||
|  |                 .setTitle(R.string.peertube_instance_add_title) | ||||||
|  |                 .setIcon(R.drawable.place_holder_peertube) | ||||||
|  |                 .setView(urlET) | ||||||
|  |                 .setNegativeButton(R.string.cancel, null) | ||||||
|  |                 .setPositiveButton(R.string.finish, (dialog1, which) -> { | ||||||
|  |                     String url = urlET.getText().toString(); | ||||||
|  |                     addInstance(url); | ||||||
|  |                 }) | ||||||
|  |                 .create(); | ||||||
|  |         dialog.show(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void addInstance(String url) { | ||||||
|  |         String cleanUrl = cleanUrl(url); | ||||||
|  |         if(null == cleanUrl) return; | ||||||
|  |         progressBar.setVisibility(View.VISIBLE); | ||||||
|  |         Disposable disposable = Single.fromCallable(() -> { | ||||||
|  |             PeertubeInstance instance = new PeertubeInstance(cleanUrl); | ||||||
|  |             instance.fetchInstanceMetaData(); | ||||||
|  |             return instance; | ||||||
|  |         }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe((instance) -> { | ||||||
|  |             progressBar.setVisibility(View.GONE); | ||||||
|  |             add(instance); | ||||||
|  |         }, e -> { | ||||||
|  |             progressBar.setVisibility(View.GONE); | ||||||
|  |             Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show(); | ||||||
|  |         }); | ||||||
|  |         disposables.add(disposable); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     private String cleanUrl(String url){ | ||||||
|  |         // if protocol not present, add https | ||||||
|  |         if(!url.startsWith("http")){ | ||||||
|  |             url = "https://" + url; | ||||||
|  |         } | ||||||
|  |         // remove trailing slash | ||||||
|  |         url = url.replaceAll("/$", ""); | ||||||
|  |         // only allow https | ||||||
|  |         if (!url.startsWith("https://")) { | ||||||
|  |             Toast.makeText(getActivity(), R.string.peertube_instance_add_https_only, Toast.LENGTH_SHORT).show(); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         // only allow if not already exists | ||||||
|  |         for (PeertubeInstance instance : instanceList) { | ||||||
|  |             if (instance.getUrl().equals(url)) { | ||||||
|  |                 Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show(); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return url; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void add(final PeertubeInstance instance) { | ||||||
|  |         instanceList.add(instance); | ||||||
|  |         instanceListAdapter.notifyDataSetChanged(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // List Handling | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private class InstanceListAdapter extends RecyclerView.Adapter<InstanceListAdapter.TabViewHolder> { | ||||||
|  |         private ItemTouchHelper itemTouchHelper; | ||||||
|  |         private final LayoutInflater inflater; | ||||||
|  |         private RadioButton lastChecked; | ||||||
|  |  | ||||||
|  |         InstanceListAdapter(Context context, ItemTouchHelper itemTouchHelper) { | ||||||
|  |             this.itemTouchHelper = itemTouchHelper; | ||||||
|  |             this.inflater = LayoutInflater.from(context); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void swapItems(int fromPosition, int toPosition) { | ||||||
|  |             Collections.swap(instanceList, fromPosition, toPosition); | ||||||
|  |             notifyItemMoved(fromPosition, toPosition); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { | ||||||
|  |             View view = inflater.inflate(R.layout.item_instance, parent, false); | ||||||
|  |             return new InstanceListAdapter.TabViewHolder(view); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public void onBindViewHolder(@NonNull InstanceListAdapter.TabViewHolder holder, int position) { | ||||||
|  |             holder.bind(position, holder); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public int getItemCount() { | ||||||
|  |             return instanceList.size(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         class TabViewHolder extends RecyclerView.ViewHolder { | ||||||
|  |             private AppCompatImageView instanceIconView; | ||||||
|  |             private TextView instanceNameView; | ||||||
|  |             private TextView instanceUrlView; | ||||||
|  |             private RadioButton instanceRB; | ||||||
|  |             private ImageView handle; | ||||||
|  |  | ||||||
|  |             TabViewHolder(View itemView) { | ||||||
|  |                 super(itemView); | ||||||
|  |  | ||||||
|  |                 instanceIconView = itemView.findViewById(R.id.instanceIcon); | ||||||
|  |                 instanceNameView = itemView.findViewById(R.id.instanceName); | ||||||
|  |                 instanceUrlView = itemView.findViewById(R.id.instanceUrl); | ||||||
|  |                 instanceRB = itemView.findViewById(R.id.selectInstanceRB); | ||||||
|  |                 handle = itemView.findViewById(R.id.handle); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @SuppressLint("ClickableViewAccessibility") | ||||||
|  |             void bind(int position, TabViewHolder holder) { | ||||||
|  |                 handle.setOnTouchListener(getOnTouchListener(holder)); | ||||||
|  |  | ||||||
|  |                 final PeertubeInstance instance = instanceList.get(position); | ||||||
|  |                 instanceNameView.setText(instance.getName()); | ||||||
|  |                 instanceUrlView.setText(instance.getUrl()); | ||||||
|  |                 instanceRB.setOnCheckedChangeListener(null); | ||||||
|  |                 if (selectedInstance.getUrl().equals(instance.getUrl())) { | ||||||
|  |                     if (lastChecked != null && lastChecked != instanceRB) { | ||||||
|  |                         lastChecked.setChecked(false); | ||||||
|  |                     } | ||||||
|  |                     instanceRB.setChecked(true); | ||||||
|  |                     lastChecked = instanceRB; | ||||||
|  |                 } | ||||||
|  |                 instanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||||
|  |                     if (isChecked) { | ||||||
|  |                         selectInstance(instance); | ||||||
|  |                         if (lastChecked != null && lastChecked != instanceRB) { | ||||||
|  |                             lastChecked.setChecked(false); | ||||||
|  |                         } | ||||||
|  |                         lastChecked = instanceRB; | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |                 instanceIconView.setImageResource(R.drawable.place_holder_peertube); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @SuppressLint("ClickableViewAccessibility") | ||||||
|  |             private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) { | ||||||
|  |                 return (view, motionEvent) -> { | ||||||
|  |                     if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { | ||||||
|  |                         if (itemTouchHelper != null && getItemCount() > 1) { | ||||||
|  |                             itemTouchHelper.startDrag(item); | ||||||
|  |                             return true; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     return false; | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||||
|  |         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, | ||||||
|  |                 ItemTouchHelper.START | ItemTouchHelper.END) { | ||||||
|  |             @Override | ||||||
|  |             public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, | ||||||
|  |                                                     int viewSizeOutOfBounds, int totalSize, | ||||||
|  |                                                     long msSinceStartScroll) { | ||||||
|  |                 final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, | ||||||
|  |                         viewSizeOutOfBounds, totalSize, msSinceStartScroll); | ||||||
|  |                 final int minimumAbsVelocity = Math.max(12, | ||||||
|  |                         Math.abs(standardSpeed)); | ||||||
|  |                 return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, | ||||||
|  |                                   RecyclerView.ViewHolder target) { | ||||||
|  |                 if (source.getItemViewType() != target.getItemViewType() || | ||||||
|  |                         instanceListAdapter == null) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 final int sourceIndex = source.getAdapterPosition(); | ||||||
|  |                 final int targetIndex = target.getAdapterPosition(); | ||||||
|  |                 instanceListAdapter.swapItems(sourceIndex, targetIndex); | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public boolean isLongPressDragEnabled() { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public boolean isItemViewSwipeEnabled() { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { | ||||||
|  |                 int position = viewHolder.getAdapterPosition(); | ||||||
|  |                 // do not allow swiping the selected instance | ||||||
|  |                 if(instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) { | ||||||
|  |                     instanceListAdapter.notifyItemChanged(position); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 instanceList.remove(position); | ||||||
|  |                 instanceListAdapter.notifyItemRemoved(position); | ||||||
|  |  | ||||||
|  |                 if (instanceList.isEmpty()) { | ||||||
|  |                     instanceList.add(selectedInstance); | ||||||
|  |                     instanceListAdapter.notifyItemInserted(0); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -15,7 +15,6 @@ import java.util.NoSuchElementException; | |||||||
|  */ |  */ | ||||||
| public class Mp4DashReader { | public class Mp4DashReader { | ||||||
|  |  | ||||||
|     // <editor-fold defaultState="collapsed" desc="Constants"> |  | ||||||
|     private static final int ATOM_MOOF = 0x6D6F6F66; |     private static final int ATOM_MOOF = 0x6D6F6F66; | ||||||
|     private static final int ATOM_MFHD = 0x6D666864; |     private static final int ATOM_MFHD = 0x6D666864; | ||||||
|     private static final int ATOM_TRAF = 0x74726166; |     private static final int ATOM_TRAF = 0x74726166; | ||||||
| @@ -50,7 +49,7 @@ public class Mp4DashReader { | |||||||
|     private static final int HANDLER_VIDE = 0x76696465; |     private static final int HANDLER_VIDE = 0x76696465; | ||||||
|     private static final int HANDLER_SOUN = 0x736F756E; |     private static final int HANDLER_SOUN = 0x736F756E; | ||||||
|     private static final int HANDLER_SUBT = 0x73756274; |     private static final int HANDLER_SUBT = 0x73756274; | ||||||
|     // </editor-fold> |  | ||||||
|  |  | ||||||
|     private final DataReader stream; |     private final DataReader stream; | ||||||
|  |  | ||||||
| @@ -293,7 +292,8 @@ public class Mp4DashReader { | |||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // <editor-fold defaultState="collapsed" desc="Utils"> |  | ||||||
|  |  | ||||||
|     private long readUint() throws IOException { |     private long readUint() throws IOException { | ||||||
|         return stream.readInt() & 0xffffffffL; |         return stream.readInt() & 0xffffffffL; | ||||||
|     } |     } | ||||||
| @@ -392,9 +392,7 @@ public class Mp4DashReader { | |||||||
|         return readBox(); |         return readBox(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // </editor-fold> |  | ||||||
|  |  | ||||||
|     // <editor-fold defaultState="collapsed" desc="Box readers"> |  | ||||||
|  |  | ||||||
|     private Moof parse_moof(Box ref, int trackId) throws IOException { |     private Moof parse_moof(Box ref, int trackId) throws IOException { | ||||||
|         Moof obj = new Moof(); |         Moof obj = new Moof(); | ||||||
| @@ -795,9 +793,8 @@ public class Mp4DashReader { | |||||||
|         return readFullBox(b); |         return readFullBox(b); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // </editor-fold> |  | ||||||
|  |  | ||||||
|     // <editor-fold defaultState="collapsed" desc="Helper classes"> |  | ||||||
|     class Box { |     class Box { | ||||||
|  |  | ||||||
|         int type; |         int type; | ||||||
| @@ -1013,5 +1010,5 @@ public class Mp4DashReader { | |||||||
|         public TrunEntry info; |         public TrunEntry info; | ||||||
|         public byte[] data; |         public byte[] data; | ||||||
|     } |     } | ||||||
| //</editor-fold> |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk; | |||||||
| import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample; | import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample; | ||||||
| import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track; | import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track; | ||||||
| import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry; | import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry; | ||||||
|  | import org.schabi.newpipe.streams.Mp4DashReader.TrackKind; | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| @@ -22,6 +23,7 @@ public class Mp4FromDashWriter { | |||||||
|     private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 |     private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 | ||||||
|     private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB |     private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB | ||||||
|     private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s |     private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s | ||||||
|  |     private final static short SINGLE_CHUNK_SAMPLE_BUFFER = 256; | ||||||
|  |  | ||||||
|     private final long time; |     private final long time; | ||||||
|  |  | ||||||
| @@ -145,7 +147,7 @@ public class Mp4FromDashWriter { | |||||||
|         //          not allowed for very short tracks (less than 0.5 seconds) |         //          not allowed for very short tracks (less than 0.5 seconds) | ||||||
|         // |         // | ||||||
|         outStream = output; |         outStream = output; | ||||||
|         int read = 8;// mdat box header size |         long read = 8;// mdat box header size | ||||||
|         long totalSampleSize = 0; |         long totalSampleSize = 0; | ||||||
|         int[] sampleExtra = new int[readers.length]; |         int[] sampleExtra = new int[readers.length]; | ||||||
|         int[] defaultMediaTime = new int[readers.length]; |         int[] defaultMediaTime = new int[readers.length]; | ||||||
| @@ -157,7 +159,9 @@ public class Mp4FromDashWriter { | |||||||
|             tablesInfo[i] = new TablesInfo(); |             tablesInfo[i] = new TablesInfo(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         //<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values"> |         boolean singleChunk = tracks.length == 1 && tracks[0].kind == TrackKind.Audio; | ||||||
|  |  | ||||||
|  |  | ||||||
|         for (int i = 0; i < readers.length; i++) { |         for (int i = 0; i < readers.length; i++) { | ||||||
|             int samplesSize = 0; |             int samplesSize = 0; | ||||||
|             int sampleSizeChanges = 0; |             int sampleSizeChanges = 0; | ||||||
| @@ -210,14 +214,21 @@ public class Mp4FromDashWriter { | |||||||
|             tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk |             tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk | ||||||
|  |  | ||||||
|             tmp = tmp % SAMPLES_PER_CHUNK; |             tmp = tmp % SAMPLES_PER_CHUNK; | ||||||
|             if (tmp == 0) { |             if (singleChunk) { | ||||||
|  |                 // avoid split audio streams in chunks | ||||||
|  |                 tablesInfo[i].stsc = 1; | ||||||
|  |                 tablesInfo[i].stsc_bEntries = new int[]{ | ||||||
|  |                         1, tablesInfo[i].stsz, 1 | ||||||
|  |                 }; | ||||||
|  |                 tablesInfo[i].stco = 1; | ||||||
|  |             } else if (tmp == 0) { | ||||||
|                 tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks |                 tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks | ||||||
|                 tablesInfo[i].stsc_bEntries = new int[]{ |                 tablesInfo[i].stsc_bEntries = new int[]{ | ||||||
|                         1, SAMPLES_PER_CHUNK_INIT, 1, |                         1, SAMPLES_PER_CHUNK_INIT, 1, | ||||||
|                         2, SAMPLES_PER_CHUNK, 1 |                         2, SAMPLES_PER_CHUNK, 1 | ||||||
|                 }; |                 }; | ||||||
|             } else { |             } else { | ||||||
|                 tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk |                 tablesInfo[i].stsc = 3;// first chunk (init) and successive chunks and remain chunk | ||||||
|                 tablesInfo[i].stsc_bEntries = new int[]{ |                 tablesInfo[i].stsc_bEntries = new int[]{ | ||||||
|                         1, SAMPLES_PER_CHUNK_INIT, 1, |                         1, SAMPLES_PER_CHUNK_INIT, 1, | ||||||
|                         2, SAMPLES_PER_CHUNK, 1, |                         2, SAMPLES_PER_CHUNK, 1, | ||||||
| @@ -244,7 +255,7 @@ public class Mp4FromDashWriter { | |||||||
|                 tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen |                 tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         //</editor-fold> |  | ||||||
|  |  | ||||||
|         boolean is64 = read > THRESHOLD_FOR_CO64; |         boolean is64 = read > THRESHOLD_FOR_CO64; | ||||||
|  |  | ||||||
| @@ -268,10 +279,10 @@ public class Mp4FromDashWriter { | |||||||
|         } else {*/ |         } else {*/ | ||||||
|         if (auxSize > 0) { |         if (auxSize > 0) { | ||||||
|             int length = auxSize; |             int length = auxSize; | ||||||
|             byte[] buffer = new byte[8 * 1024];// 8 KiB |             byte[] buffer = new byte[64 * 1024];// 64 KiB | ||||||
|             while (length > 0) { |             while (length > 0) { | ||||||
|                 int count = Math.min(length, buffer.length); |                 int count = Math.min(length, buffer.length); | ||||||
|                 outWrite(buffer, 0, count); |                 outWrite(buffer, count); | ||||||
|                 length -= count; |                 length -= count; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -280,7 +291,7 @@ public class Mp4FromDashWriter { | |||||||
|             outSeek(ftyp_size); |             outSeek(ftyp_size); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // tablesInfo contais row counts |         // tablesInfo contains row counts | ||||||
|         // and after returning from make_moov() will contain table offsets |         // and after returning from make_moov() will contain table offsets | ||||||
|         make_moov(defaultMediaTime, tablesInfo, is64); |         make_moov(defaultMediaTime, tablesInfo, is64); | ||||||
|  |  | ||||||
| @@ -291,7 +302,7 @@ public class Mp4FromDashWriter { | |||||||
|             writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries); |             writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries); | ||||||
|             tablesInfo[i].stsc_bEntries = null; |             tablesInfo[i].stsc_bEntries = null; | ||||||
|             if (tablesInfo[i].ctts > 0) { |             if (tablesInfo[i].ctts > 0) { | ||||||
|                 sampleCount[i] = 1;// index is not base zero |                 sampleCount[i] = 1;// the index is not base zero | ||||||
|                 sampleExtra[i] = -1; |                 sampleExtra[i] = -1; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -303,8 +314,8 @@ public class Mp4FromDashWriter { | |||||||
|         outWrite(make_mdat(totalSampleSize, is64)); |         outWrite(make_mdat(totalSampleSize, is64)); | ||||||
|  |  | ||||||
|         int[] sampleIndex = new int[readers.length]; |         int[] sampleIndex = new int[readers.length]; | ||||||
|         int[] sizes = new int[SAMPLES_PER_CHUNK]; |         int[] sizes = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK]; | ||||||
|         int[] sync = new int[SAMPLES_PER_CHUNK]; |         int[] sync = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK]; | ||||||
|  |  | ||||||
|         int written = readers.length; |         int written = readers.length; | ||||||
|         while (written > 0) { |         while (written > 0) { | ||||||
| @@ -317,7 +328,12 @@ public class Mp4FromDashWriter { | |||||||
|  |  | ||||||
|                 long chunkOffset = writeOffset; |                 long chunkOffset = writeOffset; | ||||||
|                 int syncCount = 0; |                 int syncCount = 0; | ||||||
|                 int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK; |                 int limit; | ||||||
|  |                 if (singleChunk) { | ||||||
|  |                     limit = SINGLE_CHUNK_SAMPLE_BUFFER; | ||||||
|  |                 } else { | ||||||
|  |                     limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 int j = 0; |                 int j = 0; | ||||||
|                 for (; j < limit; j++) { |                 for (; j < limit; j++) { | ||||||
| @@ -354,7 +370,7 @@ public class Mp4FromDashWriter { | |||||||
|                         sizes[j] = sample.data.length; |                         sizes[j] = sample.data.length; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     outWrite(sample.data, 0, sample.data.length); |                     outWrite(sample.data, sample.data.length); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 if (j > 0) { |                 if (j > 0) { | ||||||
| @@ -368,10 +384,16 @@ public class Mp4FromDashWriter { | |||||||
|                         tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync); |                         tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync); | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     if (is64) { |                     if (tablesInfo[i].stco > 0) { | ||||||
|                         tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset); |                         if (is64) { | ||||||
|                     } else { |                             tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset); | ||||||
|                         tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset); |                         } else { | ||||||
|  |                             tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         if (singleChunk) { | ||||||
|  |                             tablesInfo[i].stco = -1; | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     outRestore(); |                     outRestore(); | ||||||
| @@ -404,7 +426,7 @@ public class Mp4FromDashWriter { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // <editor-fold defaultstate="expanded" desc="Stbl handling"> |  | ||||||
|     private int writeEntry64(int offset, long value) throws IOException { |     private int writeEntry64(int offset, long value) throws IOException { | ||||||
|         outBackup(); |         outBackup(); | ||||||
|  |  | ||||||
| @@ -447,16 +469,16 @@ public class Mp4FromDashWriter { | |||||||
|             lastWriteOffset = -1; |             lastWriteOffset = -1; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     // </editor-fold> |  | ||||||
|  |  | ||||||
|     // <editor-fold defaultstate="expanded" desc="Utils"> |  | ||||||
|  |  | ||||||
|     private void outWrite(byte[] buffer) throws IOException { |     private void outWrite(byte[] buffer) throws IOException { | ||||||
|         outWrite(buffer, 0, buffer.length); |         outWrite(buffer, buffer.length); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void outWrite(byte[] buffer, int offset, int count) throws IOException { |     private void outWrite(byte[] buffer, int count) throws IOException { | ||||||
|         writeOffset += count; |         writeOffset += count; | ||||||
|         outStream.write(buffer, offset, count); |         outStream.write(buffer, 0, count); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void outSeek(long offset) throws IOException { |     private void outSeek(long offset) throws IOException { | ||||||
| @@ -509,7 +531,6 @@ public class Mp4FromDashWriter { | |||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         if (extra >= 0) { |         if (extra >= 0) { | ||||||
|             //size += 4;// commented for auxiliar buffer !!! |  | ||||||
|             offset += 4; |             offset += 4; | ||||||
|             auxWrite(extra); |             auxWrite(extra); | ||||||
|         } |         } | ||||||
| @@ -531,7 +552,7 @@ public class Mp4FromDashWriter { | |||||||
|         if (moovSimulation) { |         if (moovSimulation) { | ||||||
|             writeOffset += buffer.length; |             writeOffset += buffer.length; | ||||||
|         } else if (auxBuffer == null) { |         } else if (auxBuffer == null) { | ||||||
|             outWrite(buffer, 0, buffer.length); |             outWrite(buffer, buffer.length); | ||||||
|         } else { |         } else { | ||||||
|             auxBuffer.put(buffer); |             auxBuffer.put(buffer); | ||||||
|         } |         } | ||||||
| @@ -560,9 +581,9 @@ public class Mp4FromDashWriter { | |||||||
|     private int auxOffset() { |     private int auxOffset() { | ||||||
|         return auxBuffer == null ? (int) writeOffset : auxBuffer.position(); |         return auxBuffer == null ? (int) writeOffset : auxBuffer.position(); | ||||||
|     } |     } | ||||||
|     // </editor-fold> |  | ||||||
|  |  | ||||||
|     // <editor-fold defaultstate="expanded" desc="Box makers"> |  | ||||||
|  |  | ||||||
|     private int make_ftyp() throws IOException { |     private int make_ftyp() throws IOException { | ||||||
|         byte[] buffer = new byte[]{ |         byte[] buffer = new byte[]{ | ||||||
|                 0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp |                 0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp | ||||||
| @@ -703,7 +724,7 @@ public class Mp4FromDashWriter { | |||||||
|         int mediaTime; |         int mediaTime; | ||||||
|  |  | ||||||
|         if (tracks[index].trak.edst_elst == null) { |         if (tracks[index].trak.edst_elst == null) { | ||||||
|             // is a audio track ¿is edst/elst opcional for audio tracks? |             // is a audio track ¿is edst/elst optional for audio tracks? | ||||||
|             mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime |             mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime | ||||||
|             bMediaRate = 0x00010000; |             bMediaRate = 0x00010000; | ||||||
|         } else { |         } else { | ||||||
| @@ -794,17 +815,17 @@ public class Mp4FromDashWriter { | |||||||
|  |  | ||||||
|         return buffer.array(); |         return buffer.array(); | ||||||
|     } |     } | ||||||
|     //</editor-fold> |  | ||||||
|  |  | ||||||
|     class TablesInfo { |     class TablesInfo { | ||||||
|  |  | ||||||
|         public int stts; |         int stts; | ||||||
|         public int stsc; |         int stsc; | ||||||
|         public int[] stsc_bEntries; |         int[] stsc_bEntries; | ||||||
|         public int ctts; |         int ctts; | ||||||
|         public int stsz; |         int stsz; | ||||||
|         public int stsz_default; |         int stsz_default; | ||||||
|         public int stss; |         int stss; | ||||||
|         public int stco; |         int stco; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,431 @@ | |||||||
|  | package org.schabi.newpipe.streams; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.streams.WebMReader.Cluster; | ||||||
|  | import org.schabi.newpipe.streams.WebMReader.Segment; | ||||||
|  | import org.schabi.newpipe.streams.WebMReader.SimpleBlock; | ||||||
|  | import org.schabi.newpipe.streams.WebMReader.WebMTrack; | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
|  | import java.io.Closeable; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.nio.ByteBuffer; | ||||||
|  | import java.nio.ByteOrder; | ||||||
|  |  | ||||||
|  | import javax.annotation.Nullable; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author kapodamy | ||||||
|  |  */ | ||||||
|  | public class OggFromWebMWriter implements Closeable { | ||||||
|  |  | ||||||
|  |     private static final byte FLAG_UNSET = 0x00; | ||||||
|  |     //private static final byte FLAG_CONTINUED = 0x01; | ||||||
|  |     private static final byte FLAG_FIRST = 0x02; | ||||||
|  |     private static final byte FLAG_LAST = 0x04; | ||||||
|  |  | ||||||
|  |     private final static byte HEADER_CHECKSUM_OFFSET = 22; | ||||||
|  |     private final static byte HEADER_SIZE = 27; | ||||||
|  |  | ||||||
|  |     private final static int TIME_SCALE_NS = 1000000000; | ||||||
|  |  | ||||||
|  |     private boolean done = false; | ||||||
|  |     private boolean parsed = false; | ||||||
|  |  | ||||||
|  |     private SharpStream source; | ||||||
|  |     private SharpStream output; | ||||||
|  |  | ||||||
|  |     private int sequence_count = 0; | ||||||
|  |     private final int STREAM_ID; | ||||||
|  |     private byte packet_flag = FLAG_FIRST; | ||||||
|  |  | ||||||
|  |     private WebMReader webm = null; | ||||||
|  |     private WebMTrack webm_track = null; | ||||||
|  |     private Segment webm_segment = null; | ||||||
|  |     private Cluster webm_cluster = null; | ||||||
|  |     private SimpleBlock webm_block = null; | ||||||
|  |  | ||||||
|  |     private long webm_block_last_timecode = 0; | ||||||
|  |     private long webm_block_near_duration = 0; | ||||||
|  |  | ||||||
|  |     private short segment_table_size = 0; | ||||||
|  |     private final byte[] segment_table = new byte[255]; | ||||||
|  |     private long segment_table_next_timestamp = TIME_SCALE_NS; | ||||||
|  |  | ||||||
|  |     private final int[] crc32_table = new int[256]; | ||||||
|  |  | ||||||
|  |     public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) { | ||||||
|  |         if (!source.canRead() || !source.canRewind()) { | ||||||
|  |             throw new IllegalArgumentException("source stream must be readable and allows seeking"); | ||||||
|  |         } | ||||||
|  |         if (!target.canWrite() || !target.canRewind()) { | ||||||
|  |             throw new IllegalArgumentException("output stream must be writable and allows seeking"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.source = source; | ||||||
|  |         this.output = target; | ||||||
|  |  | ||||||
|  |         this.STREAM_ID = (int) System.currentTimeMillis(); | ||||||
|  |  | ||||||
|  |         populate_crc32_table(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean isDone() { | ||||||
|  |         return done; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean isParsed() { | ||||||
|  |         return parsed; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public WebMTrack[] getTracksFromSource() throws IllegalStateException { | ||||||
|  |         if (!parsed) { | ||||||
|  |             throw new IllegalStateException("source must be parsed first"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return webm.getAvailableTracks(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void parseSource() throws IOException, IllegalStateException { | ||||||
|  |         if (done) { | ||||||
|  |             throw new IllegalStateException("already done"); | ||||||
|  |         } | ||||||
|  |         if (parsed) { | ||||||
|  |             throw new IllegalStateException("already parsed"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             webm = new WebMReader(source); | ||||||
|  |             webm.parse(); | ||||||
|  |             webm_segment = webm.getNextSegment(); | ||||||
|  |         } finally { | ||||||
|  |             parsed = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void selectTrack(int trackIndex) throws IOException { | ||||||
|  |         if (!parsed) { | ||||||
|  |             throw new IllegalStateException("source must be parsed first"); | ||||||
|  |         } | ||||||
|  |         if (done) { | ||||||
|  |             throw new IOException("already done"); | ||||||
|  |         } | ||||||
|  |         if (webm_track != null) { | ||||||
|  |             throw new IOException("tracks already selected"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         switch (webm.getAvailableTracks()[trackIndex].kind) { | ||||||
|  |             case Audio: | ||||||
|  |             case Video: | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new UnsupportedOperationException("the track must an audio or video stream"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             webm_track = webm.selectTrack(trackIndex); | ||||||
|  |         } finally { | ||||||
|  |             parsed = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void close() throws IOException { | ||||||
|  |         done = true; | ||||||
|  |         parsed = true; | ||||||
|  |  | ||||||
|  |         webm_track = null; | ||||||
|  |         webm = null; | ||||||
|  |  | ||||||
|  |         if (!output.isClosed()) { | ||||||
|  |             output.flush(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         source.close(); | ||||||
|  |         output.close(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void build() throws IOException { | ||||||
|  |         float resolution; | ||||||
|  |         SimpleBlock bloq; | ||||||
|  |         ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); | ||||||
|  |         ByteBuffer page = ByteBuffer.allocate(64 * 1024); | ||||||
|  |  | ||||||
|  |         header.order(ByteOrder.LITTLE_ENDIAN); | ||||||
|  |  | ||||||
|  |         /* step 1: get the amount of frames per seconds */ | ||||||
|  |         switch (webm_track.kind) { | ||||||
|  |             case Audio: | ||||||
|  |                 resolution = getSampleFrequencyFromTrack(webm_track.bMetadata); | ||||||
|  |                 if (resolution == 0f) { | ||||||
|  |                     throw new RuntimeException("cannot get the audio sample rate"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case Video: | ||||||
|  |                 // WARNING: untested | ||||||
|  |                 if (webm_track.defaultDuration == 0) { | ||||||
|  |                     throw new RuntimeException("missing default frame time"); | ||||||
|  |                 } | ||||||
|  |                 resolution = 1000f / ((float) webm_track.defaultDuration / webm_segment.info.timecodeScale); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new RuntimeException("not implemented"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* step 2: create packet with code init data */ | ||||||
|  |         if (webm_track.codecPrivate != null) { | ||||||
|  |             addPacketSegment(webm_track.codecPrivate.length); | ||||||
|  |             make_packetHeader(0x00, header, webm_track.codecPrivate); | ||||||
|  |             write(header); | ||||||
|  |             output.write(webm_track.codecPrivate); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* step 3: create packet with metadata */ | ||||||
|  |         byte[] buffer = make_metadata(); | ||||||
|  |         if (buffer != null) { | ||||||
|  |             addPacketSegment(buffer.length); | ||||||
|  |             make_packetHeader(0x00, header, buffer); | ||||||
|  |             write(header); | ||||||
|  |             output.write(buffer); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* step 4: calculate amount of packets */ | ||||||
|  |         while (webm_segment != null) { | ||||||
|  |             bloq = getNextBlock(); | ||||||
|  |  | ||||||
|  |             if (bloq != null && addPacketSegment(bloq)) { | ||||||
|  |                 int pos = page.position(); | ||||||
|  |                 //noinspection ResultOfMethodCallIgnored | ||||||
|  |                 bloq.data.read(page.array(), pos, bloq.dataSize); | ||||||
|  |                 page.position(pos + bloq.dataSize); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // calculate the current packet duration using the next block | ||||||
|  |             double elapsed_ns = webm_track.codecDelay; | ||||||
|  |  | ||||||
|  |             if (bloq == null) { | ||||||
|  |                 packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed | ||||||
|  |                 elapsed_ns += webm_block_last_timecode; | ||||||
|  |  | ||||||
|  |                 if (webm_track.defaultDuration > 0) { | ||||||
|  |                     elapsed_ns += webm_track.defaultDuration; | ||||||
|  |                 } else { | ||||||
|  |                     // hardcoded way, guess the sample duration | ||||||
|  |                     elapsed_ns += webm_block_near_duration; | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 elapsed_ns += bloq.absoluteTimeCodeNs; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // get the sample count in the page | ||||||
|  |             elapsed_ns = elapsed_ns / TIME_SCALE_NS; | ||||||
|  |             elapsed_ns = Math.ceil(elapsed_ns * resolution); | ||||||
|  |  | ||||||
|  |             // create header and calculate page checksum | ||||||
|  |             int checksum = make_packetHeader((long) elapsed_ns, header, null); | ||||||
|  |             checksum = calc_crc32(checksum, page.array(), page.position()); | ||||||
|  |  | ||||||
|  |             header.putInt(HEADER_CHECKSUM_OFFSET, checksum); | ||||||
|  |  | ||||||
|  |             // dump data | ||||||
|  |             write(header); | ||||||
|  |             write(page); | ||||||
|  |  | ||||||
|  |             webm_block = bloq; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) { | ||||||
|  |         short length = HEADER_SIZE; | ||||||
|  |  | ||||||
|  |         buffer.putInt(0x5367674f);// "OggS" binary string in little-endian | ||||||
|  |         buffer.put((byte) 0x00);// version | ||||||
|  |         buffer.put(packet_flag);// type | ||||||
|  |  | ||||||
|  |         buffer.putLong(gran_pos);// granulate position | ||||||
|  |  | ||||||
|  |         buffer.putInt(STREAM_ID);// bitstream serial number | ||||||
|  |         buffer.putInt(sequence_count++);// page sequence number | ||||||
|  |  | ||||||
|  |         buffer.putInt(0x00);// page checksum | ||||||
|  |  | ||||||
|  |         buffer.put((byte) segment_table_size);// segment table | ||||||
|  |         buffer.put(segment_table, 0, segment_table_size);// segment size | ||||||
|  |  | ||||||
|  |         length += segment_table_size; | ||||||
|  |  | ||||||
|  |         clearSegmentTable();// clear segment table for next header | ||||||
|  |  | ||||||
|  |         int checksum_crc32 = calc_crc32(0x00, buffer.array(), length); | ||||||
|  |  | ||||||
|  |         if (immediate_page != null) { | ||||||
|  |             checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length); | ||||||
|  |             buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32); | ||||||
|  |             segment_table_next_timestamp -= TIME_SCALE_NS; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return checksum_crc32; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     private byte[] make_metadata() { | ||||||
|  |         if ("A_OPUS".equals(webm_track.codecId)) { | ||||||
|  |             return new byte[]{ | ||||||
|  |                     0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73,// "OpusTags" binary string | ||||||
|  |                     0x07, 0x00, 0x00, 0x00,// writting application string size | ||||||
|  |                     0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string | ||||||
|  |                     0x00, 0x00, 0x00, 0x00// additional tags count (zero means no tags) | ||||||
|  |             }; | ||||||
|  |         } else if ("A_VORBIS".equals(webm_track.codecId)) { | ||||||
|  |             return new byte[]{ | ||||||
|  |                     0x03,// ???????? | ||||||
|  |                     0x76, 0x6f, 0x72, 0x62, 0x69, 0x73,// "vorbis" binary string | ||||||
|  |                     0x07, 0x00, 0x00, 0x00,// writting application string size | ||||||
|  |                     0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string | ||||||
|  |                     0x01, 0x00, 0x00, 0x00,// additional tags count (zero means no tags) | ||||||
|  |  | ||||||
|  |                     /* | ||||||
|  |                         // whole file duration (not implemented) | ||||||
|  |                         0x44,// tag string size | ||||||
|  |                         0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, | ||||||
|  |                         0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 | ||||||
|  |                      */ | ||||||
|  |                     0x0F,// tag string size | ||||||
|  |                     0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D,// "ENCODER=" binary string | ||||||
|  |                     0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string | ||||||
|  |                     0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// ???????? | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // not implemented for the desired codec | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void write(ByteBuffer buffer) throws IOException { | ||||||
|  |         output.write(buffer.array(), 0, buffer.position()); | ||||||
|  |         buffer.position(0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     private SimpleBlock getNextBlock() throws IOException { | ||||||
|  |         SimpleBlock res; | ||||||
|  |  | ||||||
|  |         if (webm_block != null) { | ||||||
|  |             res = webm_block; | ||||||
|  |             webm_block = null; | ||||||
|  |             return res; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (webm_segment == null) { | ||||||
|  |             webm_segment = webm.getNextSegment(); | ||||||
|  |             if (webm_segment == null) { | ||||||
|  |                 return null;// no more blocks in the selected track | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (webm_cluster == null) { | ||||||
|  |             webm_cluster = webm_segment.getNextCluster(); | ||||||
|  |             if (webm_cluster == null) { | ||||||
|  |                 webm_segment = null; | ||||||
|  |                 return getNextBlock(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         res = webm_cluster.getNextSimpleBlock(); | ||||||
|  |         if (res == null) { | ||||||
|  |             webm_cluster = null; | ||||||
|  |             return getNextBlock(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode; | ||||||
|  |         webm_block_last_timecode = res.absoluteTimeCodeNs; | ||||||
|  |  | ||||||
|  |         return res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private float getSampleFrequencyFromTrack(byte[] bMetadata) { | ||||||
|  |         // hardcoded way | ||||||
|  |         ByteBuffer buffer = ByteBuffer.wrap(bMetadata); | ||||||
|  |  | ||||||
|  |         while (buffer.remaining() >= 6) { | ||||||
|  |             int id = buffer.getShort() & 0xFFFF; | ||||||
|  |             if (id == 0x0000B584) { | ||||||
|  |                 return buffer.getFloat(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return 0f; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void clearSegmentTable() { | ||||||
|  |         segment_table_next_timestamp += TIME_SCALE_NS; | ||||||
|  |         packet_flag = FLAG_UNSET; | ||||||
|  |         segment_table_size = 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean addPacketSegment(SimpleBlock block) { | ||||||
|  |         long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay; | ||||||
|  |  | ||||||
|  |         if (timestamp >= segment_table_next_timestamp) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return addPacketSegment(block.dataSize); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean addPacketSegment(int size) { | ||||||
|  |         if (size > 65025) { | ||||||
|  |             throw new UnsupportedOperationException("page size cannot be larger than 65025"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         int available = (segment_table.length - segment_table_size) * 255; | ||||||
|  |         boolean extra = (size % 255) == 0; | ||||||
|  |  | ||||||
|  |         if (extra) { | ||||||
|  |             // add a zero byte entry in the table | ||||||
|  |             // required to indicate the sample size is multiple of 255 | ||||||
|  |             available -= 255; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // check if possible add the segment, without overflow the table | ||||||
|  |         if (available < size) { | ||||||
|  |             return false;// not enough space on the page | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (; size > 0; size -= 255) { | ||||||
|  |             segment_table[segment_table_size++] = (byte) Math.min(size, 255); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (extra) { | ||||||
|  |             segment_table[segment_table_size++] = 0x00; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void populate_crc32_table() { | ||||||
|  |         for (int i = 0; i < 0x100; i++) { | ||||||
|  |             int crc = i << 24; | ||||||
|  |             for (int j = 0; j < 8; j++) { | ||||||
|  |                 long b = crc >>> 31; | ||||||
|  |                 crc <<= 1; | ||||||
|  |                 crc ^= (int) (0x100000000L - b) & 0x04c11db7; | ||||||
|  |             } | ||||||
|  |             crc32_table[i] = crc; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int calc_crc32(int initial_crc, byte[] buffer, int size) { | ||||||
|  |         for (int i = 0; i < size; i++) { | ||||||
|  |             int reg = (initial_crc >>> 24) & 0xff; | ||||||
|  |             initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return initial_crc; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -15,7 +15,6 @@ import java.util.NoSuchElementException; | |||||||
|  */ |  */ | ||||||
| public class WebMReader { | public class WebMReader { | ||||||
|  |  | ||||||
|     //<editor-fold defaultState="collapsed" desc="constants"> |  | ||||||
|     private final static int ID_EMBL = 0x0A45DFA3; |     private final static int ID_EMBL = 0x0A45DFA3; | ||||||
|     private final static int ID_EMBLReadVersion = 0x02F7; |     private final static int ID_EMBLReadVersion = 0x02F7; | ||||||
|     private final static int ID_EMBLDocType = 0x0282; |     private final static int ID_EMBLDocType = 0x0282; | ||||||
| @@ -37,11 +36,14 @@ public class WebMReader { | |||||||
|     private final static int ID_Audio = 0x61; |     private final static int ID_Audio = 0x61; | ||||||
|     private final static int ID_DefaultDuration = 0x3E383; |     private final static int ID_DefaultDuration = 0x3E383; | ||||||
|     private final static int ID_FlagLacing = 0x1C; |     private final static int ID_FlagLacing = 0x1C; | ||||||
|  |     private final static int ID_CodecDelay = 0x16AA; | ||||||
|  |  | ||||||
|     private final static int ID_Cluster = 0x0F43B675; |     private final static int ID_Cluster = 0x0F43B675; | ||||||
|     private final static int ID_Timecode = 0x67; |     private final static int ID_Timecode = 0x67; | ||||||
|     private final static int ID_SimpleBlock = 0x23; |     private final static int ID_SimpleBlock = 0x23; | ||||||
| //</editor-fold> |     private final static int ID_Block = 0x21; | ||||||
|  |     private final static int ID_GroupBlock = 0x20; | ||||||
|  |  | ||||||
|  |  | ||||||
|     public enum TrackKind { |     public enum TrackKind { | ||||||
|         Audio/*2*/, Video/*1*/, Other |         Audio/*2*/, Video/*1*/, Other | ||||||
| @@ -96,7 +98,7 @@ public class WebMReader { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         ensure(segment.ref); |         ensure(segment.ref); | ||||||
|  |         // WARNING: track cannot be the same or have different index in new segments | ||||||
|         Element elem = untilElement(null, ID_Segment); |         Element elem = untilElement(null, ID_Segment); | ||||||
|         if (elem == null) { |         if (elem == null) { | ||||||
|             done = true; |             done = true; | ||||||
| @@ -107,7 +109,8 @@ public class WebMReader { | |||||||
|         return segment; |         return segment; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     //<editor-fold defaultstate="collapsed" desc="utils"> |  | ||||||
|  |  | ||||||
|     private long readNumber(Element parent) throws IOException { |     private long readNumber(Element parent) throws IOException { | ||||||
|         int length = (int) parent.contentSize; |         int length = (int) parent.contentSize; | ||||||
|         long value = 0; |         long value = 0; | ||||||
| @@ -189,6 +192,9 @@ public class WebMReader { | |||||||
|         Element elem; |         Element elem; | ||||||
|         while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) { |         while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) { | ||||||
|             elem = readElement(); |             elem = readElement(); | ||||||
|  |             if (expected.length < 1) { | ||||||
|  |                 return elem; | ||||||
|  |             } | ||||||
|             for (int type : expected) { |             for (int type : expected) { | ||||||
|                 if (elem.type == type) { |                 if (elem.type == type) { | ||||||
|                     return elem; |                     return elem; | ||||||
| @@ -219,9 +225,9 @@ public class WebMReader { | |||||||
|  |  | ||||||
|         stream.skipBytes(skip); |         stream.skipBytes(skip); | ||||||
|     } |     } | ||||||
| //</editor-fold> |  | ||||||
|  |  | ||||||
|     //<editor-fold defaultState="collapsed" desc="elements readers"> |  | ||||||
|  |  | ||||||
|     private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException { |     private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException { | ||||||
|         Element elem = untilElement(ref, ID_EMBLReadVersion); |         Element elem = untilElement(ref, ID_EMBLReadVersion); | ||||||
|         if (elem == null) { |         if (elem == null) { | ||||||
| @@ -300,9 +306,7 @@ public class WebMReader { | |||||||
|             WebMTrack entry = new WebMTrack(); |             WebMTrack entry = new WebMTrack(); | ||||||
|             boolean drop = false; |             boolean drop = false; | ||||||
|             Element elem; |             Element elem; | ||||||
|             while ((elem = untilElement(elem_trackEntry, |             while ((elem = untilElement(elem_trackEntry)) != null) { | ||||||
|                     ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video |  | ||||||
|             )) != null) { |  | ||||||
|                 switch (elem.type) { |                 switch (elem.type) { | ||||||
|                     case ID_TrackNumber: |                     case ID_TrackNumber: | ||||||
|                         entry.trackNumber = readNumber(elem); |                         entry.trackNumber = readNumber(elem); | ||||||
| @@ -326,8 +330,9 @@ public class WebMReader { | |||||||
|                     case ID_FlagLacing: |                     case ID_FlagLacing: | ||||||
|                         drop = readNumber(elem) != lacingExpected; |                         drop = readNumber(elem) != lacingExpected; | ||||||
|                         break; |                         break; | ||||||
|  |                     case ID_CodecDelay: | ||||||
|  |                         entry.codecDelay = readNumber(elem); | ||||||
|                     default: |                     default: | ||||||
|                         System.out.println(); |  | ||||||
|                         break; |                         break; | ||||||
|                 } |                 } | ||||||
|                 ensure(elem); |                 ensure(elem); | ||||||
| @@ -360,12 +365,13 @@ public class WebMReader { | |||||||
|  |  | ||||||
|     private SimpleBlock readSimpleBlock(Element ref) throws IOException { |     private SimpleBlock readSimpleBlock(Element ref) throws IOException { | ||||||
|         SimpleBlock obj = new SimpleBlock(ref); |         SimpleBlock obj = new SimpleBlock(ref); | ||||||
|         obj.dataSize = stream.position(); |  | ||||||
|         obj.trackNumber = readEncodedNumber(); |         obj.trackNumber = readEncodedNumber(); | ||||||
|         obj.relativeTimeCode = stream.readShort(); |         obj.relativeTimeCode = stream.readShort(); | ||||||
|         obj.flags = (byte) stream.read(); |         obj.flags = (byte) stream.read(); | ||||||
|         obj.dataSize = (ref.offset + ref.size) - stream.position(); |         obj.dataSize = (int) ((ref.offset + ref.size) - stream.position()); | ||||||
|  |         obj.createdFromBlock = ref.type == ID_Block; | ||||||
|  |  | ||||||
|  |         // NOTE: lacing is not implemented, and will be mixed with the stream data | ||||||
|         if (obj.dataSize < 0) { |         if (obj.dataSize < 0) { | ||||||
|             throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize)); |             throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize)); | ||||||
|         } |         } | ||||||
| @@ -383,9 +389,9 @@ public class WebMReader { | |||||||
|  |  | ||||||
|         return obj; |         return obj; | ||||||
|     } |     } | ||||||
| //</editor-fold> |  | ||||||
|  |  | ||||||
|     //<editor-fold defaultstate="collapsed" desc="class helpers"> |  | ||||||
|  |  | ||||||
|     class Element { |     class Element { | ||||||
|  |  | ||||||
|         int type; |         int type; | ||||||
| @@ -409,6 +415,7 @@ public class WebMReader { | |||||||
|         public byte[] bMetadata; |         public byte[] bMetadata; | ||||||
|         public TrackKind kind; |         public TrackKind kind; | ||||||
|         public long defaultDuration; |         public long defaultDuration; | ||||||
|  |         public long codecDelay; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public class Segment { |     public class Segment { | ||||||
| @@ -448,6 +455,7 @@ public class WebMReader { | |||||||
|     public class SimpleBlock { |     public class SimpleBlock { | ||||||
|  |  | ||||||
|         public InputStream data; |         public InputStream data; | ||||||
|  |         public boolean createdFromBlock; | ||||||
|  |  | ||||||
|         SimpleBlock(Element ref) { |         SimpleBlock(Element ref) { | ||||||
|             this.ref = ref; |             this.ref = ref; | ||||||
| @@ -455,8 +463,9 @@ public class WebMReader { | |||||||
|  |  | ||||||
|         public long trackNumber; |         public long trackNumber; | ||||||
|         public short relativeTimeCode; |         public short relativeTimeCode; | ||||||
|  |         public long absoluteTimeCodeNs; | ||||||
|         public byte flags; |         public byte flags; | ||||||
|         public long dataSize; |         public int dataSize; | ||||||
|         private final Element ref; |         private final Element ref; | ||||||
|  |  | ||||||
|         public boolean isKeyframe() { |         public boolean isKeyframe() { | ||||||
| @@ -468,33 +477,55 @@ public class WebMReader { | |||||||
|  |  | ||||||
|         Element ref; |         Element ref; | ||||||
|         SimpleBlock currentSimpleBlock = null; |         SimpleBlock currentSimpleBlock = null; | ||||||
|  |         Element currentBlockGroup = null; | ||||||
|         public long timecode; |         public long timecode; | ||||||
|  |  | ||||||
|         Cluster(Element ref) { |         Cluster(Element ref) { | ||||||
|             this.ref = ref; |             this.ref = ref; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         boolean check() { |         boolean insideClusterBounds() { | ||||||
|             return stream.position() >= (ref.offset + ref.size); |             return stream.position() >= (ref.offset + ref.size); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public SimpleBlock getNextSimpleBlock() throws IOException { |         public SimpleBlock getNextSimpleBlock() throws IOException { | ||||||
|             if (check()) { |             if (insideClusterBounds()) { | ||||||
|                 return null; |                 return null; | ||||||
|             } |             } | ||||||
|             if (currentSimpleBlock != null) { |  | ||||||
|  |             if (currentBlockGroup != null) { | ||||||
|  |                 ensure(currentBlockGroup); | ||||||
|  |                 currentBlockGroup = null; | ||||||
|  |                 currentSimpleBlock = null; | ||||||
|  |             } else if (currentSimpleBlock != null) { | ||||||
|                 ensure(currentSimpleBlock.ref); |                 ensure(currentSimpleBlock.ref); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             while (!check()) { |             while (!insideClusterBounds()) { | ||||||
|                 Element elem = untilElement(ref, ID_SimpleBlock); |                 Element elem = untilElement(ref, ID_SimpleBlock, ID_GroupBlock); | ||||||
|                 if (elem == null) { |                 if (elem == null) { | ||||||
|                     return null; |                     return null; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 if (elem.type == ID_GroupBlock) { | ||||||
|  |                     currentBlockGroup = elem; | ||||||
|  |                     elem = untilElement(currentBlockGroup, ID_Block); | ||||||
|  |  | ||||||
|  |                     if (elem == null) { | ||||||
|  |                         ensure(currentBlockGroup); | ||||||
|  |                         currentBlockGroup = null; | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 currentSimpleBlock = readSimpleBlock(elem); |                 currentSimpleBlock = readSimpleBlock(elem); | ||||||
|                 if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { |                 if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { | ||||||
|                     currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize); |                     currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize); | ||||||
|  |  | ||||||
|  |                     // calculate the timestamp in nanoseconds | ||||||
|  |                     currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + this.timecode; | ||||||
|  |                     currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale; | ||||||
|  |  | ||||||
|                     return currentSimpleBlock; |                     return currentSimpleBlock; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
| @@ -505,5 +536,5 @@ public class WebMReader { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     } |     } | ||||||
| //</editor-fold> |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.WebMReader.SimpleBlock; | |||||||
| import org.schabi.newpipe.streams.WebMReader.WebMTrack; | import org.schabi.newpipe.streams.WebMReader.WebMTrack; | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
|  | import java.io.Closeable; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
| import java.nio.ByteBuffer; | import java.nio.ByteBuffer; | ||||||
| @@ -17,7 +18,7 @@ import java.util.ArrayList; | |||||||
| /** | /** | ||||||
|  * @author kapodamy |  * @author kapodamy | ||||||
|  */ |  */ | ||||||
| public class WebMWriter { | public class WebMWriter implements Closeable { | ||||||
|  |  | ||||||
|     private final static int BUFFER_SIZE = 8 * 1024; |     private final static int BUFFER_SIZE = 8 * 1024; | ||||||
|     private final static int DEFAULT_TIMECODE_SCALE = 1000000; |     private final static int DEFAULT_TIMECODE_SCALE = 1000000; | ||||||
| @@ -35,7 +36,7 @@ public class WebMWriter { | |||||||
|     private long written = 0; |     private long written = 0; | ||||||
|  |  | ||||||
|     private Segment[] readersSegment; |     private Segment[] readersSegment; | ||||||
|     private Cluster[] readersCluter; |     private Cluster[] readersCluster; | ||||||
|  |  | ||||||
|     private int[] predefinedDurations; |     private int[] predefinedDurations; | ||||||
|  |  | ||||||
| @@ -81,7 +82,7 @@ public class WebMWriter { | |||||||
|     public void selectTracks(int... trackIndex) throws IOException { |     public void selectTracks(int... trackIndex) throws IOException { | ||||||
|         try { |         try { | ||||||
|             readersSegment = new Segment[readers.length]; |             readersSegment = new Segment[readers.length]; | ||||||
|             readersCluter = new Cluster[readers.length]; |             readersCluster = new Cluster[readers.length]; | ||||||
|             predefinedDurations = new int[readers.length]; |             predefinedDurations = new int[readers.length]; | ||||||
|  |  | ||||||
|             for (int i = 0; i < readers.length; i++) { |             for (int i = 0; i < readers.length; i++) { | ||||||
| @@ -102,6 +103,7 @@ public class WebMWriter { | |||||||
|         return parsed; |         return parsed; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|     public void close() { |     public void close() { | ||||||
|         done = true; |         done = true; | ||||||
|         parsed = true; |         parsed = true; | ||||||
| @@ -114,7 +116,7 @@ public class WebMWriter { | |||||||
|         readers = null; |         readers = null; | ||||||
|         infoTracks = null; |         infoTracks = null; | ||||||
|         readersSegment = null; |         readersSegment = null; | ||||||
|         readersCluter = null; |         readersCluster = null; | ||||||
|         outBuffer = null; |         outBuffer = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -247,7 +249,7 @@ public class WebMWriter { | |||||||
|                             nextCueTime += DEFAULT_CUES_EACH_MS; |                             nextCueTime += DEFAULT_CUES_EACH_MS; | ||||||
|                         } |                         } | ||||||
|                         keyFrames.add( |                         keyFrames.add( | ||||||
|                                 new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode) |                                 new KeyFrame(baseSegmentOffset, currentClusterOffset - 8, written, bTimecode.length, bloq.absoluteTimecode) | ||||||
|                         ); |                         ); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| @@ -334,17 +336,17 @@ public class WebMWriter { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (readersCluter[internalTrackId] == null) { |         if (readersCluster[internalTrackId] == null) { | ||||||
|             readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster(); |             readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster(); | ||||||
|             if (readersCluter[internalTrackId] == null) { |             if (readersCluster[internalTrackId] == null) { | ||||||
|                 readersSegment[internalTrackId] = null; |                 readersSegment[internalTrackId] = null; | ||||||
|                 return getNextBlockFrom(internalTrackId); |                 return getNextBlockFrom(internalTrackId); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock(); |         SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock(); | ||||||
|         if (res == null) { |         if (res == null) { | ||||||
|             readersCluter[internalTrackId] = null; |             readersCluster[internalTrackId] = null; | ||||||
|             return new Block();// fake block to indicate the end of the cluster |             return new Block();// fake block to indicate the end of the cluster | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -353,16 +355,11 @@ public class WebMWriter { | |||||||
|         bloq.dataSize = (int) res.dataSize; |         bloq.dataSize = (int) res.dataSize; | ||||||
|         bloq.trackNumber = internalTrackId; |         bloq.trackNumber = internalTrackId; | ||||||
|         bloq.flags = res.flags; |         bloq.flags = res.flags; | ||||||
|         bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale); |         bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE; | ||||||
|         bloq.absoluteTimecode += readersCluter[internalTrackId].timecode; |  | ||||||
|  |  | ||||||
|         return bloq; |         return bloq; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private short convertTimecode(int time, long oldTimeScale) { |  | ||||||
|         return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void seekTo(SharpStream stream, long offset) throws IOException { |     private void seekTo(SharpStream stream, long offset) throws IOException { | ||||||
|         if (stream.canSeek()) { |         if (stream.canSeek()) { | ||||||
|             stream.seek(offset); |             stream.seek(offset); | ||||||
|   | |||||||
| @@ -31,6 +31,12 @@ public class KioskTranslator { | |||||||
|                 return c.getString(R.string.top_50); |                 return c.getString(R.string.top_50); | ||||||
|             case "New & hot": |             case "New & hot": | ||||||
|                 return c.getString(R.string.new_and_hot); |                 return c.getString(R.string.new_and_hot); | ||||||
|  |             case "Local": | ||||||
|  |                 return c.getString(R.string.local); | ||||||
|  |             case "Recently added": | ||||||
|  |                 return c.getString(R.string.recently_added); | ||||||
|  |             case "Most liked": | ||||||
|  |                 return c.getString(R.string.most_liked); | ||||||
|             case "conferences": |             case "conferences": | ||||||
|                 return c.getString(R.string.conferences); |                 return c.getString(R.string.conferences); | ||||||
|             default: |             default: | ||||||
| @@ -46,6 +52,12 @@ public class KioskTranslator { | |||||||
|                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); |                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); | ||||||
|             case "New & hot": |             case "New & hot": | ||||||
|                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); |                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); | ||||||
|  |             case "Local": | ||||||
|  |                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); | ||||||
|  |             case "Recently added": | ||||||
|  |                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); | ||||||
|  |             case "Most liked": | ||||||
|  |                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.thumbs_up); | ||||||
|             case "conferences": |             case "conferences": | ||||||
|                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); |                 return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); | ||||||
|             default: |             default: | ||||||
|   | |||||||
| @@ -0,0 +1,65 @@ | |||||||
|  | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  |  | ||||||
|  | import com.grack.nanojson.JsonArray; | ||||||
|  | import com.grack.nanojson.JsonObject; | ||||||
|  | import com.grack.nanojson.JsonParser; | ||||||
|  | import com.grack.nanojson.JsonParserException; | ||||||
|  | import com.grack.nanojson.JsonStringWriter; | ||||||
|  | import com.grack.nanojson.JsonWriter; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.extractor.ServiceList; | ||||||
|  | import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | public class PeertubeHelper { | ||||||
|  |  | ||||||
|  |     public static List<PeertubeInstance> getInstanceList(Context context) { | ||||||
|  |         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |         String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key); | ||||||
|  |         final String savedJson = sharedPreferences.getString(savedInstanceListKey, null); | ||||||
|  |         if (null == savedJson) { | ||||||
|  |             return Collections.singletonList(getCurrentInstance()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             JsonArray array = JsonParser.object().from(savedJson).getArray("instances"); | ||||||
|  |             List<PeertubeInstance> result = new ArrayList<>(); | ||||||
|  |             for (Object o : array) { | ||||||
|  |                 if (o instanceof JsonObject) { | ||||||
|  |                     JsonObject instance = (JsonObject) o; | ||||||
|  |                     String name = instance.getString("name"); | ||||||
|  |                     String url = instance.getString("url"); | ||||||
|  |                     result.add(new PeertubeInstance(url, name)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return result; | ||||||
|  |         } catch (JsonParserException e) { | ||||||
|  |             return Collections.singletonList(getCurrentInstance()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static PeertubeInstance selectInstance(PeertubeInstance instance, Context context) { | ||||||
|  |         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |         String selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key); | ||||||
|  |         JsonStringWriter jsonWriter = JsonWriter.string().object(); | ||||||
|  |         jsonWriter.value("name", instance.getName()); | ||||||
|  |         jsonWriter.value("url", instance.getUrl()); | ||||||
|  |         String jsonToSave = jsonWriter.end().done(); | ||||||
|  |         sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply(); | ||||||
|  |         ServiceList.PeerTube.setInstance(instance); | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static PeertubeInstance getCurrentInstance(){ | ||||||
|  |         return ServiceList.PeerTube.getInstance(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -52,10 +52,12 @@ public class SecondaryStreamHelper<T extends Stream> { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (m4v) return null; | ||||||
|  |  | ||||||
|         // retry, but this time in reverse order |         // retry, but this time in reverse order | ||||||
|         for (int i = audioStreams.size() - 1; i >= 0; i--) { |         for (int i = audioStreams.size() - 1; i >= 0; i--) { | ||||||
|             AudioStream audio = audioStreams.get(i); |             AudioStream audio = audioStreams.get(i); | ||||||
|             if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) { |             if (audio.getFormat() == MediaFormat.WEBMA_OPUS) { | ||||||
|                 return audio; |                 return audio; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,15 +1,22 @@ | |||||||
| package org.schabi.newpipe.util; | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
|  |  | ||||||
| import androidx.annotation.DrawableRes; | import androidx.annotation.DrawableRes; | ||||||
| import androidx.annotation.StringRes; | import androidx.annotation.StringRes; | ||||||
|  |  | ||||||
|  | import com.grack.nanojson.JsonObject; | ||||||
|  | import com.grack.nanojson.JsonParser; | ||||||
|  | import com.grack.nanojson.JsonParserException; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.ServiceList; | import org.schabi.newpipe.extractor.ServiceList; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  | import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; | ||||||
|  |  | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
| @@ -27,13 +34,15 @@ public class ServiceHelper { | |||||||
|                 return R.drawable.place_holder_cloud; |                 return R.drawable.place_holder_cloud; | ||||||
|             case 2: |             case 2: | ||||||
|                 return R.drawable.place_holder_gadse; |                 return R.drawable.place_holder_gadse; | ||||||
|  |             case 3: | ||||||
|  |                 return R.drawable.place_holder_peertube; | ||||||
|             default: |             default: | ||||||
|                 return R.drawable.place_holder_circle; |                 return R.drawable.place_holder_circle; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static String getTranslatedFilterString(String filter, Context c) { |     public static String getTranslatedFilterString(String filter, Context c) { | ||||||
|         switch(filter) { |         switch (filter) { | ||||||
|             case "all": return c.getString(R.string.all); |             case "all": return c.getString(R.string.all); | ||||||
|             case "videos": return c.getString(R.string.videos); |             case "videos": return c.getString(R.string.videos); | ||||||
|             case "channels": return c.getString(R.string.channels); |             case "channels": return c.getString(R.string.channels); | ||||||
| @@ -126,9 +135,36 @@ public class ServiceHelper { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static boolean isBeta(final StreamingService s) { |     public static boolean isBeta(final StreamingService s) { | ||||||
|         switch(s.getServiceInfo().getName()) { |         switch (s.getServiceInfo().getName()) { | ||||||
|             case "YouTube": return false; |             case "YouTube": return false; | ||||||
|             default: return true; |             default: return true; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static void initService(Context context, int serviceId) { | ||||||
|  |         if (serviceId == ServiceList.PeerTube.getServiceId()) { | ||||||
|  |             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |             String json = sharedPreferences.getString(context.getString(R.string.peertube_selected_instance_key), null); | ||||||
|  |             if (null == json) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             JsonObject jsonObject = null; | ||||||
|  |             try { | ||||||
|  |                 jsonObject = JsonParser.object().from(json); | ||||||
|  |             } catch (JsonParserException e) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             String name = jsonObject.getString("name"); | ||||||
|  |             String url = jsonObject.getString("url"); | ||||||
|  |             PeertubeInstance instance = new PeertubeInstance(url, name); | ||||||
|  |             ServiceList.PeerTube.setInstance(instance); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static void initServices(Context context) { | ||||||
|  |         for (StreamingService s : ServiceList.all()) { | ||||||
|  |             initService(context, s.getServiceId()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| package us.shandian.giga.get; | package us.shandian.giga.get; | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import android.text.TextUtils; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| @@ -13,6 +15,7 @@ import java.nio.channels.ClosedByInterruptException; | |||||||
| import us.shandian.giga.util.Utility; | import us.shandian.giga.util.Utility; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||||
|  | import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN; | ||||||
|  |  | ||||||
| public class DownloadInitializer extends Thread { | public class DownloadInitializer extends Thread { | ||||||
|     private final static String TAG = "DownloadInitializer"; |     private final static String TAG = "DownloadInitializer"; | ||||||
| @@ -28,9 +31,9 @@ public class DownloadInitializer extends Thread { | |||||||
|         mConn = null; |         mConn = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static void safeClose(HttpURLConnection con) { |     private void dispose() { | ||||||
|         try { |         try { | ||||||
|             con.getInputStream().close(); |             mConn.getInputStream().close(); | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|             // nothing to do |             // nothing to do | ||||||
|         } |         } | ||||||
| @@ -51,9 +54,9 @@ public class DownloadInitializer extends Thread { | |||||||
|                     long lowestSize = Long.MAX_VALUE; |                     long lowestSize = Long.MAX_VALUE; | ||||||
|  |  | ||||||
|                     for (int i = 0; i < mMission.urls.length && mMission.running; i++) { |                     for (int i = 0; i < mMission.urls.length && mMission.running; i++) { | ||||||
|                         mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1); |                         mConn = mMission.openConnection(mMission.urls[i], true, -1, -1); | ||||||
|                         mMission.establishConnection(mId, mConn); |                         mMission.establishConnection(mId, mConn); | ||||||
|                         safeClose(mConn); |                         dispose(); | ||||||
|  |  | ||||||
|                         if (Thread.interrupted()) return; |                         if (Thread.interrupted()) return; | ||||||
|                         long length = Utility.getContentLength(mConn); |                         long length = Utility.getContentLength(mConn); | ||||||
| @@ -81,9 +84,9 @@ public class DownloadInitializer extends Thread { | |||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     // ask for the current resource length |                     // ask for the current resource length | ||||||
|                     mConn = mMission.openConnection(mId, -1, -1); |                     mConn = mMission.openConnection(true, -1, -1); | ||||||
|                     mMission.establishConnection(mId, mConn); |                     mMission.establishConnection(mId, mConn); | ||||||
|                     safeClose(mConn); |                     dispose(); | ||||||
|  |  | ||||||
|                     if (!mMission.running || Thread.interrupted()) return; |                     if (!mMission.running || Thread.interrupted()) return; | ||||||
|  |  | ||||||
| @@ -107,9 +110,9 @@ public class DownloadInitializer extends Thread { | |||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     // Open again |                     // Open again | ||||||
|                     mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length); |                     mConn = mMission.openConnection(true, mMission.length - 10, mMission.length); | ||||||
|                     mMission.establishConnection(mId, mConn); |                     mMission.establishConnection(mId, mConn); | ||||||
|                     safeClose(mConn); |                     dispose(); | ||||||
|  |  | ||||||
|                     if (!mMission.running || Thread.interrupted()) return; |                     if (!mMission.running || Thread.interrupted()) return; | ||||||
|  |  | ||||||
| @@ -151,12 +154,33 @@ public class DownloadInitializer extends Thread { | |||||||
|  |  | ||||||
|                 if (!mMission.running || Thread.interrupted()) return; |                 if (!mMission.running || Thread.interrupted()) return; | ||||||
|  |  | ||||||
|  |                 if (!mMission.unknownLength && mMission.recoveryInfo != null) { | ||||||
|  |                     String entityTag = mConn.getHeaderField("ETAG"); | ||||||
|  |                     String lastModified = mConn.getHeaderField("Last-Modified"); | ||||||
|  |                     MissionRecoveryInfo recovery = mMission.recoveryInfo[mMission.current]; | ||||||
|  |  | ||||||
|  |                     if (!TextUtils.isEmpty(entityTag)) { | ||||||
|  |                         recovery.validateCondition = entityTag; | ||||||
|  |                     } else if (!TextUtils.isEmpty(lastModified)) { | ||||||
|  |                         recovery.validateCondition = lastModified;// Note: this is less precise | ||||||
|  |                     } else { | ||||||
|  |                         recovery.validateCondition = null; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 mMission.running = false; |                 mMission.running = false; | ||||||
|                 break; |                 break; | ||||||
|             } catch (InterruptedIOException | ClosedByInterruptException e) { |             } catch (InterruptedIOException | ClosedByInterruptException e) { | ||||||
|                 return; |                 return; | ||||||
|             } catch (Exception e) { |             } catch (Exception e) { | ||||||
|                 if (!mMission.running) return; |                 if (!mMission.running || super.isInterrupted()) return; | ||||||
|  |  | ||||||
|  |                 if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) { | ||||||
|  |                     // for youtube streams. The url has expired | ||||||
|  |                     interrupt(); | ||||||
|  |                     mMission.doRecover(ERROR_HTTP_FORBIDDEN); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 if (e instanceof IOException && e.getMessage().contains("Permission denied")) { |                 if (e instanceof IOException && e.getMessage().contains("Permission denied")) { | ||||||
|                     mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); |                     mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); | ||||||
| @@ -179,13 +203,6 @@ public class DownloadInitializer extends Thread { | |||||||
|     @Override |     @Override | ||||||
|     public void interrupt() { |     public void interrupt() { | ||||||
|         super.interrupt(); |         super.interrupt(); | ||||||
|  |         if (mConn != null) dispose(); | ||||||
|         if (mConn != null) { |  | ||||||
|             try { |  | ||||||
|                 mConn.disconnect(); |  | ||||||
|             } catch (Exception e) { |  | ||||||
|                 // nothing to do |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,21 +1,27 @@ | |||||||
| package us.shandian.giga.get; | package us.shandian.giga.get; | ||||||
|  |  | ||||||
|  | import android.os.Build; | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
|  | import android.system.ErrnoException; | ||||||
|  | import android.system.OsConstants; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.DownloaderImpl; | import org.schabi.newpipe.DownloaderImpl; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileNotFoundException; | import java.io.FileNotFoundException; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  | import java.io.InterruptedIOException; | ||||||
| import java.io.Serializable; | import java.io.Serializable; | ||||||
| import java.net.ConnectException; | import java.net.ConnectException; | ||||||
| import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||||
| import java.net.SocketTimeoutException; | import java.net.SocketTimeoutException; | ||||||
| import java.net.URL; | import java.net.URL; | ||||||
| import java.net.UnknownHostException; | import java.net.UnknownHostException; | ||||||
|  | import java.nio.channels.ClosedByInterruptException; | ||||||
|  |  | ||||||
| import javax.net.ssl.SSLException; | import javax.net.ssl.SSLException; | ||||||
|  |  | ||||||
| @@ -27,14 +33,11 @@ import us.shandian.giga.util.Utility; | |||||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||||
|  |  | ||||||
| public class DownloadMission extends Mission { | public class DownloadMission extends Mission { | ||||||
|     private static final long serialVersionUID = 5L;// last bump: 30 june 2019 |     private static final long serialVersionUID = 6L;// last bump: 07 october 2019 | ||||||
|  |  | ||||||
|     static final int BUFFER_SIZE = 64 * 1024; |     static final int BUFFER_SIZE = 64 * 1024; | ||||||
|     static final int BLOCK_SIZE = 512 * 1024; |     static final int BLOCK_SIZE = 512 * 1024; | ||||||
|  |  | ||||||
|     @SuppressWarnings("SpellCheckingInspection") |  | ||||||
|     private static final String INSUFFICIENT_STORAGE = "ENOSPC"; |  | ||||||
|  |  | ||||||
|     private static final String TAG = "DownloadMission"; |     private static final String TAG = "DownloadMission"; | ||||||
|  |  | ||||||
|     public static final int ERROR_NOTHING = -1; |     public static final int ERROR_NOTHING = -1; | ||||||
| @@ -51,8 +54,9 @@ public class DownloadMission extends Mission { | |||||||
|     public static final int ERROR_INSUFFICIENT_STORAGE = 1010; |     public static final int ERROR_INSUFFICIENT_STORAGE = 1010; | ||||||
|     public static final int ERROR_PROGRESS_LOST = 1011; |     public static final int ERROR_PROGRESS_LOST = 1011; | ||||||
|     public static final int ERROR_TIMEOUT = 1012; |     public static final int ERROR_TIMEOUT = 1012; | ||||||
|  |     public static final int ERROR_RESOURCE_GONE = 1013; | ||||||
|     public static final int ERROR_HTTP_NO_CONTENT = 204; |     public static final int ERROR_HTTP_NO_CONTENT = 204; | ||||||
|     public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; |     static final int ERROR_HTTP_FORBIDDEN = 403; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The urls of the file to download |      * The urls of the file to download | ||||||
| @@ -60,9 +64,9 @@ public class DownloadMission extends Mission { | |||||||
|     public String[] urls; |     public String[] urls; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Number of bytes downloaded |      * Number of bytes downloaded and written | ||||||
|      */ |      */ | ||||||
|     public long done; |     public volatile long done; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Indicates a file generated dynamically on the web server |      * Indicates a file generated dynamically on the web server | ||||||
| @@ -118,31 +122,36 @@ public class DownloadMission extends Mission { | |||||||
|     /** |     /** | ||||||
|      * Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback} |      * Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback} | ||||||
|      */ |      */ | ||||||
|     long fallbackResumeOffset; |     volatile long fallbackResumeOffset; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Maximum of download threads running, chosen by the user |      * Maximum of download threads running, chosen by the user | ||||||
|      */ |      */ | ||||||
|     public int threadCount = 3; |     public int threadCount = 3; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * information required to recover a download | ||||||
|  |      */ | ||||||
|  |     public MissionRecoveryInfo[] recoveryInfo; | ||||||
|  |  | ||||||
|     private transient int finishCount; |     private transient int finishCount; | ||||||
|     public transient boolean running; |     public transient volatile boolean running; | ||||||
|     public boolean enqueued; |     public boolean enqueued; | ||||||
|  |  | ||||||
|     public int errCode = ERROR_NOTHING; |     public int errCode = ERROR_NOTHING; | ||||||
|     public Exception errObject = null; |     public Exception errObject = null; | ||||||
|  |  | ||||||
|     public transient boolean recovered; |  | ||||||
|     public transient Handler mHandler; |     public transient Handler mHandler; | ||||||
|     private transient boolean mWritingToFile; |  | ||||||
|     private transient boolean[] blockAcquired; |     private transient boolean[] blockAcquired; | ||||||
|  |  | ||||||
|  |     private transient long writingToFileNext; | ||||||
|  |     private transient volatile boolean writingToFile; | ||||||
|  |  | ||||||
|     final Object LOCK = new Lock(); |     final Object LOCK = new Lock(); | ||||||
|  |  | ||||||
|     private transient boolean deleted; |     @NonNull | ||||||
|  |     public transient Thread[] threads = new Thread[0]; | ||||||
|     public transient volatile Thread[] threads = new Thread[0]; |     public transient Thread init = null; | ||||||
|     private transient Thread init = null; |  | ||||||
|  |  | ||||||
|     public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { |     public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { | ||||||
|         if (urls == null) throw new NullPointerException("urls is null"); |         if (urls == null) throw new NullPointerException("urls is null"); | ||||||
| @@ -197,37 +206,34 @@ public class DownloadMission extends Mission { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Open connection |      * Opens a connection | ||||||
|      * |      * | ||||||
|      * @param threadId   id of the calling thread, used only for debug |      * @param headRequest {@code true} for use {@code HEAD} request method, otherwise, {@code GET} is used | ||||||
|      * @param rangeStart range start |      * @param rangeStart  range start | ||||||
|      * @param rangeEnd   range end |      * @param rangeEnd    range end | ||||||
|      * @return a {@link java.net.URLConnection URLConnection} linking to the URL. |      * @return a {@link java.net.URLConnection URLConnection} linking to the URL. | ||||||
|      * @throws IOException if an I/O exception occurs. |      * @throws IOException if an I/O exception occurs. | ||||||
|      */ |      */ | ||||||
|     HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException { |     HttpURLConnection openConnection(boolean headRequest, long rangeStart, long rangeEnd) throws IOException { | ||||||
|         return openConnection(urls[current], threadId, rangeStart, rangeEnd); |         return openConnection(urls[current], headRequest, rangeStart, rangeEnd); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException { |     HttpURLConnection openConnection(String url, boolean headRequest, long rangeStart, long rangeEnd) throws IOException { | ||||||
|         HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); |         HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); | ||||||
|         conn.setInstanceFollowRedirects(true); |         conn.setInstanceFollowRedirects(true); | ||||||
|         conn.setRequestProperty("User-Agent", DownloaderImpl.USER_AGENT); |         conn.setRequestProperty("User-Agent", DownloaderImpl.USER_AGENT); | ||||||
|         conn.setRequestProperty("Accept", "*/*"); |         conn.setRequestProperty("Accept", "*/*"); | ||||||
|  |  | ||||||
|  |         if (headRequest) conn.setRequestMethod("HEAD"); | ||||||
|  |  | ||||||
|         // BUG workaround: switching between networks can freeze the download forever |         // BUG workaround: switching between networks can freeze the download forever | ||||||
|         conn.setConnectTimeout(30000); |         conn.setConnectTimeout(30000); | ||||||
|         conn.setReadTimeout(10000); |  | ||||||
|  |  | ||||||
|         if (rangeStart >= 0) { |         if (rangeStart >= 0) { | ||||||
|             String req = "bytes=" + rangeStart + "-"; |             String req = "bytes=" + rangeStart + "-"; | ||||||
|             if (rangeEnd > 0) req += rangeEnd; |             if (rangeEnd > 0) req += rangeEnd; | ||||||
|  |  | ||||||
|             conn.setRequestProperty("Range", req); |             conn.setRequestProperty("Range", req); | ||||||
|  |  | ||||||
|             if (DEBUG) { |  | ||||||
|                 Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range")); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return conn; |         return conn; | ||||||
| @@ -240,18 +246,21 @@ public class DownloadMission extends Mission { | |||||||
|      * @throws HttpError   if the HTTP Status-Code is not satisfiable |      * @throws HttpError   if the HTTP Status-Code is not satisfiable | ||||||
|      */ |      */ | ||||||
|     void establishConnection(int threadId, HttpURLConnection conn) throws IOException, HttpError { |     void establishConnection(int threadId, HttpURLConnection conn) throws IOException, HttpError { | ||||||
|         conn.connect(); |  | ||||||
|         int statusCode = conn.getResponseCode(); |         int statusCode = conn.getResponseCode(); | ||||||
|  |  | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode); |             Log.d(TAG, threadId + ":[request]  Range=" + conn.getRequestProperty("Range")); | ||||||
|  |             Log.d(TAG, threadId + ":[response] Code=" + statusCode); | ||||||
|  |             Log.d(TAG, threadId + ":[response] Content-Length=" + conn.getContentLength()); | ||||||
|  |             Log.d(TAG, threadId + ":[response] Content-Range=" + conn.getHeaderField("Content-Range")); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|         switch (statusCode) { |         switch (statusCode) { | ||||||
|             case 204: |             case 204: | ||||||
|             case 205: |             case 205: | ||||||
|             case 207: |             case 207: | ||||||
|                 throw new HttpError(conn.getResponseCode()); |                 throw new HttpError(statusCode); | ||||||
|             case 416: |             case 416: | ||||||
|                 return;// let the download thread handle this error |                 return;// let the download thread handle this error | ||||||
|             default: |             default: | ||||||
| @@ -268,28 +277,19 @@ public class DownloadMission extends Mission { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     synchronized void notifyProgress(long deltaLen) { |     synchronized void notifyProgress(long deltaLen) { | ||||||
|         if (!running) return; |  | ||||||
|  |  | ||||||
|         if (recovered) { |  | ||||||
|             recovered = false; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (unknownLength) { |         if (unknownLength) { | ||||||
|             length += deltaLen;// Update length before proceeding |             length += deltaLen;// Update length before proceeding | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         done += deltaLen; |         done += deltaLen; | ||||||
|  |  | ||||||
|         if (done > length) { |         if (metadata == null) return; | ||||||
|             done = length; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (done != length && !deleted && !mWritingToFile) { |         if (!writingToFile && (done > writingToFileNext || deltaLen < 0)) { | ||||||
|             mWritingToFile = true; |             writingToFile = true; | ||||||
|             runAsync(-2, this::writeThisToFile); |             writingToFileNext = done + BLOCK_SIZE; | ||||||
|  |             writeThisToFileAsync(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         notify(DownloadManagerService.MESSAGE_PROGRESS); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     synchronized void notifyError(Exception err) { |     synchronized void notifyError(Exception err) { | ||||||
| @@ -314,13 +314,29 @@ public class DownloadMission extends Mission { | |||||||
|  |  | ||||||
|     public synchronized void notifyError(int code, Exception err) { |     public synchronized void notifyError(int code, Exception err) { | ||||||
|         Log.e(TAG, "notifyError() code = " + code, err); |         Log.e(TAG, "notifyError() code = " + code, err); | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |             if (err != null && err.getCause() instanceof ErrnoException) { | ||||||
|  |                 int errno = ((ErrnoException) err.getCause()).errno; | ||||||
|  |                 if (errno == OsConstants.ENOSPC) { | ||||||
|  |                     code = ERROR_INSUFFICIENT_STORAGE; | ||||||
|  |                     err = null; | ||||||
|  |                 } else if (errno == OsConstants.EACCES) { | ||||||
|  |                     code = ERROR_PERMISSION_DENIED; | ||||||
|  |                     err = null; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (err instanceof IOException) { |         if (err instanceof IOException) { | ||||||
|             if (!storage.canWrite() || err.getMessage().contains("Permission denied")) { |             if (err.getMessage().contains("Permission denied")) { | ||||||
|                 code = ERROR_PERMISSION_DENIED; |                 code = ERROR_PERMISSION_DENIED; | ||||||
|                 err = null; |                 err = null; | ||||||
|             } else if (err.getMessage().contains(INSUFFICIENT_STORAGE)) { |             } else if (err.getMessage().contains("ENOSPC")) { | ||||||
|                 code = ERROR_INSUFFICIENT_STORAGE; |                 code = ERROR_INSUFFICIENT_STORAGE; | ||||||
|                 err = null; |                 err = null; | ||||||
|  |             } else if (!storage.canWrite()) { | ||||||
|  |                 code = ERROR_FILE_CREATION; | ||||||
|  |                 err = null; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -342,44 +358,42 @@ public class DownloadMission extends Mission { | |||||||
|  |  | ||||||
|         notify(DownloadManagerService.MESSAGE_ERROR); |         notify(DownloadManagerService.MESSAGE_ERROR); | ||||||
|  |  | ||||||
|         if (running) { |         if (running) pauseThreads(); | ||||||
|             running = false; |  | ||||||
|             recovered = true; |  | ||||||
|             if (threads != null) selfPause(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     synchronized void notifyFinished() { |     synchronized void notifyFinished() { | ||||||
|         if (errCode > ERROR_NOTHING) return; |         if (current < urls.length) { | ||||||
|  |             if (++finishCount < threads.length) return; | ||||||
|         finishCount++; |  | ||||||
|  |  | ||||||
|         if (blocks.length < 1 || threads == null || finishCount == threads.length) { |  | ||||||
|             if (errCode != ERROR_NOTHING) return; |  | ||||||
|  |  | ||||||
|             if (DEBUG) { |             if (DEBUG) { | ||||||
|                 Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length); |                 Log.d(TAG, "onFinish: downloaded " + (current + 1) + "/" + urls.length); | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if ((current + 1) < urls.length) { |  | ||||||
|                 // prepare next sub-mission |  | ||||||
|                 long current_offset = offsets[current++]; |  | ||||||
|                 offsets[current] = current_offset + length; |  | ||||||
|                 initializer(); |  | ||||||
|                 return; |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             current++; |             current++; | ||||||
|             unknownLength = false; |             if (current < urls.length) { | ||||||
|  |                 // prepare next sub-mission | ||||||
|             if (!doPostprocessing()) return; |                 offsets[current] = offsets[current - 1] + length; | ||||||
|  |                 initializer(); | ||||||
|             enqueued = false; |                 return; | ||||||
|             running = false; |             } | ||||||
|             deleteThisFromFile(); |  | ||||||
|  |  | ||||||
|             notify(DownloadManagerService.MESSAGE_FINISHED); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (psAlgorithm != null && psState == 0) { | ||||||
|  |             threads = new Thread[]{ | ||||||
|  |                     runAsync(1, this::doPostprocessing) | ||||||
|  |             }; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         // this mission is fully finished | ||||||
|  |  | ||||||
|  |         unknownLength = false; | ||||||
|  |         enqueued = false; | ||||||
|  |         running = false; | ||||||
|  |  | ||||||
|  |         deleteThisFromFile(); | ||||||
|  |         notify(DownloadManagerService.MESSAGE_FINISHED); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void notifyPostProcessing(int state) { |     private void notifyPostProcessing(int state) { | ||||||
| @@ -397,10 +411,15 @@ public class DownloadMission extends Mission { | |||||||
|  |  | ||||||
|         Log.d(TAG, action + " postprocessing on " + storage.getName()); |         Log.d(TAG, action + " postprocessing on " + storage.getName()); | ||||||
|  |  | ||||||
|  |         if (state == 2) { | ||||||
|  |             psState = state; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         synchronized (LOCK) { |         synchronized (LOCK) { | ||||||
|             // don't return without fully write the current state |             // don't return without fully write the current state | ||||||
|             psState = state; |             psState = state; | ||||||
|             Utility.writeToFile(metadata, DownloadMission.this); |             writeThisToFile(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -409,14 +428,10 @@ public class DownloadMission extends Mission { | |||||||
|      * Start downloading with multiple threads. |      * Start downloading with multiple threads. | ||||||
|      */ |      */ | ||||||
|     public void start() { |     public void start() { | ||||||
|         if (running || isFinished()) return; |         if (running || isFinished() || urls.length < 1) return; | ||||||
|  |  | ||||||
|         // ensure that the previous state is completely paused. |         // ensure that the previous state is completely paused. | ||||||
|         joinForThread(init); |         joinForThreads(10000); | ||||||
|         if (threads != null) { |  | ||||||
|             for (Thread thread : threads) joinForThread(thread); |  | ||||||
|             threads = null; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         running = true; |         running = true; | ||||||
|         errCode = ERROR_NOTHING; |         errCode = ERROR_NOTHING; | ||||||
| @@ -427,7 +442,14 @@ public class DownloadMission extends Mission { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (current >= urls.length) { |         if (current >= urls.length) { | ||||||
|             runAsync(1, this::notifyFinished); |             notifyFinished(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         notify(DownloadManagerService.MESSAGE_RUNNING); | ||||||
|  |  | ||||||
|  |         if (urls[current] == null) { | ||||||
|  |             doRecover(ERROR_RESOURCE_GONE); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -441,18 +463,13 @@ public class DownloadMission extends Mission { | |||||||
|         blockAcquired = new boolean[blocks.length]; |         blockAcquired = new boolean[blocks.length]; | ||||||
|  |  | ||||||
|         if (blocks.length < 1) { |         if (blocks.length < 1) { | ||||||
|             if (unknownLength) { |  | ||||||
|                 done = 0; |  | ||||||
|                 length = 0; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))}; |             threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))}; | ||||||
|         } else { |         } else { | ||||||
|             int remainingBlocks = 0; |             int remainingBlocks = 0; | ||||||
|             for (int block : blocks) if (block >= 0) remainingBlocks++; |             for (int block : blocks) if (block >= 0) remainingBlocks++; | ||||||
|  |  | ||||||
|             if (remainingBlocks < 1) { |             if (remainingBlocks < 1) { | ||||||
|                 runAsync(1, this::notifyFinished); |                 notifyFinished(); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -478,7 +495,7 @@ public class DownloadMission extends Mission { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         running = false; |         running = false; | ||||||
|         recovered = true; |         notify(DownloadManagerService.MESSAGE_PAUSED); | ||||||
|  |  | ||||||
|         if (init != null && init.isAlive()) { |         if (init != null && init.isAlive()) { | ||||||
|             // NOTE: if start() method is running ¡will no have effect! |             // NOTE: if start() method is running ¡will no have effect! | ||||||
| @@ -493,29 +510,14 @@ public class DownloadMission extends Mission { | |||||||
|             Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server)."); |             Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server)."); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // check if the calling thread (alias UI thread) is interrupted |         init = null; | ||||||
|         if (Thread.currentThread().isInterrupted()) { |         pauseThreads(); | ||||||
|             writeThisToFile(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // wait for all threads are suspended before save the state |  | ||||||
|         if (threads != null) runAsync(-1, this::selfPause); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void selfPause() { |     private void pauseThreads() { | ||||||
|         try { |         running = false; | ||||||
|             for (Thread thread : threads) { |         joinForThreads(-1); | ||||||
|                 if (thread.isAlive()) { |         writeThisToFile(); | ||||||
|                     thread.interrupt(); |  | ||||||
|                     thread.join(5000); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } catch (Exception e) { |  | ||||||
|             // nothing to do |  | ||||||
|         } finally { |  | ||||||
|             writeThisToFile(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -523,9 +525,10 @@ public class DownloadMission extends Mission { | |||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public boolean delete() { |     public boolean delete() { | ||||||
|         deleted = true; |  | ||||||
|         if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir(); |         if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir(); | ||||||
|  |  | ||||||
|  |         notify(DownloadManagerService.MESSAGE_DELETED); | ||||||
|  |  | ||||||
|         boolean res = deleteThisFromFile(); |         boolean res = deleteThisFromFile(); | ||||||
|  |  | ||||||
|         if (!super.delete()) return false; |         if (!super.delete()) return false; | ||||||
| @@ -540,35 +543,37 @@ public class DownloadMission extends Mission { | |||||||
|      * @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false} |      * @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false} | ||||||
|      */ |      */ | ||||||
|     public void resetState(boolean rollback, boolean persistChanges, int errorCode) { |     public void resetState(boolean rollback, boolean persistChanges, int errorCode) { | ||||||
|         done = 0; |         length = 0; | ||||||
|         errCode = errorCode; |         errCode = errorCode; | ||||||
|         errObject = null; |         errObject = null; | ||||||
|         unknownLength = false; |         unknownLength = false; | ||||||
|         threads = null; |         threads = new Thread[0]; | ||||||
|         fallbackResumeOffset = 0; |         fallbackResumeOffset = 0; | ||||||
|         blocks = null; |         blocks = null; | ||||||
|         blockAcquired = null; |         blockAcquired = null; | ||||||
|  |  | ||||||
|         if (rollback) current = 0; |         if (rollback) current = 0; | ||||||
|  |         if (persistChanges) writeThisToFile(); | ||||||
|         if (persistChanges) |  | ||||||
|             Utility.writeToFile(metadata, DownloadMission.this); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void initializer() { |     private void initializer() { | ||||||
|         init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this)); |         init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void writeThisToFileAsync() { | ||||||
|  |         runAsync(-2, this::writeThisToFile); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Write this {@link DownloadMission} to the meta file asynchronously |      * Write this {@link DownloadMission} to the meta file asynchronously | ||||||
|      * if no thread is already running. |      * if no thread is already running. | ||||||
|      */ |      */ | ||||||
|     private void writeThisToFile() { |     void writeThisToFile() { | ||||||
|         synchronized (LOCK) { |         synchronized (LOCK) { | ||||||
|             if (deleted) return; |             if (metadata == null) return; | ||||||
|             Utility.writeToFile(metadata, DownloadMission.this); |             Utility.writeToFile(metadata, this); | ||||||
|  |             writingToFile = false; | ||||||
|         } |         } | ||||||
|         mWritingToFile = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -621,11 +626,10 @@ public class DownloadMission extends Mission { | |||||||
|     public long getLength() { |     public long getLength() { | ||||||
|         long calculated; |         long calculated; | ||||||
|         if (psState == 1 || psState == 3) { |         if (psState == 1 || psState == 3) { | ||||||
|             calculated = length; |             return length; | ||||||
|         } else { |  | ||||||
|             calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; | ||||||
|         calculated -= offsets[0];// don't count reserved space |         calculated -= offsets[0];// don't count reserved space | ||||||
|  |  | ||||||
|         return calculated > nearLength ? calculated : nearLength; |         return calculated > nearLength ? calculated : nearLength; | ||||||
| @@ -638,7 +642,7 @@ public class DownloadMission extends Mission { | |||||||
|      */ |      */ | ||||||
|     public void setEnqueued(boolean queue) { |     public void setEnqueued(boolean queue) { | ||||||
|         enqueued = queue; |         enqueued = queue; | ||||||
|         runAsync(-2, this::writeThisToFile); |         writeThisToFileAsync(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -667,24 +671,29 @@ public class DownloadMission extends Mission { | |||||||
|      * @return {@code true} is this mission its "healthy", otherwise, {@code false} |      * @return {@code true} is this mission its "healthy", otherwise, {@code false} | ||||||
|      */ |      */ | ||||||
|     public boolean isCorrupt() { |     public boolean isCorrupt() { | ||||||
|  |         if (urls.length < 1) return false; | ||||||
|         return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished(); |         return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean doPostprocessing() { |     /** | ||||||
|         if (psAlgorithm == null || psState == 2) return true; |      * Indicates if mission urls has expired and there an attempt to renovate them | ||||||
|  |      * | ||||||
|  |      * @return {@code true} if the mission is running a recovery procedure, otherwise, {@code false} | ||||||
|  |      */ | ||||||
|  |     public boolean isRecovering() { | ||||||
|  |         return threads.length > 0 && threads[0] instanceof DownloadMissionRecover && threads[0].isAlive(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void doPostprocessing() { | ||||||
|  |         errCode = ERROR_NOTHING; | ||||||
|         errObject = null; |         errObject = null; | ||||||
|  |         Thread thread = Thread.currentThread(); | ||||||
|  |  | ||||||
|         notifyPostProcessing(1); |         notifyPostProcessing(1); | ||||||
|         notifyProgress(0); |  | ||||||
|  |  | ||||||
|         if (DEBUG) |         if (DEBUG) { | ||||||
|             Thread.currentThread().setName("[" + TAG + "]  ps = " + |             thread.setName("[" + TAG + "]  ps = " + psAlgorithm + "  filename = " + storage.getName()); | ||||||
|                     psAlgorithm.getClass().getSimpleName() + |         } | ||||||
|                     "  filename = " + storage.getName() |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|         threads = new Thread[]{Thread.currentThread()}; |  | ||||||
|  |  | ||||||
|         Exception exception = null; |         Exception exception = null; | ||||||
|  |  | ||||||
| @@ -693,6 +702,11 @@ public class DownloadMission extends Mission { | |||||||
|         } catch (Exception err) { |         } catch (Exception err) { | ||||||
|             Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err); |             Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err); | ||||||
|  |  | ||||||
|  |             if (err instanceof InterruptedIOException || err instanceof ClosedByInterruptException || thread.isInterrupted()) { | ||||||
|  |                 notifyError(DownloadMission.ERROR_POSTPROCESSING_STOPPED, null); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; |             if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; | ||||||
|  |  | ||||||
|             exception = err; |             exception = err; | ||||||
| @@ -703,16 +717,38 @@ public class DownloadMission extends Mission { | |||||||
|         if (errCode != ERROR_NOTHING) { |         if (errCode != ERROR_NOTHING) { | ||||||
|             if (exception == null) exception = errObject; |             if (exception == null) exception = errObject; | ||||||
|             notifyError(ERROR_POSTPROCESSING, exception); |             notifyError(ERROR_POSTPROCESSING, exception); | ||||||
|  |             return; | ||||||
|             return false; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return true; |         notifyFinished(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Attempts to recover the download | ||||||
|  |      * | ||||||
|  |      * @param errorCode error code which trigger the recovery procedure | ||||||
|  |      */ | ||||||
|  |     void doRecover(int errorCode) { | ||||||
|  |         Log.i(TAG, "Attempting to recover the mission: " + storage.getName()); | ||||||
|  |  | ||||||
|  |         if (recoveryInfo == null) { | ||||||
|  |             notifyError(errorCode, null); | ||||||
|  |             urls = new String[0];// mark this mission as dead | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         joinForThreads(0); | ||||||
|  |  | ||||||
|  |         threads = new Thread[]{ | ||||||
|  |                 runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, errorCode)) | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean deleteThisFromFile() { |     private boolean deleteThisFromFile() { | ||||||
|         synchronized (LOCK) { |         synchronized (LOCK) { | ||||||
|             return metadata.delete(); |             boolean res = metadata.delete(); | ||||||
|  |             metadata = null; | ||||||
|  |             return res; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -722,8 +758,8 @@ public class DownloadMission extends Mission { | |||||||
|      * @param id  id of new thread (used for debugging only) |      * @param id  id of new thread (used for debugging only) | ||||||
|      * @param who the Runnable whose {@code run} method is invoked. |      * @param who the Runnable whose {@code run} method is invoked. | ||||||
|      */ |      */ | ||||||
|     private void runAsync(int id, Runnable who) { |     private Thread runAsync(int id, Runnable who) { | ||||||
|         runAsync(id, new Thread(who)); |         return runAsync(id, new Thread(who)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -749,25 +785,47 @@ public class DownloadMission extends Mission { | |||||||
|         return who; |         return who; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void joinForThread(Thread thread) { |     /** | ||||||
|         if (thread == null || !thread.isAlive()) return; |      * Waits at most {@code millis} milliseconds for the thread to die | ||||||
|         if (thread == Thread.currentThread()) return; |      * | ||||||
|  |      * @param millis the time to wait in milliseconds | ||||||
|  |      */ | ||||||
|  |     private void joinForThreads(int millis) { | ||||||
|  |         final Thread currentThread = Thread.currentThread(); | ||||||
|  |  | ||||||
|         if (DEBUG) { |         if (init != null && init != currentThread && init.isAlive()) { | ||||||
|             Log.w(TAG, "a thread is !still alive!: " + thread.getName()); |             init.interrupt(); | ||||||
|  |  | ||||||
|  |             if (millis > 0) { | ||||||
|  |                 try { | ||||||
|  |                     init.join(millis); | ||||||
|  |                 } catch (InterruptedException e) { | ||||||
|  |                     Log.w(TAG, "Initializer thread is still running", e); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // still alive, this should not happen. |         // if a thread is still alive, possible reasons: | ||||||
|         // Possible reasons: |  | ||||||
|         //      slow device |         //      slow device | ||||||
|         //      the user is spamming start/pause buttons |         //      the user is spamming start/pause buttons | ||||||
|         //      start() method called quickly after pause() |         //      start() method called quickly after pause() | ||||||
|  |  | ||||||
|  |         for (Thread thread : threads) { | ||||||
|  |             if (!thread.isAlive() || thread == Thread.currentThread()) continue; | ||||||
|  |             thread.interrupt(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             thread.join(10000); |             for (Thread thread : threads) { | ||||||
|  |                 if (!thread.isAlive()) continue; | ||||||
|  |                 if (DEBUG) { | ||||||
|  |                     Log.w(TAG, "thread alive: " + thread.getName()); | ||||||
|  |                 } | ||||||
|  |                 if (millis > 0) thread.join(millis); | ||||||
|  |             } | ||||||
|         } catch (InterruptedException e) { |         } catch (InterruptedException e) { | ||||||
|             Log.d(TAG, "timeout on join : " + thread.getName()); |             throw new RuntimeException("A download thread is still running", e); | ||||||
|             throw new RuntimeException("A thread is still running:\n" + thread.getName()); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -785,9 +843,9 @@ public class DownloadMission extends Mission { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static class Block { |     public static class Block { | ||||||
|         int position; |         public int position; | ||||||
|         int done; |         public int done; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static class Lock implements Serializable { |     private static class Lock implements Serializable { | ||||||
|   | |||||||
| @@ -0,0 +1,313 @@ | |||||||
|  | package us.shandian.giga.get; | ||||||
|  |  | ||||||
|  | import android.util.Log; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.extractor.NewPipe; | ||||||
|  | import org.schabi.newpipe.extractor.StreamingService; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  | import org.schabi.newpipe.extractor.stream.AudioStream; | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamExtractor; | ||||||
|  | import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||||
|  | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InterruptedIOException; | ||||||
|  | import java.net.HttpURLConnection; | ||||||
|  | import java.nio.channels.ClosedByInterruptException; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import us.shandian.giga.get.DownloadMission.HttpError; | ||||||
|  |  | ||||||
|  | import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; | ||||||
|  |  | ||||||
|  | public class DownloadMissionRecover extends Thread { | ||||||
|  |     private static final String TAG = "DownloadMissionRecover"; | ||||||
|  |     static final int mID = -3; | ||||||
|  |  | ||||||
|  |     private final DownloadMission mMission; | ||||||
|  |     private final boolean mNotInitialized; | ||||||
|  |  | ||||||
|  |     private final int mErrCode; | ||||||
|  |  | ||||||
|  |     private HttpURLConnection mConn; | ||||||
|  |     private MissionRecoveryInfo mRecovery; | ||||||
|  |     private StreamExtractor mExtractor; | ||||||
|  |  | ||||||
|  |     DownloadMissionRecover(DownloadMission mission, int errCode) { | ||||||
|  |         mMission = mission; | ||||||
|  |         mNotInitialized = mission.blocks == null && mission.current == 0; | ||||||
|  |         mErrCode = errCode; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void run() { | ||||||
|  |         if (mMission.source == null) { | ||||||
|  |             mMission.notifyError(mErrCode, null); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Exception err = null; | ||||||
|  |         int attempt = 0; | ||||||
|  |  | ||||||
|  |         while (attempt++ < mMission.maxRetry) { | ||||||
|  |             try { | ||||||
|  |                 tryRecover(); | ||||||
|  |                 return; | ||||||
|  |             } catch (InterruptedIOException | ClosedByInterruptException e) { | ||||||
|  |                 return; | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 if (!mMission.running || super.isInterrupted()) return; | ||||||
|  |                 err = e; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // give up | ||||||
|  |         mMission.notifyError(mErrCode, err); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void tryRecover() throws ExtractionException, IOException, HttpError { | ||||||
|  |         if (mExtractor == null) { | ||||||
|  |             try { | ||||||
|  |                 StreamingService svr = NewPipe.getServiceByUrl(mMission.source); | ||||||
|  |                 mExtractor = svr.getStreamExtractor(mMission.source); | ||||||
|  |                 mExtractor.fetchPage(); | ||||||
|  |             } catch (ExtractionException e) { | ||||||
|  |                 mExtractor = null; | ||||||
|  |                 throw e; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // maybe the following check is redundant | ||||||
|  |         if (!mMission.running || super.isInterrupted()) return; | ||||||
|  |  | ||||||
|  |         if (!mNotInitialized) { | ||||||
|  |             // set the current download url to null in case if the recovery | ||||||
|  |             // process is canceled. Next time start() method is called the | ||||||
|  |             // recovery will be executed, saving time | ||||||
|  |             mMission.urls[mMission.current] = null; | ||||||
|  |  | ||||||
|  |             mRecovery = mMission.recoveryInfo[mMission.current]; | ||||||
|  |             resolveStream(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Log.w(TAG, "mission is not fully initialized, this will take a while"); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             for (; mMission.current < mMission.urls.length; mMission.current++) { | ||||||
|  |                 mRecovery = mMission.recoveryInfo[mMission.current]; | ||||||
|  |  | ||||||
|  |                 if (test()) continue; | ||||||
|  |                 if (!mMission.running) return; | ||||||
|  |  | ||||||
|  |                 resolveStream(); | ||||||
|  |                 if (!mMission.running) return; | ||||||
|  |  | ||||||
|  |                 // before continue, check if the current stream was resolved | ||||||
|  |                 if (mMission.urls[mMission.current] == null) { | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             mMission.current = 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         mMission.writeThisToFile(); | ||||||
|  |  | ||||||
|  |         if (!mMission.running || super.isInterrupted()) return; | ||||||
|  |  | ||||||
|  |         mMission.running = false; | ||||||
|  |         mMission.start(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void resolveStream() throws IOException, ExtractionException, HttpError { | ||||||
|  |         // FIXME: this getErrorMessage() always returns "video is unavailable" | ||||||
|  |         /*if (mExtractor.getErrorMessage() != null) { | ||||||
|  |             mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage())); | ||||||
|  |             return; | ||||||
|  |         }*/ | ||||||
|  |  | ||||||
|  |         String url = null; | ||||||
|  |  | ||||||
|  |         switch (mRecovery.kind) { | ||||||
|  |             case 'a': | ||||||
|  |                 for (AudioStream audio : mExtractor.getAudioStreams()) { | ||||||
|  |                     if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) { | ||||||
|  |                         url = audio.getUrl(); | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'v': | ||||||
|  |                 List<VideoStream> videoStreams; | ||||||
|  |                 if (mRecovery.desired2) | ||||||
|  |                     videoStreams = mExtractor.getVideoOnlyStreams(); | ||||||
|  |                 else | ||||||
|  |                     videoStreams = mExtractor.getVideoStreams(); | ||||||
|  |                 for (VideoStream video : videoStreams) { | ||||||
|  |                     if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) { | ||||||
|  |                         url = video.getUrl(); | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 's': | ||||||
|  |                 for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) { | ||||||
|  |                     String tag = subtitles.getLanguageTag(); | ||||||
|  |                     if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) { | ||||||
|  |                         url = subtitles.getURL(); | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new RuntimeException("Unknown stream type"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         resolve(url); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void resolve(String url) throws IOException, HttpError { | ||||||
|  |         if (mRecovery.validateCondition == null) { | ||||||
|  |             Log.w(TAG, "validation condition not defined, the resource can be stale"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (mMission.unknownLength || mRecovery.validateCondition == null) { | ||||||
|  |             recover(url, false); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /////////////////////////////////////////////////////////////////////// | ||||||
|  |         ////// Validate the http resource doing a range request | ||||||
|  |         ///////////////////// | ||||||
|  |         try { | ||||||
|  |             mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length); | ||||||
|  |             mConn.setRequestProperty("If-Range", mRecovery.validateCondition); | ||||||
|  |             mMission.establishConnection(mID, mConn); | ||||||
|  |  | ||||||
|  |             int code = mConn.getResponseCode(); | ||||||
|  |  | ||||||
|  |             switch (code) { | ||||||
|  |                 case 200: | ||||||
|  |                 case 413: | ||||||
|  |                     // stale | ||||||
|  |                     recover(url, true); | ||||||
|  |                     return; | ||||||
|  |                 case 206: | ||||||
|  |                     // in case of validation using the Last-Modified date, check the resource length | ||||||
|  |                     long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range")); | ||||||
|  |                     boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length; | ||||||
|  |  | ||||||
|  |                     recover(url, lengthMismatch); | ||||||
|  |                     return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             throw new HttpError(code); | ||||||
|  |         } finally { | ||||||
|  |             disconnect(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void recover(String url, boolean stale) { | ||||||
|  |         Log.i(TAG, | ||||||
|  |                 String.format("recover()  name=%s  isStale=%s  url=%s", mMission.storage.getName(), stale, url) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         mMission.urls[mMission.current] = url; | ||||||
|  |  | ||||||
|  |         if (url == null) { | ||||||
|  |             mMission.urls = new String[0]; | ||||||
|  |             mMission.notifyError(ERROR_RESOURCE_GONE, null); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (mNotInitialized) return; | ||||||
|  |  | ||||||
|  |         if (stale) { | ||||||
|  |             mMission.resetState(false, false, DownloadMission.ERROR_NOTHING); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         mMission.writeThisToFile(); | ||||||
|  |  | ||||||
|  |         if (!mMission.running || super.isInterrupted()) return; | ||||||
|  |  | ||||||
|  |         mMission.running = false; | ||||||
|  |         mMission.start(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private long[] parseContentRange(String value) { | ||||||
|  |         long[] range = new long[3]; | ||||||
|  |  | ||||||
|  |         if (value == null) { | ||||||
|  |             // this never should happen | ||||||
|  |             return range; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             value = value.trim(); | ||||||
|  |  | ||||||
|  |             if (!value.startsWith("bytes")) { | ||||||
|  |                 return range;// unknown range type | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             int space = value.lastIndexOf(' ') + 1; | ||||||
|  |             int dash = value.indexOf('-', space) + 1; | ||||||
|  |             int bar = value.indexOf('/', dash); | ||||||
|  |  | ||||||
|  |             // start | ||||||
|  |             range[0] = Long.parseLong(value.substring(space, dash - 1)); | ||||||
|  |  | ||||||
|  |             // end | ||||||
|  |             range[1] = Long.parseLong(value.substring(dash, bar)); | ||||||
|  |  | ||||||
|  |             // resource length | ||||||
|  |             value = value.substring(bar + 1); | ||||||
|  |             if (value.equals("*")) { | ||||||
|  |                 range[2] = -1;// unknown length received from the server but should be valid | ||||||
|  |             } else { | ||||||
|  |                 range[2] = Long.parseLong(value); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             // nothing to do | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return range; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean test() { | ||||||
|  |         if (mMission.urls[mMission.current] == null) return false; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1); | ||||||
|  |             mMission.establishConnection(mID, mConn); | ||||||
|  |  | ||||||
|  |             if (mConn.getResponseCode() == 200) return true; | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             // nothing to do | ||||||
|  |         } finally { | ||||||
|  |             disconnect(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void disconnect() { | ||||||
|  |         try { | ||||||
|  |             try { | ||||||
|  |                 mConn.getInputStream().close(); | ||||||
|  |             } finally { | ||||||
|  |                 mConn.disconnect(); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             // nothing to do | ||||||
|  |         } finally { | ||||||
|  |             mConn = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void interrupt() { | ||||||
|  |         super.interrupt(); | ||||||
|  |         if (mConn != null) disconnect(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,8 +10,10 @@ import java.net.HttpURLConnection; | |||||||
| import java.nio.channels.ClosedByInterruptException; | import java.nio.channels.ClosedByInterruptException; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission.Block; | import us.shandian.giga.get.DownloadMission.Block; | ||||||
|  | import us.shandian.giga.get.DownloadMission.HttpError; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||||
|  | import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN; | ||||||
|  |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -19,7 +21,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; | |||||||
|  * an error occurs or the process is stopped. |  * an error occurs or the process is stopped. | ||||||
|  */ |  */ | ||||||
| public class DownloadRunnable extends Thread { | public class DownloadRunnable extends Thread { | ||||||
|     private static final String TAG = DownloadRunnable.class.getSimpleName(); |     private static final String TAG = "DownloadRunnable"; | ||||||
|  |  | ||||||
|     private final DownloadMission mMission; |     private final DownloadMission mMission; | ||||||
|     private final int mId; |     private final int mId; | ||||||
| @@ -41,13 +43,7 @@ public class DownloadRunnable extends Thread { | |||||||
|     public void run() { |     public void run() { | ||||||
|         boolean retry = false; |         boolean retry = false; | ||||||
|         Block block = null; |         Block block = null; | ||||||
|  |  | ||||||
|         int retryCount = 0; |         int retryCount = 0; | ||||||
|  |  | ||||||
|         if (DEBUG) { |  | ||||||
|             Log.d(TAG, mId + ":recovered: " + mMission.recovered); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         SharpStream f; |         SharpStream f; | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
| @@ -84,13 +80,14 @@ public class DownloadRunnable extends Thread { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             try { |             try { | ||||||
|                 mConn = mMission.openConnection(mId, start, end); |                 mConn = mMission.openConnection(false, start, end); | ||||||
|                 mMission.establishConnection(mId, mConn); |                 mMission.establishConnection(mId, mConn); | ||||||
|  |  | ||||||
|                 // check if the download can be resumed |                 // check if the download can be resumed | ||||||
|                 if (mConn.getResponseCode() == 416) { |                 if (mConn.getResponseCode() == 416) { | ||||||
|                     if (block.done > 0) { |                     if (block.done > 0) { | ||||||
|                         // try again from the start (of the block) |                         // try again from the start (of the block) | ||||||
|  |                         mMission.notifyProgress(-block.done); | ||||||
|                         block.done = 0; |                         block.done = 0; | ||||||
|                         retry = true; |                         retry = true; | ||||||
|                         mConn.disconnect(); |                         mConn.disconnect(); | ||||||
| @@ -118,7 +115,7 @@ public class DownloadRunnable extends Thread { | |||||||
|                     int len; |                     int len; | ||||||
|  |  | ||||||
|                     // use always start <= end |                     // use always start <= end | ||||||
|                     // fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly |                     // fixes a deadlock because in some videos, youtube is sending one byte alone | ||||||
|                     while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) { |                     while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) { | ||||||
|                         f.write(buf, 0, len); |                         f.write(buf, 0, len); | ||||||
|                         start += len; |                         start += len; | ||||||
| @@ -133,6 +130,17 @@ public class DownloadRunnable extends Thread { | |||||||
|             } catch (Exception e) { |             } catch (Exception e) { | ||||||
|                 if (!mMission.running || e instanceof ClosedByInterruptException) break; |                 if (!mMission.running || e instanceof ClosedByInterruptException) break; | ||||||
|  |  | ||||||
|  |                 if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) { | ||||||
|  |                     // for youtube streams. The url has expired, recover | ||||||
|  |                     f.close(); | ||||||
|  |  | ||||||
|  |                     if (mId == 1) { | ||||||
|  |                         // only the first thread will execute the recovery procedure | ||||||
|  |                         mMission.doRecover(ERROR_HTTP_FORBIDDEN); | ||||||
|  |                     } | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 if (retryCount++ >= mMission.maxRetry) { |                 if (retryCount++ >= mMission.maxRetry) { | ||||||
|                     mMission.notifyError(e); |                     mMission.notifyError(e); | ||||||
|                     break; |                     break; | ||||||
| @@ -144,11 +152,7 @@ public class DownloadRunnable extends Thread { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         try { |         f.close(); | ||||||
|             f.close(); |  | ||||||
|         } catch (Exception err) { |  | ||||||
|             // ¿ejected media storage?  ¿file deleted?  ¿storage ran out of space? |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, "thread " + mId + " exited from main download loop"); |             Log.d(TAG, "thread " + mId + " exited from main download loop"); | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| package us.shandian.giga.get; | package us.shandian.giga.get; | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| @@ -10,9 +11,11 @@ import java.io.InputStream; | |||||||
| import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||||
| import java.nio.channels.ClosedByInterruptException; | import java.nio.channels.ClosedByInterruptException; | ||||||
|  |  | ||||||
|  | import us.shandian.giga.get.DownloadMission.HttpError; | ||||||
| import us.shandian.giga.util.Utility; | import us.shandian.giga.util.Utility; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||||
|  | import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Single-threaded fallback mode |  * Single-threaded fallback mode | ||||||
| @@ -33,7 +36,11 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|  |  | ||||||
|     private void dispose() { |     private void dispose() { | ||||||
|         try { |         try { | ||||||
|             if (mIs != null) mIs.close(); |             try { | ||||||
|  |                 if (mIs != null) mIs.close(); | ||||||
|  |             } finally { | ||||||
|  |                 mConn.disconnect(); | ||||||
|  |             } | ||||||
|         } catch (IOException e) { |         } catch (IOException e) { | ||||||
|             // nothing to do |             // nothing to do | ||||||
|         } |         } | ||||||
| @@ -41,22 +48,10 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|         if (mF != null) mF.close(); |         if (mF != null) mF.close(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private long loadPosition() { |  | ||||||
|         synchronized (mMission.LOCK) { |  | ||||||
|             return mMission.fallbackResumeOffset; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void savePosition(long position) { |  | ||||||
|         synchronized (mMission.LOCK) { |  | ||||||
|             mMission.fallbackResumeOffset = position; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void run() { |     public void run() { | ||||||
|         boolean done; |         boolean done; | ||||||
|         long start = loadPosition(); |         long start = mMission.fallbackResumeOffset; | ||||||
|  |  | ||||||
|         if (DEBUG && !mMission.unknownLength && start > 0) { |         if (DEBUG && !mMission.unknownLength && start > 0) { | ||||||
|             Log.i(TAG, "Resuming a single-thread download at " + start); |             Log.i(TAG, "Resuming a single-thread download at " + start); | ||||||
| @@ -66,11 +61,18 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|             long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; |             long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; | ||||||
|  |  | ||||||
|             int mId = 1; |             int mId = 1; | ||||||
|             mConn = mMission.openConnection(mId, rangeStart, -1); |             mConn = mMission.openConnection(false, rangeStart, -1); | ||||||
|  |  | ||||||
|  |             if (mRetryCount == 0 && rangeStart == -1) { | ||||||
|  |                 // workaround: bypass android connection pool | ||||||
|  |                 mConn.setRequestProperty("Range", "bytes=0-"); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             mMission.establishConnection(mId, mConn); |             mMission.establishConnection(mId, mConn); | ||||||
|  |  | ||||||
|             // check if the download can be resumed |             // check if the download can be resumed | ||||||
|             if (mConn.getResponseCode() == 416 && start > 0) { |             if (mConn.getResponseCode() == 416 && start > 0) { | ||||||
|  |                 mMission.notifyProgress(-start); | ||||||
|                 start = 0; |                 start = 0; | ||||||
|                 mRetryCount--; |                 mRetryCount--; | ||||||
|                 throw new DownloadMission.HttpError(416); |                 throw new DownloadMission.HttpError(416); | ||||||
| @@ -80,12 +82,17 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|             if (!mMission.unknownLength) |             if (!mMission.unknownLength) | ||||||
|                 mMission.unknownLength = Utility.getContentLength(mConn) == -1; |                 mMission.unknownLength = Utility.getContentLength(mConn) == -1; | ||||||
|  |  | ||||||
|  |             if (mMission.unknownLength || mConn.getResponseCode() == 200) { | ||||||
|  |                 // restart amount of bytes downloaded | ||||||
|  |                 mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0]; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             mF = mMission.storage.getStream(); |             mF = mMission.storage.getStream(); | ||||||
|             mF.seek(mMission.offsets[mMission.current] + start); |             mF.seek(mMission.offsets[mMission.current] + start); | ||||||
|  |  | ||||||
|             mIs = mConn.getInputStream(); |             mIs = mConn.getInputStream(); | ||||||
|  |  | ||||||
|             byte[] buf = new byte[64 * 1024]; |             byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; | ||||||
|             int len = 0; |             int len = 0; | ||||||
|  |  | ||||||
|             while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) { |             while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) { | ||||||
| @@ -94,15 +101,24 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|                 mMission.notifyProgress(len); |                 mMission.notifyProgress(len); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             dispose(); | ||||||
|  |  | ||||||
|             // if thread goes interrupted check if the last part is written. This avoid re-download the whole file |             // if thread goes interrupted check if the last part is written. This avoid re-download the whole file | ||||||
|             done = len == -1; |             done = len == -1; | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|             dispose(); |             dispose(); | ||||||
|  |  | ||||||
|             savePosition(start); |             mMission.fallbackResumeOffset = start; | ||||||
|  |  | ||||||
|             if (!mMission.running || e instanceof ClosedByInterruptException) return; |             if (!mMission.running || e instanceof ClosedByInterruptException) return; | ||||||
|  |  | ||||||
|  |             if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) { | ||||||
|  |                 // for youtube streams. The url has expired, recover | ||||||
|  |                 dispose(); | ||||||
|  |                 mMission.doRecover(ERROR_HTTP_FORBIDDEN); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             if (mRetryCount++ >= mMission.maxRetry) { |             if (mRetryCount++ >= mMission.maxRetry) { | ||||||
|                 mMission.notifyError(e); |                 mMission.notifyError(e); | ||||||
|                 return; |                 return; | ||||||
| @@ -116,12 +132,10 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         dispose(); |  | ||||||
|  |  | ||||||
|         if (done) { |         if (done) { | ||||||
|             mMission.notifyFinished(); |             mMission.notifyFinished(); | ||||||
|         } else { |         } else { | ||||||
|             savePosition(start); |             mMission.fallbackResumeOffset = start; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,17 +2,17 @@ package us.shandian.giga.get; | |||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
| public class FinishedMission extends  Mission { | public class FinishedMission extends Mission { | ||||||
|  |  | ||||||
|     public FinishedMission() { |     public FinishedMission() { | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public FinishedMission(@NonNull DownloadMission mission) { |     public FinishedMission(@NonNull DownloadMission mission) { | ||||||
|         source = mission.source; |         source = mission.source; | ||||||
|         length = mission.length;// ¿or mission.done? |         length = mission.length; | ||||||
|         timestamp = mission.timestamp; |         timestamp = mission.timestamp; | ||||||
|         kind = mission.kind; |         kind = mission.kind; | ||||||
|         storage = mission.storage; |         storage = mission.storage; | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										115
									
								
								app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,115 @@ | |||||||
|  | package us.shandian.giga.get; | ||||||
|  |  | ||||||
|  | import android.os.Parcel; | ||||||
|  | import android.os.Parcelable; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.extractor.MediaFormat; | ||||||
|  | import org.schabi.newpipe.extractor.stream.AudioStream; | ||||||
|  | import org.schabi.newpipe.extractor.stream.Stream; | ||||||
|  | import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||||
|  | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
|  |  | ||||||
|  | import java.io.Serializable; | ||||||
|  |  | ||||||
|  | public class MissionRecoveryInfo implements Serializable, Parcelable { | ||||||
|  |     private static final long serialVersionUID = 0L; | ||||||
|  |  | ||||||
|  |     MediaFormat format; | ||||||
|  |     String desired; | ||||||
|  |     boolean desired2; | ||||||
|  |     int desiredBitrate; | ||||||
|  |     byte kind; | ||||||
|  |     String validateCondition = null; | ||||||
|  |  | ||||||
|  |     public MissionRecoveryInfo(@NonNull Stream stream) { | ||||||
|  |         if (stream instanceof AudioStream) { | ||||||
|  |             desiredBitrate = ((AudioStream) stream).average_bitrate; | ||||||
|  |             desired2 = false; | ||||||
|  |             kind = 'a'; | ||||||
|  |         } else if (stream instanceof VideoStream) { | ||||||
|  |             desired = ((VideoStream) stream).getResolution(); | ||||||
|  |             desired2 = ((VideoStream) stream).isVideoOnly(); | ||||||
|  |             kind = 'v'; | ||||||
|  |         } else if (stream instanceof SubtitlesStream) { | ||||||
|  |             desired = ((SubtitlesStream) stream).getLanguageTag(); | ||||||
|  |             desired2 = ((SubtitlesStream) stream).isAutoGenerated(); | ||||||
|  |             kind = 's'; | ||||||
|  |         } else { | ||||||
|  |             throw new RuntimeException("Unknown stream kind"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         format = stream.getFormat(); | ||||||
|  |         if (format == null) throw new NullPointerException("Stream format cannot be null"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public String toString() { | ||||||
|  |         String info; | ||||||
|  |         StringBuilder str = new StringBuilder(); | ||||||
|  |         str.append("{type="); | ||||||
|  |         switch (kind) { | ||||||
|  |             case 'a': | ||||||
|  |                 str.append("audio"); | ||||||
|  |                 info = "bitrate=" + desiredBitrate; | ||||||
|  |                 break; | ||||||
|  |             case 'v': | ||||||
|  |                 str.append("video"); | ||||||
|  |                 info = "quality=" + desired + " videoOnly=" + desired2; | ||||||
|  |                 break; | ||||||
|  |             case 's': | ||||||
|  |                 str.append("subtitles"); | ||||||
|  |                 info = "language=" + desired + " autoGenerated=" + desired2; | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 info = ""; | ||||||
|  |                 str.append("other"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         str.append(" format=") | ||||||
|  |                 .append(format.getName()) | ||||||
|  |                 .append(' ') | ||||||
|  |                 .append(info) | ||||||
|  |                 .append('}'); | ||||||
|  |  | ||||||
|  |         return str.toString(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int describeContents() { | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void writeToParcel(Parcel parcel, int flags) { | ||||||
|  |         parcel.writeInt(this.format.ordinal()); | ||||||
|  |         parcel.writeString(this.desired); | ||||||
|  |         parcel.writeInt(this.desired2 ? 0x01 : 0x00); | ||||||
|  |         parcel.writeInt(this.desiredBitrate); | ||||||
|  |         parcel.writeByte(this.kind); | ||||||
|  |         parcel.writeString(this.validateCondition); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private MissionRecoveryInfo(Parcel parcel) { | ||||||
|  |         this.format = MediaFormat.values()[parcel.readInt()]; | ||||||
|  |         this.desired = parcel.readString(); | ||||||
|  |         this.desired2 = parcel.readInt() != 0x00; | ||||||
|  |         this.desiredBitrate = parcel.readInt(); | ||||||
|  |         this.kind = parcel.readByte(); | ||||||
|  |         this.validateCondition = parcel.readString(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static final Parcelable.Creator<MissionRecoveryInfo> CREATOR = new Parcelable.Creator<MissionRecoveryInfo>() { | ||||||
|  |         @Override | ||||||
|  |         public MissionRecoveryInfo createFromParcel(Parcel source) { | ||||||
|  |             return new MissionRecoveryInfo(source); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public MissionRecoveryInfo[] newArray(int size) { | ||||||
|  |             return new MissionRecoveryInfo[size]; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | } | ||||||
| @@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream; | |||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  |  | ||||||
| public class ChunkFileInputStream extends SharpStream { | public class ChunkFileInputStream extends SharpStream { | ||||||
|  |     private static final int REPORT_INTERVAL = 256 * 1024; | ||||||
|  |  | ||||||
|     private SharpStream source; |     private SharpStream source; | ||||||
|     private final long offset; |     private final long offset; | ||||||
|     private final long length; |     private final long length; | ||||||
|     private long position; |     private long position; | ||||||
|  |  | ||||||
|     public ChunkFileInputStream(SharpStream target, long start) throws IOException { |     private long progressReport; | ||||||
|         this(target, start, target.length()); |     private final ProgressReport onProgress; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException { |     public ChunkFileInputStream(SharpStream target, long start, long end, ProgressReport callback) throws IOException { | ||||||
|         source = target; |         source = target; | ||||||
|         offset = start; |         offset = start; | ||||||
|         length = end - start; |         length = end - start; | ||||||
|         position = 0; |         position = 0; | ||||||
|  |         onProgress = callback; | ||||||
|  |         progressReport = REPORT_INTERVAL; | ||||||
|  |  | ||||||
|         if (length < 1) { |         if (length < 1) { | ||||||
|             source.close(); |             source.close(); | ||||||
| @@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public int read(byte b[]) throws IOException { |     public int read(byte[] b) throws IOException { | ||||||
|         return read(b, 0, b.length); |         return read(b, 0, b.length); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public int read(byte b[], int off, int len) throws IOException { |     public int read(byte[] b, int off, int len) throws IOException { | ||||||
|         if ((position + len) > length) { |         if ((position + len) > length) { | ||||||
|             len = (int) (length - position); |             len = (int) (length - position); | ||||||
|         } |         } | ||||||
| @@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream { | |||||||
|         int res = source.read(b, off, len); |         int res = source.read(b, off, len); | ||||||
|         position += res; |         position += res; | ||||||
|  |  | ||||||
|  |         if (onProgress != null && position > progressReport) { | ||||||
|  |             onProgress.report(position); | ||||||
|  |             progressReport = position + REPORT_INTERVAL; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return res; |         return res; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void write(byte b[]) throws IOException { |     public void write(byte[] b) throws IOException { | ||||||
|         write(b, 0, b.length); |         write(b, 0, b.length); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void write(byte b[], int off, int len) throws IOException { |     public void write(byte[] b, int off, int len) throws IOException { | ||||||
|         if (len == 0) { |         if (len == 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|     @Override |     @Override | ||||||
|     public void rewind() throws IOException { |     public void rewind() throws IOException { | ||||||
|         if (onProgress != null) { |         if (onProgress != null) { | ||||||
|             onProgress.report(-out.length - aux.length);// rollback the whole progress |             onProgress.report(0);// rollback the whole progress | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         seek(0); |         seek(0); | ||||||
| @@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream { | |||||||
|         long check(); |         long check(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public interface ProgressReport { |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Report the size of the new file |  | ||||||
|          * |  | ||||||
|          * @param progress the new size |  | ||||||
|          */ |  | ||||||
|         void report(long progress); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public interface WriteErrorHandle { |     public interface WriteErrorHandle { | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
| @@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream { | |||||||
|  |  | ||||||
|     class BufferedFile { |     class BufferedFile { | ||||||
|  |  | ||||||
|         protected final SharpStream target; |         final SharpStream target; | ||||||
|  |  | ||||||
|         private long offset; |         private long offset; | ||||||
|         protected long length; |         long length; | ||||||
|  |  | ||||||
|         private byte[] queue = new byte[QUEUE_BUFFER_SIZE]; |         private byte[] queue = new byte[QUEUE_BUFFER_SIZE]; | ||||||
|         private int queueSize; |         private int queueSize; | ||||||
| @@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             this.target = target; |             this.target = target; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         protected long getOffset() { |         long getOffset() { | ||||||
|             return offset + queueSize;// absolute offset in the file |             return offset + queueSize;// absolute offset in the file | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         protected void close() { |         void close() { | ||||||
|             queue = null; |             queue = null; | ||||||
|             target.close(); |             target.close(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         protected void write(byte b[], int off, int len) throws IOException { |         void write(byte[] b, int off, int len) throws IOException { | ||||||
|             while (len > 0) { |             while (len > 0) { | ||||||
|                 // if the queue is full, the method available() will flush the queue |                 // if the queue is full, the method available() will flush the queue | ||||||
|                 int read = Math.min(available(), len); |                 int read = Math.min(available(), len); | ||||||
| @@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             target.seek(0); |             target.seek(0); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         protected int available() throws IOException { |         int available() throws IOException { | ||||||
|             if (queueSize >= queue.length) { |             if (queueSize >= queue.length) { | ||||||
|                 flush(); |                 flush(); | ||||||
|                 return queue.length; |                 return queue.length; | ||||||
| @@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             target.seek(0); |             target.seek(0); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         protected void seek(long absoluteOffset) throws IOException { |         void seek(long absoluteOffset) throws IOException { | ||||||
|             if (absoluteOffset == offset) { |             if (absoluteOffset == offset) { | ||||||
|                 return;// nothing to do |                 return;// nothing to do | ||||||
|             } |             } | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								app/src/main/java/us/shandian/giga/io/ProgressReport.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | package us.shandian.giga.io; | ||||||
|  |  | ||||||
|  | public interface ProgressReport { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Report the size of the new file | ||||||
|  |      * | ||||||
|  |      * @param progress the new size | ||||||
|  |      */ | ||||||
|  |     void report(long progress); | ||||||
|  | } | ||||||
| @@ -0,0 +1,44 @@ | |||||||
|  | package us.shandian.giga.postprocessing; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.streams.OggFromWebMWriter; | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.nio.ByteBuffer; | ||||||
|  |  | ||||||
|  | class OggFromWebmDemuxer extends Postprocessing { | ||||||
|  |  | ||||||
|  |     OggFromWebmDemuxer() { | ||||||
|  |         super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     boolean test(SharpStream... sources) throws IOException { | ||||||
|  |         ByteBuffer buffer = ByteBuffer.allocate(4); | ||||||
|  |         sources[0].read(buffer.array()); | ||||||
|  |  | ||||||
|  |         // youtube uses WebM as container, but the file extension (format suffix) is "*.opus" | ||||||
|  |         // check if the file is a webm/mkv file before proceed | ||||||
|  |  | ||||||
|  |         switch (buffer.getInt()) { | ||||||
|  |             case 0x1a45dfa3: | ||||||
|  |                 return true;// webm/mkv | ||||||
|  |             case 0x4F676753: | ||||||
|  |                 return false;// ogg | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     int process(SharpStream out, @NonNull SharpStream... sources) throws IOException { | ||||||
|  |         OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out); | ||||||
|  |         demuxer.parseSource(); | ||||||
|  |         demuxer.selectTrack(0); | ||||||
|  |         demuxer.build(); | ||||||
|  |  | ||||||
|  |         return OK_RESULT; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| package us.shandian.giga.postprocessing; | package us.shandian.giga.postprocessing; | ||||||
|  |  | ||||||
| import android.os.Message; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| @@ -14,11 +14,11 @@ import us.shandian.giga.get.DownloadMission; | |||||||
| import us.shandian.giga.io.ChunkFileInputStream; | import us.shandian.giga.io.ChunkFileInputStream; | ||||||
| import us.shandian.giga.io.CircularFileWriter; | import us.shandian.giga.io.CircularFileWriter; | ||||||
| import us.shandian.giga.io.CircularFileWriter.OffsetChecker; | import us.shandian.giga.io.CircularFileWriter.OffsetChecker; | ||||||
| import us.shandian.giga.service.DownloadManagerService; | import us.shandian.giga.io.ProgressReport; | ||||||
|  |  | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; | import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; | ||||||
|  | import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; | import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; |  | ||||||
|  |  | ||||||
| public abstract class Postprocessing implements Serializable { | public abstract class Postprocessing implements Serializable { | ||||||
|  |  | ||||||
| @@ -28,6 +28,7 @@ public abstract class Postprocessing implements Serializable { | |||||||
|     public transient static final String ALGORITHM_WEBM_MUXER = "webm"; |     public transient static final String ALGORITHM_WEBM_MUXER = "webm"; | ||||||
|     public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; |     public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; | ||||||
|     public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; |     public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; | ||||||
|  |     public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d"; | ||||||
|  |  | ||||||
|     public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) { |     public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) { | ||||||
|         Postprocessing instance; |         Postprocessing instance; | ||||||
| @@ -45,6 +46,9 @@ public abstract class Postprocessing implements Serializable { | |||||||
|             case ALGORITHM_M4A_NO_DASH: |             case ALGORITHM_M4A_NO_DASH: | ||||||
|                 instance = new M4aNoDash(); |                 instance = new M4aNoDash(); | ||||||
|                 break; |                 break; | ||||||
|  |             case ALGORITHM_OGG_FROM_WEBM_DEMUXER: | ||||||
|  |                 instance = new OggFromWebmDemuxer(); | ||||||
|  |                 break; | ||||||
|             /*case "example-algorithm": |             /*case "example-algorithm": | ||||||
|                 instance = new ExampleAlgorithm();*/ |                 instance = new ExampleAlgorithm();*/ | ||||||
|             default: |             default: | ||||||
| @@ -59,22 +63,22 @@ public abstract class Postprocessing implements Serializable { | |||||||
|      * Get a boolean value that indicate if the given algorithm work on the same |      * Get a boolean value that indicate if the given algorithm work on the same | ||||||
|      * file |      * file | ||||||
|      */ |      */ | ||||||
|     public final boolean worksOnSameFile; |     public boolean worksOnSameFile; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Indicates whether the selected algorithm needs space reserved at the beginning of the file |      * Indicates whether the selected algorithm needs space reserved at the beginning of the file | ||||||
|      */ |      */ | ||||||
|     public final boolean reserveSpace; |     public boolean reserveSpace; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Gets the given algorithm short name |      * Gets the given algorithm short name | ||||||
|      */ |      */ | ||||||
|     private final String name; |     private String name; | ||||||
|  |  | ||||||
|  |  | ||||||
|     private String[] args; |     private String[] args; | ||||||
|  |  | ||||||
|     protected transient DownloadMission mission; |     private transient DownloadMission mission; | ||||||
|  |  | ||||||
|     private File tempFile; |     private File tempFile; | ||||||
|  |  | ||||||
| @@ -105,16 +109,24 @@ public abstract class Postprocessing implements Serializable { | |||||||
|         long finalLength = -1; |         long finalLength = -1; | ||||||
|  |  | ||||||
|         mission.done = 0; |         mission.done = 0; | ||||||
|         mission.length = mission.storage.length(); |  | ||||||
|  |         long length = mission.storage.length() - mission.offsets[0]; | ||||||
|  |         mission.length = length > mission.nearLength ? length : mission.nearLength; | ||||||
|  |  | ||||||
|  |         final ProgressReport readProgress = (long position) -> { | ||||||
|  |             position -= mission.offsets[0]; | ||||||
|  |             if (position > mission.done) mission.done = position; | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         if (worksOnSameFile) { |         if (worksOnSameFile) { | ||||||
|             ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; |             ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; | ||||||
|             try { |             try { | ||||||
|                 int i = 0; |                 for (int i = 0, j = 1; i < sources.length; i++, j++) { | ||||||
|                 for (; i < sources.length - 1; i++) { |                     SharpStream source = mission.storage.getStream(); | ||||||
|                     sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); |                     long end = j < sources.length ? mission.offsets[j] : source.length(); | ||||||
|  |  | ||||||
|  |                     sources[i] = new ChunkFileInputStream(source, mission.offsets[i], end, readProgress); | ||||||
|                 } |                 } | ||||||
|                 sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); |  | ||||||
|  |  | ||||||
|                 if (test(sources)) { |                 if (test(sources)) { | ||||||
|                     for (SharpStream source : sources) source.rewind(); |                     for (SharpStream source : sources) source.rewind(); | ||||||
| @@ -136,7 +148,7 @@ public abstract class Postprocessing implements Serializable { | |||||||
|                     }; |                     }; | ||||||
|  |  | ||||||
|                     out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker); |                     out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker); | ||||||
|                     out.onProgress = this::progressReport; |                     out.onProgress = (long position) -> mission.done = position; | ||||||
|  |  | ||||||
|                     out.onWriteError = (err) -> { |                     out.onWriteError = (err) -> { | ||||||
|                         mission.psState = 3; |                         mission.psState = 3; | ||||||
| @@ -183,11 +195,10 @@ public abstract class Postprocessing implements Serializable { | |||||||
|  |  | ||||||
|         if (result == OK_RESULT) { |         if (result == OK_RESULT) { | ||||||
|             if (finalLength != -1) { |             if (finalLength != -1) { | ||||||
|                 mission.done = finalLength; |  | ||||||
|                 mission.length = finalLength; |                 mission.length = finalLength; | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             mission.errCode = ERROR_UNKNOWN_EXCEPTION; |             mission.errCode = ERROR_POSTPROCESSING; | ||||||
|             mission.errObject = new RuntimeException("post-processing algorithm returned " + result); |             mission.errObject = new RuntimeException("post-processing algorithm returned " + result); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -212,7 +223,7 @@ public abstract class Postprocessing implements Serializable { | |||||||
|      * |      * | ||||||
|      * @param out     output stream |      * @param out     output stream | ||||||
|      * @param sources files to be processed |      * @param sources files to be processed | ||||||
|      * @return a error code, 0 means the operation was successful |      * @return an error code, {@code OK_RESULT} means the operation was successful | ||||||
|      * @throws IOException if an I/O error occurs. |      * @throws IOException if an I/O error occurs. | ||||||
|      */ |      */ | ||||||
|     abstract int process(SharpStream out, SharpStream... sources) throws IOException; |     abstract int process(SharpStream out, SharpStream... sources) throws IOException; | ||||||
| @@ -225,23 +236,12 @@ public abstract class Postprocessing implements Serializable { | |||||||
|         return args[index]; |         return args[index]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void progressReport(long done) { |  | ||||||
|         mission.done = done; |  | ||||||
|         if (mission.length < mission.done) mission.length = mission.done; |  | ||||||
|  |  | ||||||
|         Message m = new Message(); |  | ||||||
|         m.what = DownloadManagerService.MESSAGE_PROGRESS; |  | ||||||
|         m.obj = mission; |  | ||||||
|  |  | ||||||
|         mission.mHandler.sendMessage(m); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public String toString() { |     public String toString() { | ||||||
|         StringBuilder str = new StringBuilder(); |         StringBuilder str = new StringBuilder(); | ||||||
|  |  | ||||||
|         str.append("name=").append(name).append('['); |         str.append("{ name=").append(name).append('['); | ||||||
|  |  | ||||||
|         if (args != null) { |         if (args != null) { | ||||||
|             for (String arg : args) { |             for (String arg : args) { | ||||||
| @@ -251,6 +251,6 @@ public abstract class Postprocessing implements Serializable { | |||||||
|             str.delete(0, 1); |             str.delete(0, 1); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return str.append(']').toString(); |         return str.append("] }").toString(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,13 +2,11 @@ package us.shandian.giga.service; | |||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
|  | import android.util.Log; | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.recyclerview.widget.DiffUtil; | import androidx.recyclerview.widget.DiffUtil; | ||||||
| import android.util.Log; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; |  | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| @@ -37,6 +35,7 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|     public static final String TAG_AUDIO = "audio"; |     public static final String TAG_AUDIO = "audio"; | ||||||
|     public static final String TAG_VIDEO = "video"; |     public static final String TAG_VIDEO = "video"; | ||||||
|  |     private static final String DOWNLOADS_METADATA_FOLDER = "pending_downloads"; | ||||||
|  |  | ||||||
|     private final FinishedMissionStore mFinishedMissionStore; |     private final FinishedMissionStore mFinishedMissionStore; | ||||||
|  |  | ||||||
| @@ -74,25 +73,35 @@ public class DownloadManager { | |||||||
|         mMissionsFinished = loadFinishedMissions(); |         mMissionsFinished = loadFinishedMissions(); | ||||||
|         mPendingMissionsDir = getPendingDir(context); |         mPendingMissionsDir = getPendingDir(context); | ||||||
|  |  | ||||||
|         if (!Utility.mkdir(mPendingMissionsDir, false)) { |  | ||||||
|             throw new RuntimeException("failed to create pending_downloads in data directory"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         loadPendingMissions(context); |         loadPendingMissions(context); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static File getPendingDir(@NonNull Context context) { |     private static File getPendingDir(@NonNull Context context) { | ||||||
|         //File dir = new File(ContextCompat.getDataDir(context), "pending_downloads"); |         File dir = context.getExternalFilesDir(DOWNLOADS_METADATA_FOLDER); | ||||||
|         File dir = context.getExternalFilesDir("pending_downloads"); |         if (testDir(dir)) return dir; | ||||||
|  |  | ||||||
|         if (dir == null) { |         dir = new File(context.getFilesDir(), DOWNLOADS_METADATA_FOLDER); | ||||||
|             // One of the following paths are not accessible ¿unmounted internal memory? |         if (testDir(dir)) return dir; | ||||||
|             //        /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads |  | ||||||
|             //        /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads |         throw new RuntimeException("path to pending downloads are not accessible"); | ||||||
|             Log.w(TAG, "path to pending downloads are not accessible"); |     } | ||||||
|  |  | ||||||
|  |     private static boolean testDir(@Nullable File dir) { | ||||||
|  |         if (dir == null) return false; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             if (!Utility.mkdir(dir, false)) { | ||||||
|  |                 Log.e(TAG, "testDir() cannot create the directory in path: " + dir.getAbsolutePath()); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             File tmp = new File(dir, ".tmp"); | ||||||
|  |             if (!tmp.createNewFile()) return false; | ||||||
|  |             return tmp.delete();// if the file was created, SHOULD BE deleted too | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.e(TAG, "testDir() failed: " + dir.getAbsolutePath(), e); | ||||||
|  |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return dir; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -132,6 +141,7 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|         for (File sub : subs) { |         for (File sub : subs) { | ||||||
|             if (!sub.isFile()) continue; |             if (!sub.isFile()) continue; | ||||||
|  |             if (sub.getName().equals(".tmp")) continue; | ||||||
|  |  | ||||||
|             DownloadMission mis = Utility.readFromFile(sub); |             DownloadMission mis = Utility.readFromFile(sub); | ||||||
|             if (mis == null || mis.isFinished()) { |             if (mis == null || mis.isFinished()) { | ||||||
| @@ -140,6 +150,8 @@ public class DownloadManager { | |||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             mis.threads = new Thread[0]; | ||||||
|  |  | ||||||
|             boolean exists; |             boolean exists; | ||||||
|             try { |             try { | ||||||
|                 mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); |                 mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); | ||||||
| @@ -158,8 +170,6 @@ public class DownloadManager { | |||||||
|                     // is Java IO (avoid showing the "Save as..." dialog) |                     // is Java IO (avoid showing the "Save as..." dialog) | ||||||
|                     if (exists && mis.storage.isDirect() && !mis.storage.delete()) |                     if (exists && mis.storage.isDirect() && !mis.storage.delete()) | ||||||
|                         Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); |                         Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); | ||||||
|  |  | ||||||
|                     exists = true; |  | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 mis.psState = 0; |                 mis.psState = 0; | ||||||
| @@ -177,7 +187,6 @@ public class DownloadManager { | |||||||
|                 mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx)); |                 mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx)); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             mis.recovered = exists; |  | ||||||
|             mis.metadata = sub; |             mis.metadata = sub; | ||||||
|             mis.maxRetry = mPrefMaxRetry; |             mis.maxRetry = mPrefMaxRetry; | ||||||
|             mis.mHandler = mHandler; |             mis.mHandler = mHandler; | ||||||
| @@ -232,7 +241,6 @@ public class DownloadManager { | |||||||
|             boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; |             boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; | ||||||
|  |  | ||||||
|             if (canDownloadInCurrentNetwork() && start) { |             if (canDownloadInCurrentNetwork() && start) { | ||||||
|                 mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); |  | ||||||
|                 mission.start(); |                 mission.start(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -241,7 +249,6 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|     public void resumeMission(DownloadMission mission) { |     public void resumeMission(DownloadMission mission) { | ||||||
|         if (!mission.running) { |         if (!mission.running) { | ||||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); |  | ||||||
|             mission.start(); |             mission.start(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -250,7 +257,6 @@ public class DownloadManager { | |||||||
|         if (mission.running) { |         if (mission.running) { | ||||||
|             mission.setEnqueued(false); |             mission.setEnqueued(false); | ||||||
|             mission.pause(); |             mission.pause(); | ||||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -263,7 +269,6 @@ public class DownloadManager { | |||||||
|                 mFinishedMissionStore.deleteMission(mission); |                 mFinishedMissionStore.deleteMission(mission); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); |  | ||||||
|             mission.delete(); |             mission.delete(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -280,7 +285,6 @@ public class DownloadManager { | |||||||
|                 mFinishedMissionStore.deleteMission(mission); |                 mFinishedMissionStore.deleteMission(mission); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); |  | ||||||
|             mission.storage = null; |             mission.storage = null; | ||||||
|             mission.delete(); |             mission.delete(); | ||||||
|         } |         } | ||||||
| @@ -363,35 +367,29 @@ public class DownloadManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void pauseAllMissions(boolean force) { |     public void pauseAllMissions(boolean force) { | ||||||
|         boolean flag = false; |  | ||||||
|  |  | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             for (DownloadMission mission : mMissionsPending) { |             for (DownloadMission mission : mMissionsPending) { | ||||||
|                 if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue; |                 if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue; | ||||||
|  |  | ||||||
|                 if (force) mission.threads = null;// avoid waiting for threads |                 if (force) { | ||||||
|  |                     // avoid waiting for threads | ||||||
|  |                     mission.init = null; | ||||||
|  |                     mission.threads = new Thread[0]; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 mission.pause(); |                 mission.pause(); | ||||||
|                 flag = true; |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void startAllMissions() { |     public void startAllMissions() { | ||||||
|         boolean flag = false; |  | ||||||
|  |  | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             for (DownloadMission mission : mMissionsPending) { |             for (DownloadMission mission : mMissionsPending) { | ||||||
|                 if (mission.running || mission.isCorrupt()) continue; |                 if (mission.running || mission.isCorrupt()) continue; | ||||||
|  |  | ||||||
|                 flag = true; |  | ||||||
|                 mission.start(); |                 mission.start(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -472,28 +470,18 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|         boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating; |         boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating; | ||||||
|  |  | ||||||
|         int running = 0; |  | ||||||
|         int paused = 0; |  | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             for (DownloadMission mission : mMissionsPending) { |             for (DownloadMission mission : mMissionsPending) { | ||||||
|                 if (mission.isCorrupt() || mission.isPsRunning()) continue; |                 if (mission.isCorrupt() || mission.isPsRunning()) continue; | ||||||
|  |  | ||||||
|                 if (mission.running && isMetered) { |                 if (mission.running && isMetered) { | ||||||
|                     paused++; |  | ||||||
|                     mission.pause(); |                     mission.pause(); | ||||||
|                 } else if (!mission.running && !isMetered && mission.enqueued) { |                 } else if (!mission.running && !isMetered && mission.enqueued) { | ||||||
|                     running++; |  | ||||||
|                     mission.start(); |                     mission.start(); | ||||||
|                     if (mPrefQueueLimit) break; |                     if (mPrefQueueLimit) break; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (running > 0) { |  | ||||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     void updateMaximumAttempts() { |     void updateMaximumAttempts() { | ||||||
| @@ -502,22 +490,6 @@ public class DownloadManager { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Fast check for pending downloads. If exists, the user will be notified |  | ||||||
|      * TODO: call this method in somewhere |  | ||||||
|      * |  | ||||||
|      * @param context the application context |  | ||||||
|      */ |  | ||||||
|     public static void notifyUserPendingDownloads(Context context) { |  | ||||||
|         int pending = getPendingDir(context).list().length; |  | ||||||
|         if (pending < 1) return; |  | ||||||
|  |  | ||||||
|         Toast.makeText(context, context.getString( |  | ||||||
|                 R.string.msg_pending_downloads, |  | ||||||
|                 String.valueOf(pending) |  | ||||||
|         ), Toast.LENGTH_LONG).show(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public MissionState checkForExistingMission(StoredFileHelper storage) { |     public MissionState checkForExistingMission(StoredFileHelper storage) { | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             DownloadMission pending = getPendingMission(storage); |             DownloadMission pending = getPendingMission(storage); | ||||||
|   | |||||||
| @@ -23,15 +23,17 @@ import android.os.Handler; | |||||||
| import android.os.Handler.Callback; | import android.os.Handler.Callback; | ||||||
| import android.os.IBinder; | import android.os.IBinder; | ||||||
| import android.os.Message; | import android.os.Message; | ||||||
|  | import android.os.Parcelable; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.util.SparseArray; | ||||||
|  | import android.widget.Toast; | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.annotation.StringRes; | import androidx.annotation.StringRes; | ||||||
| import androidx.core.app.NotificationCompat; | import androidx.core.app.NotificationCompat; | ||||||
| import androidx.core.app.NotificationCompat.Builder; | import androidx.core.app.NotificationCompat.Builder; | ||||||
| import android.util.Log; |  | ||||||
| import android.util.SparseArray; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.download.DownloadActivity; | import org.schabi.newpipe.download.DownloadActivity; | ||||||
| @@ -42,6 +44,7 @@ import java.io.IOException; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; | import us.shandian.giga.get.DownloadMission; | ||||||
|  | import us.shandian.giga.get.MissionRecoveryInfo; | ||||||
| import us.shandian.giga.io.StoredDirectoryHelper; | import us.shandian.giga.io.StoredDirectoryHelper; | ||||||
| import us.shandian.giga.io.StoredFileHelper; | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.postprocessing.Postprocessing; | import us.shandian.giga.postprocessing.Postprocessing; | ||||||
| @@ -54,11 +57,11 @@ public class DownloadManagerService extends Service { | |||||||
|  |  | ||||||
|     private static final String TAG = "DownloadManagerService"; |     private static final String TAG = "DownloadManagerService"; | ||||||
|  |  | ||||||
|  |     public static final int MESSAGE_RUNNING = 0; | ||||||
|     public static final int MESSAGE_PAUSED = 1; |     public static final int MESSAGE_PAUSED = 1; | ||||||
|     public static final int MESSAGE_FINISHED = 2; |     public static final int MESSAGE_FINISHED = 2; | ||||||
|     public static final int MESSAGE_PROGRESS = 3; |     public static final int MESSAGE_ERROR = 3; | ||||||
|     public static final int MESSAGE_ERROR = 4; |     public static final int MESSAGE_DELETED = 4; | ||||||
|     public static final int MESSAGE_DELETED = 5; |  | ||||||
|  |  | ||||||
|     private static final int FOREGROUND_NOTIFICATION_ID = 1000; |     private static final int FOREGROUND_NOTIFICATION_ID = 1000; | ||||||
|     private static final int DOWNLOADS_NOTIFICATION_ID = 1001; |     private static final int DOWNLOADS_NOTIFICATION_ID = 1001; | ||||||
| @@ -73,6 +76,7 @@ public class DownloadManagerService extends Service { | |||||||
|     private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath"; |     private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath"; | ||||||
|     private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; |     private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; | ||||||
|     private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; |     private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; | ||||||
|  |     private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo"; | ||||||
|  |  | ||||||
|     private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; |     private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; | ||||||
|     private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; |     private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; | ||||||
| @@ -212,9 +216,11 @@ public class DownloadManagerService extends Service { | |||||||
|                             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |                             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
|  |                 return START_NOT_STICKY; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return START_NOT_STICKY; |  | ||||||
|  |         return START_STICKY; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -245,6 +251,7 @@ public class DownloadManagerService extends Service { | |||||||
|         if (icDownloadFailed != null) icDownloadFailed.recycle(); |         if (icDownloadFailed != null) icDownloadFailed.recycle(); | ||||||
|         if (icLauncher != null) icLauncher.recycle(); |         if (icLauncher != null) icLauncher.recycle(); | ||||||
|  |  | ||||||
|  |         mHandler = null; | ||||||
|         mManager.pauseAllMissions(true); |         mManager.pauseAllMissions(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -269,6 +276,8 @@ public class DownloadManagerService extends Service { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean handleMessage(@NonNull Message msg) { |     private boolean handleMessage(@NonNull Message msg) { | ||||||
|  |         if (mHandler == null) return true; | ||||||
|  |  | ||||||
|         DownloadMission mission = (DownloadMission) msg.obj; |         DownloadMission mission = (DownloadMission) msg.obj; | ||||||
|  |  | ||||||
|         switch (msg.what) { |         switch (msg.what) { | ||||||
| @@ -279,7 +288,7 @@ public class DownloadManagerService extends Service { | |||||||
|                 handleConnectivityState(false); |                 handleConnectivityState(false); | ||||||
|                 updateForegroundState(mManager.runMissions()); |                 updateForegroundState(mManager.runMissions()); | ||||||
|                 break; |                 break; | ||||||
|             case MESSAGE_PROGRESS: |             case MESSAGE_RUNNING: | ||||||
|                 updateForegroundState(true); |                 updateForegroundState(true); | ||||||
|                 break; |                 break; | ||||||
|             case MESSAGE_ERROR: |             case MESSAGE_ERROR: | ||||||
| @@ -295,11 +304,8 @@ public class DownloadManagerService extends Service { | |||||||
|         if (msg.what != MESSAGE_ERROR) |         if (msg.what != MESSAGE_ERROR) | ||||||
|             mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission)); |             mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission)); | ||||||
|  |  | ||||||
|         synchronized (mEchoObservers) { |         for (Callback observer : mEchoObservers) | ||||||
|             for (Callback observer : mEchoObservers) { |             observer.handleMessage(msg); | ||||||
|                 observer.handleMessage(msg); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| @@ -364,18 +370,20 @@ public class DownloadManagerService extends Service { | |||||||
|     /** |     /** | ||||||
|      * Start a new download mission |      * Start a new download mission | ||||||
|      * |      * | ||||||
|      * @param context    the activity context |      * @param context      the activity context | ||||||
|      * @param urls       the list of urls to download |      * @param urls         array of urls to download | ||||||
|      * @param storage    where the file is saved |      * @param storage      where the file is saved | ||||||
|      * @param kind       type of file (a: audio  v: video  s: subtitle ?: file-extension defined) |      * @param kind         type of file (a: audio  v: video  s: subtitle ?: file-extension defined) | ||||||
|      * @param threads    the number of threads maximal used to download chunks of the file. |      * @param threads      the number of threads maximal used to download chunks of the file. | ||||||
|      * @param psName     the name of the required post-processing algorithm, or {@code null} to ignore. |      * @param psName       the name of the required post-processing algorithm, or {@code null} to ignore. | ||||||
|      * @param source     source url of the resource |      * @param source       source url of the resource | ||||||
|      * @param psArgs     the arguments for the post-processing algorithm. |      * @param psArgs       the arguments for the post-processing algorithm. | ||||||
|      * @param nearLength the approximated final length of the file |      * @param nearLength   the approximated final length of the file | ||||||
|  |      * @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download | ||||||
|      */ |      */ | ||||||
|     public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind, |     public static void startMission(Context context, String[] urls, StoredFileHelper storage, | ||||||
|                                     int threads, String source, String psName, String[] psArgs, long nearLength) { |                                     char kind, int threads, String source, String psName, | ||||||
|  |                                     String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) { | ||||||
|         Intent intent = new Intent(context, DownloadManagerService.class); |         Intent intent = new Intent(context, DownloadManagerService.class); | ||||||
|         intent.setAction(Intent.ACTION_RUN); |         intent.setAction(Intent.ACTION_RUN); | ||||||
|         intent.putExtra(EXTRA_URLS, urls); |         intent.putExtra(EXTRA_URLS, urls); | ||||||
| @@ -385,6 +393,7 @@ public class DownloadManagerService extends Service { | |||||||
|         intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); |         intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); | ||||||
|         intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); |         intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); | ||||||
|         intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); |         intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); | ||||||
|  |         intent.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo); | ||||||
|  |  | ||||||
|         intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); |         intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); | ||||||
|         intent.putExtra(EXTRA_PATH, storage.getUri()); |         intent.putExtra(EXTRA_PATH, storage.getUri()); | ||||||
| @@ -404,6 +413,7 @@ public class DownloadManagerService extends Service { | |||||||
|         String source = intent.getStringExtra(EXTRA_SOURCE); |         String source = intent.getStringExtra(EXTRA_SOURCE); | ||||||
|         long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); |         long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); | ||||||
|         String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); |         String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); | ||||||
|  |         Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO); | ||||||
|  |  | ||||||
|         StoredFileHelper storage; |         StoredFileHelper storage; | ||||||
|         try { |         try { | ||||||
| @@ -418,10 +428,15 @@ public class DownloadManagerService extends Service { | |||||||
|         else |         else | ||||||
|             ps = Postprocessing.getAlgorithm(psName, psArgs); |             ps = Postprocessing.getAlgorithm(psName, psArgs); | ||||||
|  |  | ||||||
|  |         MissionRecoveryInfo[] recovery = new MissionRecoveryInfo[parcelRecovery.length]; | ||||||
|  |         for (int i = 0; i < parcelRecovery.length; i++) | ||||||
|  |             recovery[i] = (MissionRecoveryInfo) parcelRecovery[i]; | ||||||
|  |  | ||||||
|         final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); |         final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); | ||||||
|         mission.threadCount = threads; |         mission.threadCount = threads; | ||||||
|         mission.source = source; |         mission.source = source; | ||||||
|         mission.nearLength = nearLength; |         mission.nearLength = nearLength; | ||||||
|  |         mission.recoveryInfo = recovery; | ||||||
|  |  | ||||||
|         if (ps != null) |         if (ps != null) | ||||||
|             ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this)); |             ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this)); | ||||||
| @@ -509,16 +524,6 @@ public class DownloadManagerService extends Service { | |||||||
|         return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); |         return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void manageObservers(Callback handler, boolean add) { |  | ||||||
|         synchronized (mEchoObservers) { |  | ||||||
|             if (add) { |  | ||||||
|                 mEchoObservers.add(handler); |  | ||||||
|             } else { |  | ||||||
|                 mEchoObservers.remove(handler); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void manageLock(boolean acquire) { |     private void manageLock(boolean acquire) { | ||||||
|         if (acquire == mLockAcquired) return; |         if (acquire == mLockAcquired) return; | ||||||
|  |  | ||||||
| @@ -591,11 +596,11 @@ public class DownloadManagerService extends Service { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         public void addMissionEventListener(Callback handler) { |         public void addMissionEventListener(Callback handler) { | ||||||
|             manageObservers(handler, true); |             mEchoObservers.add(handler); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public void removeMissionEventListener(Callback handler) { |         public void removeMissionEventListener(Callback handler) { | ||||||
|             manageObservers(handler, false); |             mEchoObservers.remove(handler); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public void clearDownloadNotifications() { |         public void clearDownloadNotifications() { | ||||||
|   | |||||||
| @@ -10,16 +10,6 @@ import android.os.AsyncTask; | |||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import android.os.Message; | import android.os.Message; | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.annotation.StringRes; |  | ||||||
| import androidx.core.content.FileProvider; |  | ||||||
| import androidx.core.view.ViewCompat; |  | ||||||
| import androidx.appcompat.app.AlertDialog; |  | ||||||
| import androidx.recyclerview.widget.DiffUtil; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView.Adapter; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView.ViewHolder; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.util.SparseArray; | import android.util.SparseArray; | ||||||
| import android.view.HapticFeedbackConstants; | import android.view.HapticFeedbackConstants; | ||||||
| @@ -34,8 +24,20 @@ import android.widget.PopupMenu; | |||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.annotation.StringRes; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.core.content.FileProvider; | ||||||
|  | import androidx.core.view.ViewCompat; | ||||||
|  | import androidx.recyclerview.widget.DiffUtil; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView.Adapter; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView.ViewHolder; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.BuildConfig; | import org.schabi.newpipe.BuildConfig; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; | import org.schabi.newpipe.report.ErrorActivity; | ||||||
| import org.schabi.newpipe.report.UserAction; | import org.schabi.newpipe.report.UserAction; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| @@ -44,11 +46,11 @@ import java.io.File; | |||||||
| import java.lang.ref.WeakReference; | import java.lang.ref.WeakReference; | ||||||
| import java.net.URI; | import java.net.URI; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collections; |  | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; | import us.shandian.giga.get.DownloadMission; | ||||||
| import us.shandian.giga.get.FinishedMission; | import us.shandian.giga.get.FinishedMission; | ||||||
| import us.shandian.giga.get.Mission; | import us.shandian.giga.get.Mission; | ||||||
|  | import us.shandian.giga.get.MissionRecoveryInfo; | ||||||
| import us.shandian.giga.io.StoredFileHelper; | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.service.DownloadManager; | import us.shandian.giga.service.DownloadManager; | ||||||
| import us.shandian.giga.service.DownloadManagerService; | import us.shandian.giga.service.DownloadManagerService; | ||||||
| @@ -62,7 +64,6 @@ import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; | |||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; | import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION; | import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT; | import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE; |  | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE; | import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; | import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION; | import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION; | ||||||
| @@ -71,6 +72,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; | |||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; | import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; | import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; | import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; | ||||||
|  | import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; | import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT; | import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; | import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; | ||||||
| @@ -81,6 +83,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|     private static final String TAG = "MissionAdapter"; |     private static final String TAG = "MissionAdapter"; | ||||||
|     private static final String UNDEFINED_PROGRESS = "--.-%"; |     private static final String UNDEFINED_PROGRESS = "--.-%"; | ||||||
|     private static final String DEFAULT_MIME_TYPE = "*/*"; |     private static final String DEFAULT_MIME_TYPE = "*/*"; | ||||||
|  |     private static final String UNDEFINED_ETA = "--:--"; | ||||||
|  |  | ||||||
|  |  | ||||||
|     static { |     static { | ||||||
| @@ -102,10 +105,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|     private View mEmptyMessage; |     private View mEmptyMessage; | ||||||
|     private RecoverHelper mRecover; |     private RecoverHelper mRecover; | ||||||
|  |  | ||||||
|     public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) { |     private final Runnable rUpdater = this::updater; | ||||||
|  |  | ||||||
|  |     public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) { | ||||||
|         mContext = context; |         mContext = context; | ||||||
|         mDownloadManager = downloadManager; |         mDownloadManager = downloadManager; | ||||||
|         mDeleter = null; |  | ||||||
|  |  | ||||||
|         mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |         mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | ||||||
|         mLayout = R.layout.mission_item; |         mLayout = R.layout.mission_item; | ||||||
| @@ -116,7 +120,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|  |  | ||||||
|         mIterator = downloadManager.getIterator(); |         mIterator = downloadManager.getIterator(); | ||||||
|  |  | ||||||
|  |         mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler); | ||||||
|  |  | ||||||
|         checkEmptyMessageVisibility(); |         checkEmptyMessageVisibility(); | ||||||
|  |         onResume(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -141,17 +148,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|         if (h.item.mission instanceof DownloadMission) { |         if (h.item.mission instanceof DownloadMission) { | ||||||
|             mPendingDownloadsItems.remove(h); |             mPendingDownloadsItems.remove(h); | ||||||
|             if (mPendingDownloadsItems.size() < 1) { |             if (mPendingDownloadsItems.size() < 1) { | ||||||
|                 setAutoRefresh(false); |  | ||||||
|                 checkMasterButtonsVisibility(); |                 checkMasterButtonsVisibility(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         h.popupMenu.dismiss(); |         h.popupMenu.dismiss(); | ||||||
|         h.item = null; |         h.item = null; | ||||||
|         h.lastTimeStamp = -1; |         h.resetSpeedMeasure(); | ||||||
|         h.lastDone = -1; |  | ||||||
|         h.lastCurrent = -1; |  | ||||||
|         h.state = 0; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -190,7 +193,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|  |  | ||||||
|             h.size.setText(length); |             h.size.setText(length); | ||||||
|             h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); |             h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); | ||||||
|             h.lastCurrent = mission.current; |  | ||||||
|             updateProgress(h); |             updateProgress(h); | ||||||
|             mPendingDownloadsItems.add(h); |             mPendingDownloadsItems.add(h); | ||||||
|         } else { |         } else { | ||||||
| @@ -215,40 +217,27 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|     private void updateProgress(ViewHolderItem h) { |     private void updateProgress(ViewHolderItem h) { | ||||||
|         if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; |         if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; | ||||||
|  |  | ||||||
|         long now = System.currentTimeMillis(); |  | ||||||
|         DownloadMission mission = (DownloadMission) h.item.mission; |         DownloadMission mission = (DownloadMission) h.item.mission; | ||||||
|  |         double done = mission.done; | ||||||
|         if (h.lastCurrent != mission.current) { |         long length = mission.getLength(); | ||||||
|             h.lastCurrent = mission.current; |         long now = System.currentTimeMillis(); | ||||||
|             h.lastTimeStamp = now; |  | ||||||
|             h.lastDone = 0; |  | ||||||
|         } else { |  | ||||||
|             if (h.lastTimeStamp == -1) h.lastTimeStamp = now; |  | ||||||
|             if (h.lastDone == -1) h.lastDone = mission.done; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         long deltaTime = now - h.lastTimeStamp; |  | ||||||
|         long deltaDone = mission.done - h.lastDone; |  | ||||||
|         boolean hasError = mission.errCode != ERROR_NOTHING; |         boolean hasError = mission.errCode != ERROR_NOTHING; | ||||||
|  |  | ||||||
|         // hide on error |         // hide on error | ||||||
|         // show if current resource length is not fetched |         // show if current resource length is not fetched | ||||||
|         // show if length is unknown |         // show if length is unknown | ||||||
|         h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength)); |         h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength)); | ||||||
|  |  | ||||||
|         float progress; |         double progress; | ||||||
|         if (mission.unknownLength) { |         if (mission.unknownLength) { | ||||||
|             progress = Float.NaN; |             progress = Double.NaN; | ||||||
|             h.progress.setProgress(0f); |             h.progress.setProgress(0f); | ||||||
|         } else { |         } else { | ||||||
|             progress = (float) ((double) mission.done / mission.length); |             progress = done / length; | ||||||
|             if (mission.urls.length > 1 && mission.current < mission.urls.length) { |  | ||||||
|                 progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (hasError) { |         if (hasError) { | ||||||
|             h.progress.setProgress(isNotFinite(progress) ? 1f : progress); |             h.progress.setProgress(isNotFinite(progress) ? 1d : progress); | ||||||
|             h.status.setText(R.string.msg_error); |             h.status.setText(R.string.msg_error); | ||||||
|         } else if (isNotFinite(progress)) { |         } else if (isNotFinite(progress)) { | ||||||
|             h.status.setText(UNDEFINED_PROGRESS); |             h.status.setText(UNDEFINED_PROGRESS); | ||||||
| @@ -257,59 +246,78 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|             h.progress.setProgress(progress); |             h.progress.setProgress(progress); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         long length = mission.getLength(); |         @StringRes int state; | ||||||
|  |         String sizeStr = Utility.formatBytes(length).concat("  "); | ||||||
|  |  | ||||||
|         int state; |  | ||||||
|         if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) { |         if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) { | ||||||
|             state = 0; |             h.size.setText(sizeStr); | ||||||
|  |             return; | ||||||
|         } else if (!mission.running) { |         } else if (!mission.running) { | ||||||
|             state = mission.enqueued ? 1 : 2; |             state = mission.enqueued ? R.string.queued : R.string.paused; | ||||||
|         } else if (mission.isPsRunning()) { |         } else if (mission.isPsRunning()) { | ||||||
|             state = 3; |             state = R.string.post_processing; | ||||||
|  |         } else if (mission.isRecovering()) { | ||||||
|  |             state = R.string.recovering; | ||||||
|         } else { |         } else { | ||||||
|             state = 0; |             state = 0; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (state != 0) { |         if (state != 0) { | ||||||
|             // update state without download speed |             // update state without download speed | ||||||
|             if (h.state != state) { |             h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")")); | ||||||
|                 String statusStr; |             h.resetSpeedMeasure(); | ||||||
|                 h.state = state; |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|                 switch (state) { |         if (h.lastTimestamp < 0) { | ||||||
|                     case 1: |             h.size.setText(sizeStr); | ||||||
|                         statusStr = mContext.getString(R.string.queued); |             h.lastTimestamp = now; | ||||||
|                         break; |             h.lastDone = done; | ||||||
|                     case 2: |             return; | ||||||
|                         statusStr = mContext.getString(R.string.paused); |         } | ||||||
|                         break; |  | ||||||
|                     case 3: |  | ||||||
|                         statusStr = mContext.getString(R.string.post_processing); |  | ||||||
|                         break; |  | ||||||
|                     default: |  | ||||||
|                         statusStr = "?"; |  | ||||||
|                         break; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 h.size.setText(Utility.formatBytes(length).concat("  (").concat(statusStr).concat(")")); |         long deltaTime = now - h.lastTimestamp; | ||||||
|             } else if (deltaDone > 0) { |         double deltaDone = done - h.lastDone; | ||||||
|                 h.lastTimeStamp = now; |  | ||||||
|                 h.lastDone = mission.done; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|  |         if (h.lastDone > done) { | ||||||
|  |             h.lastDone = done; | ||||||
|  |             h.size.setText(sizeStr); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (deltaDone > 0 && deltaTime > 0) { |         if (deltaDone > 0 && deltaTime > 0) { | ||||||
|             float speed = (deltaDone * 1000f) / deltaTime; |             float speed = (float) ((deltaDone * 1000d) / deltaTime); | ||||||
|  |             float averageSpeed = speed; | ||||||
|  |  | ||||||
|             String speedStr = Utility.formatSpeed(speed); |             if (h.lastSpeedIdx < 0) { | ||||||
|             String sizeStr = Utility.formatBytes(length); |                 for (int i = 0; i < h.lastSpeed.length; i++) { | ||||||
|  |                     h.lastSpeed[i] = speed; | ||||||
|  |                 } | ||||||
|  |                 h.lastSpeedIdx = 0; | ||||||
|  |             } else { | ||||||
|  |                 for (int i = 0; i < h.lastSpeed.length; i++) { | ||||||
|  |                     averageSpeed += h.lastSpeed[i]; | ||||||
|  |                 } | ||||||
|  |                 averageSpeed /= h.lastSpeed.length + 1f; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             h.size.setText(sizeStr.concat(" ").concat(speedStr)); |             String speedStr = Utility.formatSpeed(averageSpeed); | ||||||
|  |             String etaStr; | ||||||
|  |  | ||||||
|             h.lastTimeStamp = now; |             if (mission.unknownLength) { | ||||||
|             h.lastDone = mission.done; |                 etaStr = ""; | ||||||
|  |             } else { | ||||||
|  |                 long eta = (long) Math.ceil((length - done) / averageSpeed); | ||||||
|  |                 etaStr = Utility.formatBytes((long) done) + "/" + Utility.stringifySeconds(eta) + "  "; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             h.size.setText(sizeStr.concat(etaStr).concat(speedStr)); | ||||||
|  |  | ||||||
|  |             h.lastTimestamp = now; | ||||||
|  |             h.lastDone = done; | ||||||
|  |             h.lastSpeed[h.lastSpeedIdx++] = speed; | ||||||
|  |  | ||||||
|  |             if (h.lastSpeedIdx >= h.lastSpeed.length) h.lastSpeedIdx = 0; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -388,6 +396,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private ViewHolderItem getViewHolder(Object mission) { | ||||||
|  |         for (ViewHolderItem h : mPendingDownloadsItems) { | ||||||
|  |             if (h.item.mission == mission) return h; | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean handleMessage(@NonNull Message msg) { |     public boolean handleMessage(@NonNull Message msg) { | ||||||
|         if (mStartButton != null && mPauseButton != null) { |         if (mStartButton != null && mPauseButton != null) { | ||||||
| @@ -395,33 +410,28 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         switch (msg.what) { |         switch (msg.what) { | ||||||
|             case DownloadManagerService.MESSAGE_PROGRESS: |  | ||||||
|             case DownloadManagerService.MESSAGE_ERROR: |             case DownloadManagerService.MESSAGE_ERROR: | ||||||
|             case DownloadManagerService.MESSAGE_FINISHED: |             case DownloadManagerService.MESSAGE_FINISHED: | ||||||
|  |             case DownloadManagerService.MESSAGE_DELETED: | ||||||
|  |             case DownloadManagerService.MESSAGE_PAUSED: | ||||||
|                 break; |                 break; | ||||||
|             default: |             default: | ||||||
|                 return false; |                 return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) { |         ViewHolderItem h = getViewHolder(msg.obj); | ||||||
|             setAutoRefresh(true); |         if (h == null) return false; | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         for (ViewHolderItem h : mPendingDownloadsItems) { |         switch (msg.what) { | ||||||
|             if (h.item.mission != msg.obj) continue; |             case DownloadManagerService.MESSAGE_FINISHED: | ||||||
|  |             case DownloadManagerService.MESSAGE_DELETED: | ||||||
|             if (msg.what == DownloadManagerService.MESSAGE_FINISHED) { |  | ||||||
|                 // DownloadManager should mark the download as finished |                 // DownloadManager should mark the download as finished | ||||||
|                 applyChanges(); |                 applyChanges(); | ||||||
|                 return true; |                 return true; | ||||||
|             } |  | ||||||
|  |  | ||||||
|             updateProgress(h); |  | ||||||
|             return true; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return false; |         updateProgress(h); | ||||||
|  |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void showError(@NonNull DownloadMission mission) { |     private void showError(@NonNull DownloadMission mission) { | ||||||
| @@ -430,7 +440,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|  |  | ||||||
|         switch (mission.errCode) { |         switch (mission.errCode) { | ||||||
|             case 416: |             case 416: | ||||||
|                 msg = R.string.error_http_requested_range_not_satisfiable; |                 msg = R.string.error_http_unsupported_range; | ||||||
|                 break; |                 break; | ||||||
|             case 404: |             case 404: | ||||||
|                 msg = R.string.error_http_not_found; |                 msg = R.string.error_http_not_found; | ||||||
| @@ -443,9 +453,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|             case ERROR_HTTP_NO_CONTENT: |             case ERROR_HTTP_NO_CONTENT: | ||||||
|                 msg = R.string.error_http_no_content; |                 msg = R.string.error_http_no_content; | ||||||
|                 break; |                 break; | ||||||
|             case ERROR_HTTP_UNSUPPORTED_RANGE: |  | ||||||
|                 msg = R.string.error_http_unsupported_range; |  | ||||||
|                 break; |  | ||||||
|             case ERROR_PATH_CREATION: |             case ERROR_PATH_CREATION: | ||||||
|                 msg = R.string.error_path_creation; |                 msg = R.string.error_path_creation; | ||||||
|                 break; |                 break; | ||||||
| @@ -466,27 +473,35 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|                 break; |                 break; | ||||||
|             case ERROR_POSTPROCESSING: |             case ERROR_POSTPROCESSING: | ||||||
|             case ERROR_POSTPROCESSING_HOLD: |             case ERROR_POSTPROCESSING_HOLD: | ||||||
|                 showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed); |                 showError(mission, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed); | ||||||
|                 return; |                 return; | ||||||
|             case ERROR_INSUFFICIENT_STORAGE: |             case ERROR_INSUFFICIENT_STORAGE: | ||||||
|                 msg = R.string.error_insufficient_storage; |                 msg = R.string.error_insufficient_storage; | ||||||
|                 break; |                 break; | ||||||
|             case ERROR_UNKNOWN_EXCEPTION: |             case ERROR_UNKNOWN_EXCEPTION: | ||||||
|                 showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error); |                 if (mission.errObject != null) { | ||||||
|                 return; |                     showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error); | ||||||
|  |                     return; | ||||||
|  |                 } else { | ||||||
|  |                     msg = R.string.msg_error; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|             case ERROR_PROGRESS_LOST: |             case ERROR_PROGRESS_LOST: | ||||||
|                 msg = R.string.error_progress_lost; |                 msg = R.string.error_progress_lost; | ||||||
|                 break; |                 break; | ||||||
|             case ERROR_TIMEOUT: |             case ERROR_TIMEOUT: | ||||||
|                 msg = R.string.error_timeout; |                 msg = R.string.error_timeout; | ||||||
|                 break; |                 break; | ||||||
|  |             case ERROR_RESOURCE_GONE: | ||||||
|  |                 msg = R.string.error_download_resource_gone; | ||||||
|  |                 break; | ||||||
|             default: |             default: | ||||||
|                 if (mission.errCode >= 100 && mission.errCode < 600) { |                 if (mission.errCode >= 100 && mission.errCode < 600) { | ||||||
|                     msgEx = "HTTP " + mission.errCode; |                     msgEx = "HTTP " + mission.errCode; | ||||||
|                 } else if (mission.errObject == null) { |                 } else if (mission.errObject == null) { | ||||||
|                     msgEx = "(not_decelerated_error_code)"; |                     msgEx = "(not_decelerated_error_code)"; | ||||||
|                 } else { |                 } else { | ||||||
|                     showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg); |                     showError(mission, UserAction.DOWNLOAD_FAILED, msg); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 break; |                 break; | ||||||
| @@ -503,7 +518,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|         if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) { |         if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) { | ||||||
|             @StringRes final int mMsg = msg; |             @StringRes final int mMsg = msg; | ||||||
|             builder.setPositiveButton(R.string.error_report_title, (dialog, which) -> |             builder.setPositiveButton(R.string.error_report_title, (dialog, which) -> | ||||||
|                     showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg) |                     showError(mission, UserAction.DOWNLOAD_FAILED, mMsg) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -513,13 +528,32 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|                 .show(); |                 .show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void showError(Exception exception, UserAction action, @StringRes int reason) { |     private void showError(DownloadMission mission, UserAction action, @StringRes int reason) { | ||||||
|  |         StringBuilder request = new StringBuilder(256); | ||||||
|  |         request.append(mission.source); | ||||||
|  |  | ||||||
|  |         request.append(" ["); | ||||||
|  |         if (mission.recoveryInfo != null) { | ||||||
|  |             for (MissionRecoveryInfo recovery : mission.recoveryInfo) | ||||||
|  |                 request.append(' ') | ||||||
|  |                         .append(recovery.toString()) | ||||||
|  |                         .append(' '); | ||||||
|  |         } | ||||||
|  |         request.append("]"); | ||||||
|  |  | ||||||
|  |         String service; | ||||||
|  |         try { | ||||||
|  |             service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName(); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             service = "-"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         ErrorActivity.reportError( |         ErrorActivity.reportError( | ||||||
|                 mContext, |                 mContext, | ||||||
|                 Collections.singletonList(exception), |                 mission.errObject, | ||||||
|                 null, |                 null, | ||||||
|                 null, |                 null, | ||||||
|                 ErrorActivity.ErrorInfo.make(action, "-", "-", reason) |                 ErrorActivity.ErrorInfo.make(action, service, request.toString(), reason) | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -538,16 +572,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|             switch (id) { |             switch (id) { | ||||||
|                 case R.id.start: |                 case R.id.start: | ||||||
|                     h.status.setText(UNDEFINED_PROGRESS); |                     h.status.setText(UNDEFINED_PROGRESS); | ||||||
|                     h.state = -1; |  | ||||||
|                     h.size.setText(Utility.formatBytes(mission.getLength())); |  | ||||||
|                     mDownloadManager.resumeMission(mission); |                     mDownloadManager.resumeMission(mission); | ||||||
|                     return true; |                     return true; | ||||||
|                 case R.id.pause: |                 case R.id.pause: | ||||||
|                     h.state = -1; |  | ||||||
|                     mDownloadManager.pauseMission(mission); |                     mDownloadManager.pauseMission(mission); | ||||||
|                     updateProgress(h); |  | ||||||
|                     h.lastTimeStamp = -1; |  | ||||||
|                     h.lastDone = -1; |  | ||||||
|                     return true; |                     return true; | ||||||
|                 case R.id.error_message_view: |                 case R.id.error_message_view: | ||||||
|                     showError(mission); |                     showError(mission); | ||||||
| @@ -580,12 +608,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|                 shareFile(h.item.mission); |                 shareFile(h.item.mission); | ||||||
|                 return true; |                 return true; | ||||||
|             case R.id.delete: |             case R.id.delete: | ||||||
|                 if (mDeleter == null) { |                 mDeleter.append(h.item.mission); | ||||||
|                     mDownloadManager.deleteMission(h.item.mission); |  | ||||||
|                 } else { |  | ||||||
|                     mDeleter.append(h.item.mission); |  | ||||||
|                 } |  | ||||||
|                 applyChanges(); |                 applyChanges(); | ||||||
|  |                 checkMasterButtonsVisibility(); | ||||||
|                 return true; |                 return true; | ||||||
|             case R.id.md5: |             case R.id.md5: | ||||||
|             case R.id.sha1: |             case R.id.sha1: | ||||||
| @@ -621,7 +646,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|         mIterator.end(); |         mIterator.end(); | ||||||
|  |  | ||||||
|         for (ViewHolderItem item : mPendingDownloadsItems) { |         for (ViewHolderItem item : mPendingDownloadsItems) { | ||||||
|             item.lastTimeStamp = -1; |             item.resetSpeedMeasure(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         notifyDataSetChanged(); |         notifyDataSetChanged(); | ||||||
| @@ -654,6 +679,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|  |  | ||||||
|     public void checkMasterButtonsVisibility() { |     public void checkMasterButtonsVisibility() { | ||||||
|         boolean[] state = mIterator.hasValidPendingMissions(); |         boolean[] state = mIterator.hasValidPendingMissions(); | ||||||
|  |         Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]); | ||||||
|         setButtonVisible(mPauseButton, state[0]); |         setButtonVisible(mPauseButton, state[0]); | ||||||
|         setButtonVisible(mStartButton, state[1]); |         setButtonVisible(mStartButton, state[1]); | ||||||
|     } |     } | ||||||
| @@ -663,86 +689,57 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|             button.setVisible(visible); |             button.setVisible(visible); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void ensurePausedMissions() { |     public void refreshMissionItems() { | ||||||
|         for (ViewHolderItem h : mPendingDownloadsItems) { |         for (ViewHolderItem h : mPendingDownloadsItems) { | ||||||
|             if (((DownloadMission) h.item.mission).running) continue; |             if (((DownloadMission) h.item.mission).running) continue; | ||||||
|             updateProgress(h); |             updateProgress(h); | ||||||
|             h.lastTimeStamp = -1; |             h.resetSpeedMeasure(); | ||||||
|             h.lastDone = -1; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     public void deleterDispose(boolean commitChanges) { |     public void onDestroy() { | ||||||
|         if (mDeleter != null) mDeleter.dispose(commitChanges); |         mDeleter.dispose(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void deleterLoad(View view) { |     public void onResume() { | ||||||
|         if (mDeleter == null) |         mDeleter.resume(); | ||||||
|             mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler); |         mHandler.post(rUpdater); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void deleterResume() { |  | ||||||
|         if (mDeleter != null) mDeleter.resume(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void recoverMission(DownloadMission mission) { |  | ||||||
|         for (ViewHolderItem h : mPendingDownloadsItems) { |  | ||||||
|             if (mission != h.item.mission) continue; |  | ||||||
|  |  | ||||||
|             mission.errObject = null; |  | ||||||
|             mission.resetState(true, false, DownloadMission.ERROR_NOTHING); |  | ||||||
|  |  | ||||||
|             h.status.setText(UNDEFINED_PROGRESS); |  | ||||||
|             h.state = -1; |  | ||||||
|             h.size.setText(Utility.formatBytes(mission.getLength())); |  | ||||||
|             h.progress.setMarquee(true); |  | ||||||
|  |  | ||||||
|             mDownloadManager.resumeMission(mission); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private boolean mUpdaterRunning = false; |  | ||||||
|     private final Runnable rUpdater = this::updater; |  | ||||||
|  |  | ||||||
|     public void onPaused() { |     public void onPaused() { | ||||||
|         setAutoRefresh(false); |         mDeleter.pause(); | ||||||
|  |         mHandler.removeCallbacks(rUpdater); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void setAutoRefresh(boolean enabled) { |  | ||||||
|         if (enabled && !mUpdaterRunning) { |     public void recoverMission(DownloadMission mission) { | ||||||
|             mUpdaterRunning = true; |         ViewHolderItem h = getViewHolder(mission); | ||||||
|             updater(); |         if (h == null) return; | ||||||
|         } else if (!enabled && mUpdaterRunning) { |  | ||||||
|             mUpdaterRunning = false; |         mission.errObject = null; | ||||||
|             mHandler.removeCallbacks(rUpdater); |         mission.resetState(true, false, DownloadMission.ERROR_NOTHING); | ||||||
|         } |  | ||||||
|  |         h.status.setText(UNDEFINED_PROGRESS); | ||||||
|  |         h.size.setText(Utility.formatBytes(mission.getLength())); | ||||||
|  |         h.progress.setMarquee(true); | ||||||
|  |  | ||||||
|  |         mDownloadManager.resumeMission(mission); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void updater() { |     private void updater() { | ||||||
|         if (!mUpdaterRunning) return; |  | ||||||
|  |  | ||||||
|         boolean running = false; |  | ||||||
|         for (ViewHolderItem h : mPendingDownloadsItems) { |         for (ViewHolderItem h : mPendingDownloadsItems) { | ||||||
|             // check if the mission is running first |             // check if the mission is running first | ||||||
|             if (!((DownloadMission) h.item.mission).running) continue; |             if (!((DownloadMission) h.item.mission).running) continue; | ||||||
|  |  | ||||||
|             updateProgress(h); |             updateProgress(h); | ||||||
|             running = true; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (running) { |         mHandler.postDelayed(rUpdater, 1000); | ||||||
|             mHandler.postDelayed(rUpdater, 1000); |  | ||||||
|         } else { |  | ||||||
|             mUpdaterRunning = false; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean isNotFinite(Float value) { |     private boolean isNotFinite(double value) { | ||||||
|         return Float.isNaN(value) || Float.isInfinite(value); |         return Double.isNaN(value) || Double.isInfinite(value); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setRecover(@NonNull RecoverHelper callback) { |     public void setRecover(@NonNull RecoverHelper callback) { | ||||||
| @@ -771,10 +768,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|         MenuItem source; |         MenuItem source; | ||||||
|         MenuItem checksum; |         MenuItem checksum; | ||||||
|  |  | ||||||
|         long lastTimeStamp = -1; |         long lastTimestamp = -1; | ||||||
|         long lastDone = -1; |         double lastDone; | ||||||
|         int lastCurrent = -1; |         int lastSpeedIdx; | ||||||
|         int state = 0; |         float[] lastSpeed = new float[3]; | ||||||
|  |         String estimatedTimeArrival = UNDEFINED_ETA; | ||||||
|  |  | ||||||
|         ViewHolderItem(View view) { |         ViewHolderItem(View view) { | ||||||
|             super(view); |             super(view); | ||||||
| @@ -859,7 +857,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|  |  | ||||||
|                         delete.setVisible(true); |                         delete.setVisible(true); | ||||||
|  |  | ||||||
|                         boolean flag = !mission.isPsFailed(); |                         boolean flag = !mission.isPsFailed() && mission.urls.length > 0; | ||||||
|                         start.setVisible(flag); |                         start.setVisible(flag); | ||||||
|                         queue.setVisible(flag); |                         queue.setVisible(flag); | ||||||
|                     } |                     } | ||||||
| @@ -884,6 +882,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|  |  | ||||||
|             return popup; |             return popup; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         private void resetSpeedMeasure() { | ||||||
|  |             estimatedTimeArrival = UNDEFINED_ETA; | ||||||
|  |             lastTimestamp = -1; | ||||||
|  |             lastSpeedIdx = -1; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     class ViewHolderHeader extends RecyclerView.ViewHolder { |     class ViewHolderHeader extends RecyclerView.ViewHolder { | ||||||
|   | |||||||
| @@ -4,9 +4,10 @@ import android.content.Context; | |||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.graphics.Color; | import android.graphics.Color; | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import com.google.android.material.snackbar.Snackbar; |  | ||||||
| import android.view.View; | import android.view.View; | ||||||
|  |  | ||||||
|  | import com.google.android.material.snackbar.Snackbar; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| @@ -113,7 +114,7 @@ public class Deleter { | |||||||
|         show(); |         show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void pause() { |     public void pause() { | ||||||
|         running = false; |         running = false; | ||||||
|         mHandler.removeCallbacks(rNext); |         mHandler.removeCallbacks(rNext); | ||||||
|         mHandler.removeCallbacks(rShow); |         mHandler.removeCallbacks(rShow); | ||||||
| @@ -126,13 +127,11 @@ public class Deleter { | |||||||
|         mHandler.postDelayed(rShow, DELAY_RESUME); |         mHandler.postDelayed(rShow, DELAY_RESUME); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void dispose(boolean commitChanges) { |     public void dispose() { | ||||||
|         if (items.size() < 1) return; |         if (items.size() < 1) return; | ||||||
|  |  | ||||||
|         pause(); |         pause(); | ||||||
|  |  | ||||||
|         if (!commitChanges) return; |  | ||||||
|  |  | ||||||
|         for (Mission mission : items) mDownloadManager.deleteMission(mission); |         for (Mission mission : items) mDownloadManager.deleteMission(mission); | ||||||
|         items = null; |         items = null; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import android.graphics.Rect; | |||||||
| import android.graphics.drawable.Drawable; | import android.graphics.drawable.Drawable; | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import android.os.Looper; | import android.os.Looper; | ||||||
|  |  | ||||||
| import androidx.annotation.ColorInt; | import androidx.annotation.ColorInt; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
| @@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable { | |||||||
|         mForegroundColor = foreground; |         mForegroundColor = foreground; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setProgress(float progress) { |     public void setProgress(double progress) { | ||||||
|         mProgress = progress; |         mProgress = (float) progress; | ||||||
|         invalidateSelf(); |         invalidateSelf(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,11 +12,6 @@ import android.os.Bundle; | |||||||
| import android.os.Environment; | import android.os.Environment; | ||||||
| import android.os.IBinder; | import android.os.IBinder; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.recyclerview.widget.GridLayoutManager; |  | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.Menu; | import android.view.Menu; | ||||||
| import android.view.MenuItem; | import android.view.MenuItem; | ||||||
| @@ -24,6 +19,12 @@ import android.view.View; | |||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.recyclerview.widget.GridLayoutManager; | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  |  | ||||||
| import com.nononsenseapps.filepicker.Utils; | import com.nononsenseapps.filepicker.Utils; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| @@ -72,8 +73,7 @@ public class MissionsFragment extends Fragment { | |||||||
|             mBinder = (DownloadManagerBinder) binder; |             mBinder = (DownloadManagerBinder) binder; | ||||||
|             mBinder.clearDownloadNotifications(); |             mBinder.clearDownloadNotifications(); | ||||||
|  |  | ||||||
|             mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); |             mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView()); | ||||||
|             mAdapter.deleterLoad(getView()); |  | ||||||
|  |  | ||||||
|             mAdapter.setRecover(MissionsFragment.this::recoverMission); |             mAdapter.setRecover(MissionsFragment.this::recoverMission); | ||||||
|  |  | ||||||
| @@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment { | |||||||
|      * Added in API level 23. |      * Added in API level 23. | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void onAttach(Context context) { |     public void onAttach(@NonNull Context context) { | ||||||
|         super.onAttach(context); |         super.onAttach(context); | ||||||
|  |  | ||||||
|         // Bug: in api< 23 this is never called |         // Bug: in api< 23 this is never called | ||||||
| @@ -147,7 +147,7 @@ public class MissionsFragment extends Fragment { | |||||||
|      */ |      */ | ||||||
|     @SuppressWarnings("deprecation") |     @SuppressWarnings("deprecation") | ||||||
|     @Override |     @Override | ||||||
|     public void onAttach(Activity activity) { |     public void onAttach(@NonNull Activity activity) { | ||||||
|         super.onAttach(activity); |         super.onAttach(activity); | ||||||
|  |  | ||||||
|         mContext = activity; |         mContext = activity; | ||||||
| @@ -162,7 +162,7 @@ public class MissionsFragment extends Fragment { | |||||||
|         mBinder.removeMissionEventListener(mAdapter); |         mBinder.removeMissionEventListener(mAdapter); | ||||||
|         mBinder.enableNotifications(true); |         mBinder.enableNotifications(true); | ||||||
|         mContext.unbindService(mConnection); |         mContext.unbindService(mConnection); | ||||||
|         mAdapter.deleterDispose(true); |         mAdapter.onDestroy(); | ||||||
|  |  | ||||||
|         mBinder = null; |         mBinder = null; | ||||||
|         mAdapter = null; |         mAdapter = null; | ||||||
| @@ -196,13 +196,11 @@ public class MissionsFragment extends Fragment { | |||||||
|                 prompt.create().show(); |                 prompt.create().show(); | ||||||
|                 return true; |                 return true; | ||||||
|             case R.id.start_downloads: |             case R.id.start_downloads: | ||||||
|                 item.setVisible(false); |  | ||||||
|                 mBinder.getDownloadManager().startAllMissions(); |                 mBinder.getDownloadManager().startAllMissions(); | ||||||
|                 return true; |                 return true; | ||||||
|             case R.id.pause_downloads: |             case R.id.pause_downloads: | ||||||
|                 item.setVisible(false); |  | ||||||
|                 mBinder.getDownloadManager().pauseAllMissions(false); |                 mBinder.getDownloadManager().pauseAllMissions(false); | ||||||
|                 mAdapter.ensurePausedMissions();// update items view |                 mAdapter.refreshMissionItems();// update items view | ||||||
|             default: |             default: | ||||||
|                 return super.onOptionsItemSelected(item); |                 return super.onOptionsItemSelected(item); | ||||||
|         } |         } | ||||||
| @@ -271,23 +269,12 @@ public class MissionsFragment extends Fragment { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onSaveInstanceState(@NonNull Bundle outState) { |  | ||||||
|         super.onSaveInstanceState(outState); |  | ||||||
|  |  | ||||||
|         if (mAdapter != null) { |  | ||||||
|             mAdapter.deleterDispose(false); |  | ||||||
|             mForceUpdate = true; |  | ||||||
|             mBinder.removeMissionEventListener(mAdapter); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onResume() { |     public void onResume() { | ||||||
|         super.onResume(); |         super.onResume(); | ||||||
|  |  | ||||||
|         if (mAdapter != null) { |         if (mAdapter != null) { | ||||||
|             mAdapter.deleterResume(); |             mAdapter.onResume(); | ||||||
|  |  | ||||||
|             if (mForceUpdate) { |             if (mForceUpdate) { | ||||||
|                 mForceUpdate = false; |                 mForceUpdate = false; | ||||||
| @@ -303,7 +290,13 @@ public class MissionsFragment extends Fragment { | |||||||
|     @Override |     @Override | ||||||
|     public void onPause() { |     public void onPause() { | ||||||
|         super.onPause(); |         super.onPause(); | ||||||
|         if (mAdapter != null) mAdapter.onPaused(); |  | ||||||
|  |         if (mAdapter != null) { | ||||||
|  |             mForceUpdate = true; | ||||||
|  |             mBinder.removeMissionEventListener(mAdapter); | ||||||
|  |             mAdapter.onPaused(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (mBinder != null) mBinder.enableNotifications(true); |         if (mBinder != null) mBinder.enableNotifications(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,13 +4,14 @@ import android.content.ClipData; | |||||||
| import android.content.ClipboardManager; | import android.content.ClipboardManager; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.widget.Toast; | ||||||
|  |  | ||||||
| import androidx.annotation.ColorInt; | import androidx.annotation.ColorInt; | ||||||
| import androidx.annotation.DrawableRes; | import androidx.annotation.DrawableRes; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| import android.util.Log; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
| @@ -26,6 +27,7 @@ import java.io.Serializable; | |||||||
| import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||||
| import java.security.MessageDigest; | import java.security.MessageDigest; | ||||||
| import java.security.NoSuchAlgorithmException; | import java.security.NoSuchAlgorithmException; | ||||||
|  | import java.util.Locale; | ||||||
|  |  | ||||||
| import us.shandian.giga.io.StoredFileHelper; | import us.shandian.giga.io.StoredFileHelper; | ||||||
|  |  | ||||||
| @@ -39,26 +41,28 @@ public class Utility { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static String formatBytes(long bytes) { |     public static String formatBytes(long bytes) { | ||||||
|  |         Locale locale = Locale.getDefault(); | ||||||
|         if (bytes < 1024) { |         if (bytes < 1024) { | ||||||
|             return String.format("%d B", bytes); |             return String.format(locale, "%d B", bytes); | ||||||
|         } else if (bytes < 1024 * 1024) { |         } else if (bytes < 1024 * 1024) { | ||||||
|             return String.format("%.2f kB", bytes / 1024d); |             return String.format(locale, "%.2f kB", bytes / 1024d); | ||||||
|         } else if (bytes < 1024 * 1024 * 1024) { |         } else if (bytes < 1024 * 1024 * 1024) { | ||||||
|             return String.format("%.2f MB", bytes / 1024d / 1024d); |             return String.format(locale, "%.2f MB", bytes / 1024d / 1024d); | ||||||
|         } else { |         } else { | ||||||
|             return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d); |             return String.format(locale, "%.2f GB", bytes / 1024d / 1024d / 1024d); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static String formatSpeed(float speed) { |     public static String formatSpeed(double speed) { | ||||||
|  |         Locale locale = Locale.getDefault(); | ||||||
|         if (speed < 1024) { |         if (speed < 1024) { | ||||||
|             return String.format("%.2f B/s", speed); |             return String.format(locale, "%.2f B/s", speed); | ||||||
|         } else if (speed < 1024 * 1024) { |         } else if (speed < 1024 * 1024) { | ||||||
|             return String.format("%.2f kB/s", speed / 1024); |             return String.format(locale, "%.2f kB/s", speed / 1024); | ||||||
|         } else if (speed < 1024 * 1024 * 1024) { |         } else if (speed < 1024 * 1024 * 1024) { | ||||||
|             return String.format("%.2f MB/s", speed / 1024 / 1024); |             return String.format(locale, "%.2f MB/s", speed / 1024 / 1024); | ||||||
|         } else { |         } else { | ||||||
|             return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024); |             return String.format(locale, "%.2f GB/s", speed / 1024 / 1024 / 1024); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -188,12 +192,11 @@ public class Utility { | |||||||
|         switch (type) { |         switch (type) { | ||||||
|             case MUSIC: |             case MUSIC: | ||||||
|                 return R.drawable.music; |                 return R.drawable.music; | ||||||
|  |             default: | ||||||
|             case VIDEO: |             case VIDEO: | ||||||
|                 return R.drawable.video; |                 return R.drawable.video; | ||||||
|             case SUBTITLE: |             case SUBTITLE: | ||||||
|                 return R.drawable.subtitle; |                 return R.drawable.subtitle; | ||||||
|             default: |  | ||||||
|                 return R.drawable.video; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -274,4 +277,25 @@ public class Utility { | |||||||
|  |  | ||||||
|         return -1; |         return -1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private static String pad(int number) { | ||||||
|  |         return number < 10 ? ("0" + number) : String.valueOf(number); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static String stringifySeconds(double seconds) { | ||||||
|  |         int h = (int) Math.floor(seconds / 3600); | ||||||
|  |         int m = (int) Math.floor((seconds - (h * 3600)) / 60); | ||||||
|  |         int s = (int) (seconds - (h * 3600) - (m * 60)); | ||||||
|  |  | ||||||
|  |         String str = ""; | ||||||
|  |  | ||||||
|  |         if (h < 1 && m < 1) { | ||||||
|  |             str = "00:"; | ||||||
|  |         } else { | ||||||
|  |             if (h > 0) str = pad(h) + ":"; | ||||||
|  |             if (m > 0) str += pad(m) + ":"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return str + pad(s); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 208 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 206 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 475 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 460 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 166 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 166 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 311 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 312 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-nodpi/place_holder_peertube.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 235 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 226 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 597 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 588 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 291 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 284 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 856 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 835 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 344 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 345 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
| @@ -47,15 +47,22 @@ | |||||||
|  |  | ||||||
|     <TextView |     <TextView | ||||||
|         android:id="@+id/drawer_header_service_view" |         android:id="@+id/drawer_header_service_view" | ||||||
|         android:layout_width="100dp" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="100dp" |         android:layout_height="100dp" | ||||||
|         android:layout_alignLeft="@id/drawer_header_np_text_view" |         android:layout_alignLeft="@id/drawer_header_np_text_view" | ||||||
|         android:layout_alignStart="@id/drawer_header_np_text_view" |         android:layout_alignStart="@id/drawer_header_np_text_view" | ||||||
|         android:layout_below="@id/drawer_header_np_text_view" |         android:layout_below="@id/drawer_header_np_text_view" | ||||||
|  |         android:layout_toLeftOf="@id/drawer_arrow" | ||||||
|  |         android:layout_marginRight="5dp" | ||||||
|         android:text="YouTube" |         android:text="YouTube" | ||||||
|         android:textSize="18sp" |         android:textSize="18sp" | ||||||
|         android:textColor="@color/drawer_header_font_color" |         android:textColor="@color/drawer_header_font_color" | ||||||
|         android:textStyle="italic" /> |         android:textStyle="italic" | ||||||
|  |         android:ellipsize="marquee" | ||||||
|  |         android:fadingEdge="horizontal" | ||||||
|  |         android:marqueeRepeatLimit="marquee_forever" | ||||||
|  |         android:scrollHorizontally="true" | ||||||
|  |         android:singleLine="true" /> | ||||||
|  |  | ||||||
|     <ImageView |     <ImageView | ||||||
|         android:id="@+id/drawer_arrow" |         android:id="@+id/drawer_arrow" | ||||||
|   | |||||||
| @@ -46,15 +46,22 @@ android:focusable="true"> | |||||||
|  |  | ||||||
|     <TextView |     <TextView | ||||||
|         android:id="@+id/drawer_header_service_view" |         android:id="@+id/drawer_header_service_view" | ||||||
|         android:layout_width="100dp" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="100dp" |         android:layout_height="100dp" | ||||||
|         android:text="YouTube" |         android:text="YouTube" | ||||||
|         android:layout_below="@id/drawer_header_np_text_view" |         android:layout_below="@id/drawer_header_np_text_view" | ||||||
|         android:layout_alignLeft="@id/drawer_header_np_text_view" |         android:layout_alignLeft="@id/drawer_header_np_text_view" | ||||||
|         android:layout_alignStart="@id/drawer_header_np_text_view" |         android:layout_alignStart="@id/drawer_header_np_text_view" | ||||||
|  |         android:layout_toLeftOf="@id/drawer_arrow" | ||||||
|  |         android:layout_marginRight="5dp" | ||||||
|         android:textSize="18sp" |         android:textSize="18sp" | ||||||
|         android:textColor="@color/drawer_header_font_color" |         android:textColor="@color/drawer_header_font_color" | ||||||
|         android:textStyle="italic"/> |         android:textStyle="italic" | ||||||
|  |         android:ellipsize="marquee" | ||||||
|  |         android:fadingEdge="horizontal" | ||||||
|  |         android:marqueeRepeatLimit="marquee_forever" | ||||||
|  |         android:scrollHorizontally="true" | ||||||
|  |         android:singleLine="true" /> | ||||||
|  |  | ||||||
|     <ImageView |     <ImageView | ||||||
|         android:id="@+id/drawer_arrow" |         android:id="@+id/drawer_arrow" | ||||||
|   | |||||||
| @@ -16,7 +16,8 @@ | |||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         app:elevation="0dp" |         app:elevation="0dp" | ||||||
|         android:background="?attr/android:windowBackground" |         android:background="?attr/android:windowBackground" | ||||||
|         app:headerLayout="@layout/drawer_header"/> |         app:headerLayout="@layout/drawer_header" | ||||||
|  |         android:theme="@style/NavViewTextStyle"/> | ||||||
|         <!-- app:menu="@menu/drawer_items" --> |         <!-- app:menu="@menu/drawer_items" --> | ||||||
|  |  | ||||||
|     <LinearLayout |     <LinearLayout | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								app/src/main/res/layout/fragment_instance_list.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:orientation="vertical"> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/instanceHelpTV" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_margin="15dp" | ||||||
|  |         android:autoLink="web" | ||||||
|  |         android:text="@string/peertube_instance_url_help"/> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/instances" | ||||||
|  |         android:layout_below="@id/instanceHelpTV" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         tools:listitem="@layout/item_instance" /> | ||||||
|  |  | ||||||
|  |     <!-- LOADING INDICATOR--> | ||||||
|  |     <ProgressBar | ||||||
|  |         android:id="@+id/loading_progress_bar" | ||||||
|  |         style="@style/Widget.AppCompat.ProgressBar" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerInParent="true" | ||||||
|  |         android:indeterminate="true" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         tools:visibility="visible" /> | ||||||
|  |  | ||||||
|  |     <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||||
|  |         android:id="@+id/addInstanceButton" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_alignParentEnd="true" | ||||||
|  |         android:layout_alignParentRight="true" | ||||||
|  |         android:layout_alignParentBottom="true" | ||||||
|  |         android:layout_marginEnd="16dp" | ||||||
|  |         android:layout_marginRight="16dp" | ||||||
|  |         android:layout_marginBottom="16dp" | ||||||
|  |         android:clickable="true" | ||||||
|  |         android:focusable="true" | ||||||
|  |         app:backgroundTint="?attr/colorPrimary" | ||||||
|  |         app:fabSize="auto" | ||||||
|  |         app:srcCompat="?attr/ic_add" /> | ||||||
|  |  | ||||||
|  | </RelativeLayout> | ||||||
							
								
								
									
										6
									
								
								app/src/main/res/layout/instance_spinner_item.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <TextView xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:id="@android:id/text1" | ||||||
|  |     android:layout_width="wrap_content" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:maxLength="0" /> | ||||||
							
								
								
									
										9
									
								
								app/src/main/res/layout/instance_spinner_layout.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <Spinner xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:id="@+id/spinner" | ||||||
|  |     tools:listitem="@layout/instance_spinner_item" | ||||||
|  |     android:layout_width="wrap_content" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:gravity="end" | ||||||
|  |     android:prompt="@string/choose_instance_prompt" /> | ||||||
							
								
								
									
										83
									
								
								app/src/main/res/layout/item_instance.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,83 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.cardview.widget.CardView | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:id="@+id/layoutCard" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:layout_marginBottom="3dp" | ||||||
|  |     android:layout_marginLeft="5dp" | ||||||
|  |     android:layout_marginRight="5dp" | ||||||
|  |     android:layout_marginTop="3dp" | ||||||
|  |     android:minHeight="?listPreferredItemHeightSmall" | ||||||
|  |     android:orientation="horizontal" | ||||||
|  |     app:cardCornerRadius="5dp" | ||||||
|  |     app:cardElevation="4dp"> | ||||||
|  |  | ||||||
|  |     <RelativeLayout | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:layout_gravity="center_vertical"> | ||||||
|  |  | ||||||
|  |         <androidx.appcompat.widget.AppCompatImageView | ||||||
|  |             android:id="@+id/instanceIcon" | ||||||
|  |             android:layout_width="24dp" | ||||||
|  |             android:layout_height="24dp" | ||||||
|  |             android:layout_centerVertical="true" | ||||||
|  |             android:layout_alignParentLeft="true" | ||||||
|  |             android:layout_marginLeft="10dp" | ||||||
|  |             tools:ignore="ContentDescription,RtlHardcoded" | ||||||
|  |             tools:src="@drawable/place_holder_peertube"/> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/instanceName" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginLeft="10dp" | ||||||
|  |             android:layout_marginTop="6dp" | ||||||
|  |             android:layout_toRightOf="@+id/instanceIcon" | ||||||
|  |             android:layout_toLeftOf="@id/selectInstanceRB" | ||||||
|  |             android:singleLine="true" | ||||||
|  |             android:ellipsize="marquee" | ||||||
|  |             android:textAppearance="?textAppearanceListItem" | ||||||
|  |             tools:ignore="RtlHardcoded" | ||||||
|  |             tools:text="Framatube"/> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/instanceUrl" | ||||||
|  |             android:autoLink="web" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginBottom="6dp" | ||||||
|  |             android:layout_marginLeft="10dp" | ||||||
|  |             android:layout_toRightOf="@id/instanceIcon" | ||||||
|  |             android:layout_toLeftOf="@id/selectInstanceRB" | ||||||
|  |             android:layout_below="@id/instanceName" | ||||||
|  |             android:singleLine="true" | ||||||
|  |             android:ellipsize="marquee" | ||||||
|  |             android:textAppearance="?textAppearanceListItemSecondary" | ||||||
|  |             tools:ignore="RtlHardcoded" | ||||||
|  |             tools:text="https://framatube.org"/> | ||||||
|  |  | ||||||
|  |         <RadioButton | ||||||
|  |             android:id="@+id/selectInstanceRB" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_toLeftOf="@id/handle" | ||||||
|  |             android:layout_centerVertical="true"/> | ||||||
|  |  | ||||||
|  |         <androidx.appcompat.widget.AppCompatImageView | ||||||
|  |             android:id="@+id/handle" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_alignParentRight="true" | ||||||
|  |             android:layout_centerVertical="true" | ||||||
|  |             android:paddingBottom="12dp" | ||||||
|  |             android:paddingLeft="16dp" | ||||||
|  |             android:paddingRight="10dp" | ||||||
|  |             android:paddingTop="12dp" | ||||||
|  |             android:src="?attr/drag_handle" | ||||||
|  |             tools:ignore="ContentDescription,RtlHardcoded"/> | ||||||
|  |     </RelativeLayout> | ||||||
|  | </androidx.cardview.widget.CardView> | ||||||
| @@ -468,11 +468,9 @@ | |||||||
|     <string name="error_connect_host">لا يمكن الاتصال بالخادم</string> |     <string name="error_connect_host">لا يمكن الاتصال بالخادم</string> | ||||||
|     <string name="error_http_no_content">الخادم لايقوم بإرسال البيانات</string> |     <string name="error_http_no_content">الخادم لايقوم بإرسال البيانات</string> | ||||||
|     <string name="error_http_unsupported_range">الخادم لا يقبل التنزيل المتعدد، إعادة المحاولة مع @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">الخادم لا يقبل التنزيل المتعدد، إعادة المحاولة مع @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">عدم استيفاء النطاق المطلوب</string> |  | ||||||
|     <string name="error_http_not_found">غير موجود</string> |     <string name="error_http_not_found">غير موجود</string> | ||||||
|     <string name="error_postprocessing_failed">فشلت المعالجة الاولية</string> |     <string name="error_postprocessing_failed">فشلت المعالجة الاولية</string> | ||||||
|     <string name="clear_finished_download">حذف التنزيلات المنتهية</string> |     <string name="clear_finished_download">حذف التنزيلات المنتهية</string> | ||||||
|     <string name="msg_pending_downloads">"قم بإستكمال  %s حيثما  يتم التحويل من التنزيلات"</string> |  | ||||||
|     <string name="stop">توقف</string> |     <string name="stop">توقف</string> | ||||||
|     <string name="max_retry_msg">أقصى عدد للمحاولات</string> |     <string name="max_retry_msg">أقصى عدد للمحاولات</string> | ||||||
|     <string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string> |     <string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string> | ||||||
|   | |||||||
| @@ -455,11 +455,9 @@ | |||||||
|     <string name="error_connect_host">Немагчыма злучыцца з серверам</string> |     <string name="error_connect_host">Немагчыма злучыцца з серверам</string> | ||||||
|     <string name="error_http_no_content">Не атрымалася атрымаць дадзеныя з сервера</string> |     <string name="error_http_no_content">Не атрымалася атрымаць дадзеныя з сервера</string> | ||||||
|     <string name="error_http_unsupported_range">Сервер не падтрымлівае шматструменную загрузку, паспрабуйце з @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Сервер не падтрымлівае шматструменную загрузку, паспрабуйце з @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Запытаны дыяпазон недапушчальны</string> |  | ||||||
|     <string name="error_http_not_found">Не знойдзена</string> |     <string name="error_http_not_found">Не знойдзена</string> | ||||||
|     <string name="error_postprocessing_failed">Пасляапрацоўка не ўдалася</string> |     <string name="error_postprocessing_failed">Пасляапрацоўка не ўдалася</string> | ||||||
|     <string name="clear_finished_download">Ачысціць завершаныя</string> |     <string name="clear_finished_download">Ачысціць завершаныя</string> | ||||||
|     <string name="msg_pending_downloads">Аднавіць прыпыненыя загрузкі (%s)</string> |  | ||||||
|     <string name="stop">Спыніць</string> |     <string name="stop">Спыніць</string> | ||||||
|     <string name="max_retry_msg">Максімум спробаў</string> |     <string name="max_retry_msg">Максімум спробаў</string> | ||||||
|     <string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string> |     <string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string> | ||||||
|   | |||||||
| @@ -460,8 +460,6 @@ | |||||||
|     <string name="app_update_notification_content_title">NewPipe 更新可用!</string> |     <string name="app_update_notification_content_title">NewPipe 更新可用!</string> | ||||||
|     <string name="error_path_creation">无法创建目标文件夹</string> |     <string name="error_path_creation">无法创建目标文件夹</string> | ||||||
|     <string name="error_http_unsupported_range">服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试</string> |     <string name="error_http_unsupported_range">服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string> |  | ||||||
|     <string name="msg_pending_downloads">继续进行%s个待下载转移</string> |  | ||||||
|     <string name="pause_downloads_on_mobile_desc">切换至移动数据时有用,尽管一些下载无法被暂停</string> |     <string name="pause_downloads_on_mobile_desc">切换至移动数据时有用,尽管一些下载无法被暂停</string> | ||||||
|     <string name="show_comments_title">显示评论</string> |     <string name="show_comments_title">显示评论</string> | ||||||
|     <string name="show_comments_summary">禁用停止显示评论</string> |     <string name="show_comments_summary">禁用停止显示评论</string> | ||||||
|   | |||||||
| @@ -463,11 +463,9 @@ otevření ve vyskakovacím okně</string> | |||||||
|     <string name="error_connect_host">Nelze se připojit k serveru</string> |     <string name="error_connect_host">Nelze se připojit k serveru</string> | ||||||
|     <string name="error_http_no_content">Server neposílá data</string> |     <string name="error_http_no_content">Server neposílá data</string> | ||||||
|     <string name="error_http_unsupported_range">Server neakceptuje vícevláknové stahování, opakujte akci s @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Server neakceptuje vícevláknové stahování, opakujte akci s @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Požadovaný rozsah nelze splnit</string> |  | ||||||
|     <string name="error_http_not_found">Nenalezeno</string> |     <string name="error_http_not_found">Nenalezeno</string> | ||||||
|     <string name="error_postprocessing_failed">Post-processing selhal</string> |     <string name="error_postprocessing_failed">Post-processing selhal</string> | ||||||
|     <string name="clear_finished_download">Vyčistit dokončená stahování</string> |     <string name="clear_finished_download">Vyčistit dokončená stahování</string> | ||||||
|     <string name="msg_pending_downloads">Pokračovat ve stahování %s souborů, čekajících na stažení</string> |  | ||||||
|     <string name="stop">Zastavit</string> |     <string name="stop">Zastavit</string> | ||||||
|     <string name="max_retry_msg">Maximální počet pokusů o opakování</string> |     <string name="max_retry_msg">Maximální počet pokusů o opakování</string> | ||||||
|     <string name="max_retry_desc">Maximální počet pokusů před zrušením stahování</string> |     <string name="max_retry_desc">Maximální počet pokusů před zrušením stahování</string> | ||||||
|   | |||||||
| @@ -380,7 +380,6 @@ | |||||||
|     <string name="error_connect_host">Kan ikke forbinde til serveren</string> |     <string name="error_connect_host">Kan ikke forbinde til serveren</string> | ||||||
|     <string name="error_http_no_content">Serveren sender ikke data</string> |     <string name="error_http_no_content">Serveren sender ikke data</string> | ||||||
|     <string name="error_http_unsupported_range">Serveren accepterer ikke multitrådede downloads; prøv igen med @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Serveren accepterer ikke multitrådede downloads; prøv igen med @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Det anmodede interval er ikke gyldigt</string> |  | ||||||
|     <string name="error_http_not_found">Ikke fundet</string> |     <string name="error_http_not_found">Ikke fundet</string> | ||||||
|     <string name="error_postprocessing_failed">Efterbehandling fejlede</string> |     <string name="error_postprocessing_failed">Efterbehandling fejlede</string> | ||||||
|     <string name="stop">Stop</string> |     <string name="stop">Stop</string> | ||||||
| @@ -448,7 +447,6 @@ | |||||||
|     <string name="paused">sat på pause</string> |     <string name="paused">sat på pause</string> | ||||||
|     <string name="queued">sat i kø</string> |     <string name="queued">sat i kø</string> | ||||||
|     <string name="clear_finished_download">Ryd færdige downloads</string> |     <string name="clear_finished_download">Ryd færdige downloads</string> | ||||||
|     <string name="msg_pending_downloads">Fortsæt dine %s ventende overførsler fra Downloads</string> |  | ||||||
|     <string name="max_retry_msg">Maksimalt antal genforsøg</string> |     <string name="max_retry_msg">Maksimalt antal genforsøg</string> | ||||||
|     <string name="max_retry_desc">Maksimalt antal forsøg før downloaden opgives</string> |     <string name="max_retry_desc">Maksimalt antal forsøg før downloaden opgives</string> | ||||||
|     <string name="pause_downloads_on_mobile">Sæt på pause ved skift til mobildata</string> |     <string name="pause_downloads_on_mobile">Sæt på pause ved skift til mobildata</string> | ||||||
|   | |||||||
| @@ -454,11 +454,9 @@ | |||||||
|     <string name="error_connect_host">Kann nicht mit dem Server verbinden</string> |     <string name="error_connect_host">Kann nicht mit dem Server verbinden</string> | ||||||
|     <string name="error_http_no_content">Der Server sendet keine Daten</string> |     <string name="error_http_no_content">Der Server sendet keine Daten</string> | ||||||
|     <string name="error_http_unsupported_range">Der Server erlaubt kein mehrfädiges Herunterladen – wiederhole mit @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Der Server erlaubt kein mehrfädiges Herunterladen – wiederhole mit @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Gewünschter Bereich ist nicht verfügbar</string> |  | ||||||
|     <string name="error_http_not_found">Nicht gefunden</string> |     <string name="error_http_not_found">Nicht gefunden</string> | ||||||
|     <string name="error_postprocessing_failed">Nachbearbeitung fehlgeschlagen</string> |     <string name="error_postprocessing_failed">Nachbearbeitung fehlgeschlagen</string> | ||||||
|     <string name="clear_finished_download">Um fertige Downloads bereinigen</string> |     <string name="clear_finished_download">Um fertige Downloads bereinigen</string> | ||||||
|     <string name="msg_pending_downloads">Setze deine %s ausstehenden Übertragungen von Downloads fort</string> |  | ||||||
|     <string name="stop">Anhalten</string> |     <string name="stop">Anhalten</string> | ||||||
|     <string name="max_retry_msg">Maximale Wiederholungen</string> |     <string name="max_retry_msg">Maximale Wiederholungen</string> | ||||||
|     <string name="max_retry_desc">Maximalanzahl der Versuche, bevor der Download abgebrochen wird</string> |     <string name="max_retry_desc">Maximalanzahl der Versuche, bevor der Download abgebrochen wird</string> | ||||||
|   | |||||||
| @@ -456,11 +456,9 @@ | |||||||
|     <string name="error_connect_host">Αδυναμία σύνδεσης με τον εξυπηρετητή</string> |     <string name="error_connect_host">Αδυναμία σύνδεσης με τον εξυπηρετητή</string> | ||||||
|     <string name="error_http_no_content">Ο εξυπηρετητής δεν μπορεί να στείλει τα δεδομένα</string> |     <string name="error_http_no_content">Ο εξυπηρετητής δεν μπορεί να στείλει τα δεδομένα</string> | ||||||
|     <string name="error_http_unsupported_range">Ο εξυπηρετητής δέν υποστηρίζει πολυνηματικές λήψεις, ξαναπροσπαθήστε με @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Ο εξυπηρετητής δέν υποστηρίζει πολυνηματικές λήψεις, ξαναπροσπαθήστε με @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Το ζητούμενο εύρος δεν μπορεί να εξυπηρετηθεί</string> |  | ||||||
|     <string name="error_http_not_found">Δεν βρέθηκε</string> |     <string name="error_http_not_found">Δεν βρέθηκε</string> | ||||||
|     <string name="error_postprocessing_failed">Μετεπεξεργασία απέτυχε</string> |     <string name="error_postprocessing_failed">Μετεπεξεργασία απέτυχε</string> | ||||||
|     <string name="clear_finished_download">Εκκαθάριση ολοκληρωμένων λήψεων</string> |     <string name="clear_finished_download">Εκκαθάριση ολοκληρωμένων λήψεων</string> | ||||||
|     <string name="msg_pending_downloads">Συνέχιση των %s εκκρεμών σας λήψεων</string> |  | ||||||
|     <string name="stop">Διακοπή</string> |     <string name="stop">Διακοπή</string> | ||||||
|     <string name="max_retry_msg">Μέγιστες επαναπροσπάθειες</string> |     <string name="max_retry_msg">Μέγιστες επαναπροσπάθειες</string> | ||||||
|     <string name="max_retry_desc">Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης</string> |     <string name="max_retry_desc">Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης</string> | ||||||
|   | |||||||
| @@ -351,8 +351,8 @@ | |||||||
| \n3. Inicie sesión cuando se le pida  | \n3. Inicie sesión cuando se le pida  | ||||||
| \n4. Copie la URL del perfil a la que fue redireccionado.</string> | \n4. Copie la URL del perfil a la que fue redireccionado.</string> | ||||||
|     <string name="import_soundcloud_instructions_hint">suID, soundcloud.com/suID</string> |     <string name="import_soundcloud_instructions_hint">suID, soundcloud.com/suID</string> | ||||||
|     <string name="import_network_expensive_warning">Observe que esta operación puede causar un uso intensivo de la red.  |     <string name="import_network_expensive_warning">Observe que esta operación puede causar un uso intensivo de la red. | ||||||
| \n  | \n | ||||||
| \n¿Quiere continuar\?</string> | \n¿Quiere continuar\?</string> | ||||||
|     <string name="download_thumbnail_title">Cargar miniaturas</string> |     <string name="download_thumbnail_title">Cargar miniaturas</string> | ||||||
|     <string name="download_thumbnail_summary">Desactívela para evitar la carga de miniaturas y ahorrar datos y memoria. Se vaciará la antememoria de imágenes en la memoria volátil y en el disco.</string> |     <string name="download_thumbnail_summary">Desactívela para evitar la carga de miniaturas y ahorrar datos y memoria. Se vaciará la antememoria de imágenes en la memoria volátil y en el disco.</string> | ||||||
| @@ -406,6 +406,7 @@ | |||||||
|     <string name="paused">pausado</string> |     <string name="paused">pausado</string> | ||||||
|     <string name="queued">en cola</string> |     <string name="queued">en cola</string> | ||||||
|     <string name="post_processing">posprocesamiento</string> |     <string name="post_processing">posprocesamiento</string> | ||||||
|  |     <string name="recovering">recuperando</string> | ||||||
|     <string name="enqueue">Añadir a cola</string> |     <string name="enqueue">Añadir a cola</string> | ||||||
|     <string name="permission_denied">Acción denegada por el sistema</string> |     <string name="permission_denied">Acción denegada por el sistema</string> | ||||||
|     <string name="file_deleted">Se eliminó el archivo</string> |     <string name="file_deleted">Se eliminó el archivo</string> | ||||||
| @@ -424,7 +425,6 @@ | |||||||
|     <string name="grid">Mostrar como grilla</string> |     <string name="grid">Mostrar como grilla</string> | ||||||
|     <string name="list">Mostrar como lista</string> |     <string name="list">Mostrar como lista</string> | ||||||
|     <string name="clear_finished_download">Limpiar descargas finalizadas</string> |     <string name="clear_finished_download">Limpiar descargas finalizadas</string> | ||||||
|     <string name="msg_pending_downloads">Tienes %s descargas pendientes, ve a Descargas para continuarlas</string> |  | ||||||
|     <string name="confirm_prompt">¿Lo confirma\?</string> |     <string name="confirm_prompt">¿Lo confirma\?</string> | ||||||
|     <string name="stop">Detener</string> |     <string name="stop">Detener</string> | ||||||
|     <string name="max_retry_msg">Intentos máximos</string> |     <string name="max_retry_msg">Intentos máximos</string> | ||||||
| @@ -444,8 +444,8 @@ | |||||||
|     <string name="error_ssl_exception">Fallo la conexión segura</string> |     <string name="error_ssl_exception">Fallo la conexión segura</string> | ||||||
|     <string name="error_unknown_host">No se pudo encontrar el servidor</string> |     <string name="error_unknown_host">No se pudo encontrar el servidor</string> | ||||||
|     <string name="error_connect_host">No se puede conectar con el servidor</string> |     <string name="error_connect_host">No se puede conectar con el servidor</string> | ||||||
|     <string name="error_http_no_content">El servidor no está enviando datos</string> |     <string name="error_http_no_content">El servidor no devolvio datos</string> | ||||||
|     <string name="error_http_unsupported_range">El servidor no acepta descargas multiproceso; intente de nuevo con @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">No se puede satisfacer el intervalo seleccionado</string> |     <string name="error_http_requested_range_not_satisfiable">No se puede satisfacer el intervalo seleccionado</string> | ||||||
|     <string name="error_http_not_found">No encontrado</string> |     <string name="error_http_not_found">No encontrado</string> | ||||||
|     <string name="error_postprocessing_failed">Falló el posprocesamiento</string> |     <string name="error_postprocessing_failed">Falló el posprocesamiento</string> | ||||||
| @@ -453,6 +453,7 @@ | |||||||
|     <string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string> |     <string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string> | ||||||
|     <string name="error_progress_lost">Se perdió el progreso porque el archivo fue eliminado</string> |     <string name="error_progress_lost">Se perdió el progreso porque el archivo fue eliminado</string> | ||||||
|     <string name="error_timeout">Tiempo de espera excedido</string> |     <string name="error_timeout">Tiempo de espera excedido</string> | ||||||
|  |     <string name="error_download_resource_gone">No se puede recuperar esta descarga</string> | ||||||
|     <string name="downloads_storage_ask_title">Preguntar dónde descargar</string> |     <string name="downloads_storage_ask_title">Preguntar dónde descargar</string> | ||||||
|     <string name="downloads_storage_ask_summary">Se preguntará dónde guardar cada descarga</string> |     <string name="downloads_storage_ask_summary">Se preguntará dónde guardar cada descarga</string> | ||||||
|     <string name="downloads_storage_ask_summary_kitkat">Se le preguntará dónde guardar cada descarga. |     <string name="downloads_storage_ask_summary_kitkat">Se le preguntará dónde guardar cada descarga. | ||||||
|   | |||||||
| @@ -457,11 +457,9 @@ | |||||||
|     <string name="error_connect_host">Serveriga ei saadud ühendust</string> |     <string name="error_connect_host">Serveriga ei saadud ühendust</string> | ||||||
|     <string name="error_http_no_content">Server ei saada andmeid</string> |     <string name="error_http_no_content">Server ei saada andmeid</string> | ||||||
|     <string name="error_http_unsupported_range">Server ei toeta mitmelõimelisi allalaadimisi. Proovi uuesti kasutades @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Server ei toeta mitmelõimelisi allalaadimisi. Proovi uuesti kasutades @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Taotletud vahemik ei ole rahuldatav</string> |  | ||||||
|     <string name="error_http_not_found">Ei leitud</string> |     <string name="error_http_not_found">Ei leitud</string> | ||||||
|     <string name="error_postprocessing_failed">Järeltöötlemine nurjus</string> |     <string name="error_postprocessing_failed">Järeltöötlemine nurjus</string> | ||||||
|     <string name="clear_finished_download">Eemalda lõpetatud allalaadimised</string> |     <string name="clear_finished_download">Eemalda lõpetatud allalaadimised</string> | ||||||
|     <string name="msg_pending_downloads">Jätka %s pooleliolevat allalaadimist</string> |  | ||||||
|     <string name="stop">Stopp</string> |     <string name="stop">Stopp</string> | ||||||
|     <string name="max_retry_msg">Korduskatseid</string> |     <string name="max_retry_msg">Korduskatseid</string> | ||||||
|     <string name="max_retry_desc">Suurim katsete arv enne allalaadimise tühistamist</string> |     <string name="max_retry_desc">Suurim katsete arv enne allalaadimise tühistamist</string> | ||||||
|   | |||||||
| @@ -456,11 +456,9 @@ | |||||||
|     <string name="error_connect_host">Ezin da zerbitzariarekin konektatu</string> |     <string name="error_connect_host">Ezin da zerbitzariarekin konektatu</string> | ||||||
|     <string name="error_http_no_content">Zerbitzariak ez du daturik bidaltzen</string> |     <string name="error_http_no_content">Zerbitzariak ez du daturik bidaltzen</string> | ||||||
|     <string name="error_http_unsupported_range">Zerbitzariak ez ditu hainbat hariko deskargak onartzen, saiatu @string/msg_threads = 1 erabilita</string> |     <string name="error_http_unsupported_range">Zerbitzariak ez ditu hainbat hariko deskargak onartzen, saiatu @string/msg_threads = 1 erabilita</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Eskatutako barrutia ezin da bete</string> |  | ||||||
|     <string name="error_http_not_found">Ez aurkitua</string> |     <string name="error_http_not_found">Ez aurkitua</string> | ||||||
|     <string name="error_postprocessing_failed">Post-prozesuak huts egin du</string> |     <string name="error_postprocessing_failed">Post-prozesuak huts egin du</string> | ||||||
|     <string name="clear_finished_download">Garbitu amaitutako deskargak</string> |     <string name="clear_finished_download">Garbitu amaitutako deskargak</string> | ||||||
|     <string name="msg_pending_downloads">Berrekin burutzeke dauden %s transferentzia deskargetatik</string> |  | ||||||
|     <string name="stop">Gelditu</string> |     <string name="stop">Gelditu</string> | ||||||
|     <string name="max_retry_msg">Gehienezko saiakerak</string> |     <string name="max_retry_msg">Gehienezko saiakerak</string> | ||||||
|     <string name="max_retry_desc">Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua</string> |     <string name="max_retry_desc">Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua</string> | ||||||
|   | |||||||
| @@ -466,8 +466,6 @@ | |||||||
|     <string name="max_retry_desc">Nombre maximum de tentatives avant d’annuler le téléchargement</string> |     <string name="max_retry_desc">Nombre maximum de tentatives avant d’annuler le téléchargement</string> | ||||||
|     <string name="saved_tabs_invalid_json">Utilisation des onglets par défaut, erreur lors de la lecture des onglets enregistrés</string> |     <string name="saved_tabs_invalid_json">Utilisation des onglets par défaut, erreur lors de la lecture des onglets enregistrés</string> | ||||||
|     <string name="error_http_unsupported_range">Le serveur n’accepte pas les téléchargements multi-fils, veuillez réessayer avec @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Le serveur n’accepte pas les téléchargements multi-fils, veuillez réessayer avec @string/msg_threads = 1</string> | ||||||
|     <string name="msg_pending_downloads">Continuer vos %s transferts en attente depuis Téléchargement</string> |  | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Le domaine désiré n\'est pas disponible</string> |  | ||||||
|     <string name="show_comments_title">Afficher les commentaires</string> |     <string name="show_comments_title">Afficher les commentaires</string> | ||||||
|     <string name="show_comments_summary">Désactiver pour ne pas afficher les commentaires</string> |     <string name="show_comments_summary">Désactiver pour ne pas afficher les commentaires</string> | ||||||
|     <string name="autoplay_title">Lecture automatique</string> |     <string name="autoplay_title">Lecture automatique</string> | ||||||
|   | |||||||
| @@ -461,11 +461,9 @@ | |||||||
|     <string name="error_connect_host">לא ניתן להתחבר לשרת</string> |     <string name="error_connect_host">לא ניתן להתחבר לשרת</string> | ||||||
|     <string name="error_http_no_content">השרת לא שולח נתונים</string> |     <string name="error_http_no_content">השרת לא שולח נתונים</string> | ||||||
|     <string name="error_http_unsupported_range">"השרת לא מקבל הורדות רב ערוציות, מוטב לנסות שוב עם @string/msg_threads = 1 "</string> |     <string name="error_http_unsupported_range">"השרת לא מקבל הורדות רב ערוציות, מוטב לנסות שוב עם @string/msg_threads = 1 "</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">הטווח המבוקש לא מתאים</string> |  | ||||||
|     <string name="error_http_not_found">לא נמצא</string> |     <string name="error_http_not_found">לא נמצא</string> | ||||||
|     <string name="error_postprocessing_failed">העיבוד המאוחר נכשל</string> |     <string name="error_postprocessing_failed">העיבוד המאוחר נכשל</string> | ||||||
|     <string name="clear_finished_download">פינוי ההורדות שהסתיימו</string> |     <string name="clear_finished_download">פינוי ההורדות שהסתיימו</string> | ||||||
|     <string name="msg_pending_downloads">ניתן להמשיך את %s ההורדות הממתינות שלך דרך ההורדות</string> |  | ||||||
|     <string name="stop">עצירה</string> |     <string name="stop">עצירה</string> | ||||||
|     <string name="max_retry_msg">מספר הניסיונות החוזרים המרבי</string> |     <string name="max_retry_msg">מספר הניסיונות החוזרים המרבי</string> | ||||||
|     <string name="max_retry_desc">מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה</string> |     <string name="max_retry_desc">מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה</string> | ||||||
|   | |||||||
| @@ -454,11 +454,9 @@ | |||||||
|     <string name="error_connect_host">Nije moguće povezati se s serverom</string> |     <string name="error_connect_host">Nije moguće povezati se s serverom</string> | ||||||
|     <string name="error_http_no_content">Server ne šalje podatke</string> |     <string name="error_http_no_content">Server ne šalje podatke</string> | ||||||
|     <string name="error_http_unsupported_range">Poslužitelj ne prihvaća preuzimanja s više niti, pokušaj ponovo s @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Poslužitelj ne prihvaća preuzimanja s više niti, pokušaj ponovo s @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Traženi raspon nije zadovoljavajući</string> |  | ||||||
|     <string name="error_http_not_found">Nije pronađeno</string> |     <string name="error_http_not_found">Nije pronađeno</string> | ||||||
|     <string name="error_postprocessing_failed">Naknadna obrada nije uspjela</string> |     <string name="error_postprocessing_failed">Naknadna obrada nije uspjela</string> | ||||||
|     <string name="clear_finished_download">Obriši završena preuzimanja</string> |     <string name="clear_finished_download">Obriši završena preuzimanja</string> | ||||||
|     <string name="msg_pending_downloads">Nastavite s prijenosima na čekanju za %s s preuzimanja</string> |  | ||||||
|     <string name="stop">Stop</string> |     <string name="stop">Stop</string> | ||||||
|     <string name="max_retry_msg">Maksimalnih ponovnih pokušaja</string> |     <string name="max_retry_msg">Maksimalnih ponovnih pokušaja</string> | ||||||
|     <string name="max_retry_desc">Maksimalni broj pokušaja prije poništavanja preuzimanja</string> |     <string name="max_retry_desc">Maksimalni broj pokušaja prije poništavanja preuzimanja</string> | ||||||
|   | |||||||
| @@ -450,11 +450,9 @@ | |||||||
|     <string name="error_connect_host">Tidak dapat terhubung ke server</string> |     <string name="error_connect_host">Tidak dapat terhubung ke server</string> | ||||||
|     <string name="error_http_no_content">Server tidak mengirim data</string> |     <string name="error_http_no_content">Server tidak mengirim data</string> | ||||||
|     <string name="error_http_unsupported_range">Server tidak menerima unduhan multi-utas, coba lagi dengan @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Server tidak menerima unduhan multi-utas, coba lagi dengan @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Rentang yang diminta tidak memuaskan</string> |  | ||||||
|     <string name="error_http_not_found">Tidak ditemukan</string> |     <string name="error_http_not_found">Tidak ditemukan</string> | ||||||
|     <string name="error_postprocessing_failed">Pengolahan-pasca gagal</string> |     <string name="error_postprocessing_failed">Pengolahan-pasca gagal</string> | ||||||
|     <string name="clear_finished_download">Hapus unduhan yang sudah selesai</string> |     <string name="clear_finished_download">Hapus unduhan yang sudah selesai</string> | ||||||
|     <string name="msg_pending_downloads">Lanjutkan %s transfer anda yang tertunda dari Unduhan</string> |  | ||||||
|     <string name="stop">Berhenti</string> |     <string name="stop">Berhenti</string> | ||||||
|     <string name="max_retry_msg">Percobaan maksimum</string> |     <string name="max_retry_msg">Percobaan maksimum</string> | ||||||
|     <string name="max_retry_desc">Jumlah upaya maksimum sebelum membatalkan unduhan</string> |     <string name="max_retry_desc">Jumlah upaya maksimum sebelum membatalkan unduhan</string> | ||||||
|   | |||||||
| @@ -454,11 +454,9 @@ | |||||||
|     <string name="error_connect_host">Impossibile connettersi al server</string> |     <string name="error_connect_host">Impossibile connettersi al server</string> | ||||||
|     <string name="error_http_no_content">Il server non invia dati</string> |     <string name="error_http_no_content">Il server non invia dati</string> | ||||||
|     <string name="error_http_unsupported_range">Il server non accetta download multipli, riprovare con @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Il server non accetta download multipli, riprovare con @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Intervallo richiesto non soddisfatto</string> |  | ||||||
|     <string name="error_http_not_found">Non trovato</string> |     <string name="error_http_not_found">Non trovato</string> | ||||||
|     <string name="error_postprocessing_failed">Post-processing fallito</string> |     <string name="error_postprocessing_failed">Post-processing fallito</string> | ||||||
|     <string name="clear_finished_download">Pulisci i download completati</string> |     <string name="clear_finished_download">Pulisci i download completati</string> | ||||||
|     <string name="msg_pending_downloads">Continua i %s trasferimenti in corso dai Download</string> |  | ||||||
|     <string name="stop">Ferma</string> |     <string name="stop">Ferma</string> | ||||||
|     <string name="max_retry_msg">Tentativi massimi</string> |     <string name="max_retry_msg">Tentativi massimi</string> | ||||||
|     <string name="max_retry_desc">Tentativi massimi prima di cancellare il download</string> |     <string name="max_retry_desc">Tentativi massimi prima di cancellare il download</string> | ||||||
|   | |||||||
| @@ -440,7 +440,6 @@ | |||||||
|     <string name="error_connect_host">サーバに接続できません</string> |     <string name="error_connect_host">サーバに接続できません</string> | ||||||
|     <string name="error_http_no_content">サーバがデータを送信していません</string> |     <string name="error_http_no_content">サーバがデータを送信していません</string> | ||||||
|     <string name="error_http_unsupported_range">サーバが同時接続ダウンロードを受け付けません。再試行してください @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">サーバが同時接続ダウンロードを受け付けません。再試行してください @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">必要な範囲が満たされていません</string> |  | ||||||
|     <string name="error_http_not_found">見つかりません</string> |     <string name="error_http_not_found">見つかりません</string> | ||||||
|     <string name="error_postprocessing_failed">保存処理に失敗しました</string> |     <string name="error_postprocessing_failed">保存処理に失敗しました</string> | ||||||
|     <string name="clear_finished_download">完了済みを一覧から削除します</string> |     <string name="clear_finished_download">完了済みを一覧から削除します</string> | ||||||
| @@ -457,7 +456,6 @@ | |||||||
|     <string name="saved_tabs_invalid_json">デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました</string> |     <string name="saved_tabs_invalid_json">デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました</string> | ||||||
|     <string name="main_page_content_summary">メインページに表示されるタブ</string> |     <string name="main_page_content_summary">メインページに表示されるタブ</string> | ||||||
|     <string name="updates_setting_description">新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します</string> |     <string name="updates_setting_description">新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します</string> | ||||||
|     <string name="msg_pending_downloads">ダウンロードから %s の保留中の転送を続行します</string> |  | ||||||
|     <string name="pause_downloads_on_mobile">従量制課金ネットワークの割り込み</string> |     <string name="pause_downloads_on_mobile">従量制課金ネットワークの割り込み</string> | ||||||
|     <string name="pause_downloads_on_mobile_desc">モバイルデータ通信に切り替える場合に便利ですが、一部のダウンロードは一時停止できません</string> |     <string name="pause_downloads_on_mobile_desc">モバイルデータ通信に切り替える場合に便利ですが、一部のダウンロードは一時停止できません</string> | ||||||
|     <string name="show_comments_title">コメントを表示</string> |     <string name="show_comments_title">コメントを表示</string> | ||||||
|   | |||||||
| @@ -451,11 +451,9 @@ | |||||||
|     <string name="error_connect_host">서버에 접속할 수 없습니다</string> |     <string name="error_connect_host">서버에 접속할 수 없습니다</string> | ||||||
|     <string name="error_http_no_content">서버가 데이터를 전송하지 않고 있습니다</string> |     <string name="error_http_no_content">서버가 데이터를 전송하지 않고 있습니다</string> | ||||||
|     <string name="error_http_unsupported_range">서버가 다중 스레드 다운로드를 받아들이지 않습니다, @string/msg_threads = 1 를 사용해 다시 시도해보세요</string> |     <string name="error_http_unsupported_range">서버가 다중 스레드 다운로드를 받아들이지 않습니다, @string/msg_threads = 1 를 사용해 다시 시도해보세요</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">요청된 HTTP 범위가 충분하지 않습니다</string> |  | ||||||
|     <string name="error_http_not_found">HTTP 찾을 수 없습니다</string> |     <string name="error_http_not_found">HTTP 찾을 수 없습니다</string> | ||||||
|     <string name="error_postprocessing_failed">후처리 작업이 실패하였습니다</string> |     <string name="error_postprocessing_failed">후처리 작업이 실패하였습니다</string> | ||||||
|     <string name="clear_finished_download">완료된 다운로드 비우기</string> |     <string name="clear_finished_download">완료된 다운로드 비우기</string> | ||||||
|     <string name="msg_pending_downloads">대기중인 %s 다운로드를 지속하세요</string> |  | ||||||
|     <string name="stop">멈추기</string> |     <string name="stop">멈추기</string> | ||||||
|     <string name="max_retry_msg">최대 재시도 횟수</string> |     <string name="max_retry_msg">최대 재시도 횟수</string> | ||||||
|     <string name="max_retry_desc">다운로드를 취소하기 전까지 다시 시도할 최대 횟수</string> |     <string name="max_retry_desc">다운로드를 취소하기 전까지 다시 시도할 최대 횟수</string> | ||||||
|   | |||||||
| @@ -450,11 +450,9 @@ | |||||||
|     <string name="error_connect_host">Tidak dapat menyambung ke server</string> |     <string name="error_connect_host">Tidak dapat menyambung ke server</string> | ||||||
|     <string name="error_http_no_content">Server tidak menghantar data</string> |     <string name="error_http_no_content">Server tidak menghantar data</string> | ||||||
|     <string name="error_http_unsupported_range">Server tidak menerima muat turun berbilang thread, cuba lagi dengan @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Server tidak menerima muat turun berbilang thread, cuba lagi dengan @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Julat yang diminta tidak memuaskan</string> |  | ||||||
|     <string name="error_http_not_found">Tidak ditemui</string> |     <string name="error_http_not_found">Tidak ditemui</string> | ||||||
|     <string name="error_postprocessing_failed">Pemprosesan-pasca gagal</string> |     <string name="error_postprocessing_failed">Pemprosesan-pasca gagal</string> | ||||||
|     <string name="clear_finished_download">Hapuskan senarai muat turun yang selesai</string> |     <string name="clear_finished_download">Hapuskan senarai muat turun yang selesai</string> | ||||||
|     <string name="msg_pending_downloads">Teruskan %s pemindahan anda yang menunggu dari muat turun</string> |  | ||||||
|     <string name="stop">Berhenti</string> |     <string name="stop">Berhenti</string> | ||||||
|     <string name="max_retry_msg">Percubaan maksimum</string> |     <string name="max_retry_msg">Percubaan maksimum</string> | ||||||
|     <string name="max_retry_desc">Jumlah percubaan maksimum sebelum membatalkan muat turun</string> |     <string name="max_retry_desc">Jumlah percubaan maksimum sebelum membatalkan muat turun</string> | ||||||
|   | |||||||
| @@ -458,7 +458,6 @@ | |||||||
|     <string name="error_http_not_found">Ikke funnet</string> |     <string name="error_http_not_found">Ikke funnet</string> | ||||||
|     <string name="error_postprocessing_failed">Etterbehandling mislyktes</string> |     <string name="error_postprocessing_failed">Etterbehandling mislyktes</string> | ||||||
|     <string name="clear_finished_download">Tøm fullførte nedlastinger</string> |     <string name="clear_finished_download">Tøm fullførte nedlastinger</string> | ||||||
|     <string name="msg_pending_downloads">Fortsett dine %s ventende overføringer fra Nedlastinger</string> |  | ||||||
|     <string name="stop">Stopp</string> |     <string name="stop">Stopp</string> | ||||||
|     <string name="max_retry_msg">Maksimalt antall forsøk</string> |     <string name="max_retry_msg">Maksimalt antall forsøk</string> | ||||||
|     <string name="max_retry_desc">Maksimalt antall tilkoblingsforsøk før nedlastingen avblåses</string> |     <string name="max_retry_desc">Maksimalt antall tilkoblingsforsøk før nedlastingen avblåses</string> | ||||||
| @@ -496,7 +495,7 @@ | |||||||
|     <string name="pause_downloads">Sett nedlastinger på pause</string> |     <string name="pause_downloads">Sett nedlastinger på pause</string> | ||||||
|     <string name="downloads_storage_ask_title">Spør om hvor ting skal lastes ned til</string> |     <string name="downloads_storage_ask_title">Spør om hvor ting skal lastes ned til</string> | ||||||
|     <string name="downloads_storage_ask_summary">Du vil bli spurt om hvor hver nedlasting skal plasseres</string> |     <string name="downloads_storage_ask_summary">Du vil bli spurt om hvor hver nedlasting skal plasseres</string> | ||||||
|     <string name="downloads_storage_ask_summary_kitkat">Du vil bli spurt om hvor hver nedlasting skal plasseres.  |     <string name="downloads_storage_ask_summary_kitkat">Du vil bli spurt om hvor hver nedlasting skal plasseres. | ||||||
| \nSkru på SAF hvis du vil laste ned til eksternt SD-kort</string> | \nSkru på SAF hvis du vil laste ned til eksternt SD-kort</string> | ||||||
|     <string name="downloads_storage_use_saf_title">Bruk SAF</string> |     <string name="downloads_storage_use_saf_title">Bruk SAF</string> | ||||||
|     <string name="downloads_storage_use_saf_summary">Lagringstilgangsrammeverk (SAF) tillater nedlastinger til eksternt SD-kort. |     <string name="downloads_storage_use_saf_summary">Lagringstilgangsrammeverk (SAF) tillater nedlastinger til eksternt SD-kort. | ||||||
|   | |||||||
| @@ -454,11 +454,9 @@ | |||||||
|     <string name="error_connect_host">Kan geen verbinding maken met de server</string> |     <string name="error_connect_host">Kan geen verbinding maken met de server</string> | ||||||
|     <string name="error_http_no_content">De server verzendt geen gegevens</string> |     <string name="error_http_no_content">De server verzendt geen gegevens</string> | ||||||
|     <string name="error_http_unsupported_range">De server aanvaardt geen meerdradige downloads, probeert het opnieuw met @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">De server aanvaardt geen meerdradige downloads, probeert het opnieuw met @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Gevraagd bereik niet beschikbaar</string> |  | ||||||
|     <string name="error_http_not_found">Niet gevonden</string> |     <string name="error_http_not_found">Niet gevonden</string> | ||||||
|     <string name="error_postprocessing_failed">Nabewerking mislukt</string> |     <string name="error_postprocessing_failed">Nabewerking mislukt</string> | ||||||
|     <string name="clear_finished_download">Voltooide downloads wissen</string> |     <string name="clear_finished_download">Voltooide downloads wissen</string> | ||||||
|     <string name="msg_pending_downloads">Zet uw %s wachtende downloads verder via Downloads</string> |  | ||||||
|     <string name="stop">Stoppen</string> |     <string name="stop">Stoppen</string> | ||||||
|     <string name="max_retry_msg">Maximaal aantal pogingen</string> |     <string name="max_retry_msg">Maximaal aantal pogingen</string> | ||||||
|     <string name="max_retry_desc">Maximaal aantal pogingen vooraleer dat den download wordt geannuleerd</string> |     <string name="max_retry_desc">Maximaal aantal pogingen vooraleer dat den download wordt geannuleerd</string> | ||||||
|   | |||||||
| @@ -454,11 +454,9 @@ | |||||||
|     <string name="error_connect_host">Kan niet met de server verbinden</string> |     <string name="error_connect_host">Kan niet met de server verbinden</string> | ||||||
|     <string name="error_http_no_content">De server verzendt geen gegevens</string> |     <string name="error_http_no_content">De server verzendt geen gegevens</string> | ||||||
|     <string name="error_http_unsupported_range">De server accepteert geen multi-threaded downloads, probeer het opnieuw met @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">De server accepteert geen multi-threaded downloads, probeer het opnieuw met @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Gevraagde bereik niet beschikbaar</string> |  | ||||||
|     <string name="error_http_not_found">Niet gevonden</string> |     <string name="error_http_not_found">Niet gevonden</string> | ||||||
|     <string name="error_postprocessing_failed">Nabewerking mislukt</string> |     <string name="error_postprocessing_failed">Nabewerking mislukt</string> | ||||||
|     <string name="clear_finished_download">Voltooide downloads wissen</string> |     <string name="clear_finished_download">Voltooide downloads wissen</string> | ||||||
|     <string name="msg_pending_downloads">Zet je %s wachtende downloads voort via Downloads</string> |  | ||||||
|     <string name="stop">Stop</string> |     <string name="stop">Stop</string> | ||||||
|     <string name="max_retry_msg">Maximum aantal keer proberen</string> |     <string name="max_retry_msg">Maximum aantal keer proberen</string> | ||||||
|     <string name="max_retry_desc">Maximum aantal pogingen voordat de download wordt geannuleerd</string> |     <string name="max_retry_desc">Maximum aantal pogingen voordat de download wordt geannuleerd</string> | ||||||
|   | |||||||
| @@ -450,11 +450,9 @@ | |||||||
|     <string name="error_connect_host">ਸਰਵਰ ਨਾਲ ਜੁੜ ਨਹੀਂ ਸਕਦਾ</string> |     <string name="error_connect_host">ਸਰਵਰ ਨਾਲ ਜੁੜ ਨਹੀਂ ਸਕਦਾ</string> | ||||||
|     <string name="error_http_no_content">ਸਰਵਰ ਨੇ ਡਾਟਾ ਨਹੀਂ ਭੇਜਿਆ</string> |     <string name="error_http_no_content">ਸਰਵਰ ਨੇ ਡਾਟਾ ਨਹੀਂ ਭੇਜਿਆ</string> | ||||||
|     <string name="error_http_unsupported_range">ਸਰਵਰ ਮਲਟੀ-Threaded ਡਾਊਨਲੋਡਸ ਨੂੰ ਸਵੀਕਾਰ ਨਹੀਂ ਕਰਦਾ, ਇਸ ਨਾਲ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">ਸਰਵਰ ਮਲਟੀ-Threaded ਡਾਊਨਲੋਡਸ ਨੂੰ ਸਵੀਕਾਰ ਨਹੀਂ ਕਰਦਾ, ਇਸ ਨਾਲ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">ਬੇਨਤੀ ਕੀਤੀ ਸੀਮਾ ਤਸੱਲੀਬਖਸ਼ ਨਹੀਂ ਹੈ</string> |  | ||||||
|     <string name="error_http_not_found">ਨਹੀਂ ਲਭਿਆ</string> |     <string name="error_http_not_found">ਨਹੀਂ ਲਭਿਆ</string> | ||||||
|     <string name="error_postprocessing_failed">Post-processing ਫੇਲ੍ਹ</string> |     <string name="error_postprocessing_failed">Post-processing ਫੇਲ੍ਹ</string> | ||||||
|     <string name="clear_finished_download">ਮੁਕੰਮਲ ਹੋਈਆਂ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ</string> |     <string name="clear_finished_download">ਮੁਕੰਮਲ ਹੋਈਆਂ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ</string> | ||||||
|     <string name="msg_pending_downloads">ਡਾਉਨਲੋਡਸ ਤੋਂ ਆਪਣੀਆਂ %s ਬਕਾਇਆ ਟ੍ਰਾਂਸਫਰ ਜਾਰੀ ਰੱਖੋ</string> |  | ||||||
|     <string name="stop">ਰੁੱਕੋ</string> |     <string name="stop">ਰੁੱਕੋ</string> | ||||||
|     <string name="max_retry_msg">ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string> |     <string name="max_retry_msg">ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string> | ||||||
|     <string name="max_retry_desc">ਡਾਉਨਲੋਡ ਰੱਦ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string> |     <string name="max_retry_desc">ਡਾਉਨਲੋਡ ਰੱਦ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string> | ||||||
|   | |||||||
| @@ -456,11 +456,9 @@ | |||||||
|     <string name="error_connect_host">Nie można połączyć się z serwerem</string> |     <string name="error_connect_host">Nie można połączyć się z serwerem</string> | ||||||
|     <string name="error_http_no_content">Serwer nie wysyła danych</string> |     <string name="error_http_no_content">Serwer nie wysyła danych</string> | ||||||
|     <string name="error_http_unsupported_range">Serwer nie akceptuje pobierania wielowątkowego, spróbuj ponownie za pomocą @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Serwer nie akceptuje pobierania wielowątkowego, spróbuj ponownie za pomocą @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Niewłaściwy zakres</string> |  | ||||||
|     <string name="error_http_not_found">Nie znaleziono</string> |     <string name="error_http_not_found">Nie znaleziono</string> | ||||||
|     <string name="error_postprocessing_failed">Przetwarzanie końcowe nie powiodło się</string> |     <string name="error_postprocessing_failed">Przetwarzanie końcowe nie powiodło się</string> | ||||||
|     <string name="clear_finished_download">Wyczyść ukończone pobieranie</string> |     <string name="clear_finished_download">Wyczyść ukończone pobieranie</string> | ||||||
|     <string name="msg_pending_downloads">Kontynuuj %s oczekujące transfery z plików do pobrania</string> |  | ||||||
|     <string name="stop">Zatrzymaj</string> |     <string name="stop">Zatrzymaj</string> | ||||||
|     <string name="max_retry_msg">Maksymalna liczba powtórzeń</string> |     <string name="max_retry_msg">Maksymalna liczba powtórzeń</string> | ||||||
|     <string name="max_retry_desc">Maksymalna liczba prób przed anulowaniem pobierania</string> |     <string name="max_retry_desc">Maksymalna liczba prób przed anulowaniem pobierania</string> | ||||||
|   | |||||||
| @@ -463,11 +463,9 @@ abrir em modo popup</string> | |||||||
|     <string name="error_connect_host">Não foi possível conectar ao servidor</string> |     <string name="error_connect_host">Não foi possível conectar ao servidor</string> | ||||||
|     <string name="error_http_no_content">O servidor não envia dados</string> |     <string name="error_http_no_content">O servidor não envia dados</string> | ||||||
|     <string name="error_http_unsupported_range">O servidor não aceita downloads em multi-thread, tente com @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">O servidor não aceita downloads em multi-thread, tente com @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Intervalo solicitado não aceito</string> |  | ||||||
|     <string name="error_http_not_found">Não encontrado</string> |     <string name="error_http_not_found">Não encontrado</string> | ||||||
|     <string name="error_postprocessing_failed">Falha no pós processamento</string> |     <string name="error_postprocessing_failed">Falha no pós processamento</string> | ||||||
|     <string name="clear_finished_download">Limpar downloads finalizados</string> |     <string name="clear_finished_download">Limpar downloads finalizados</string> | ||||||
|     <string name="msg_pending_downloads">Continuar seus %s downloads pendentes</string> |  | ||||||
|     <string name="stop">Parar</string> |     <string name="stop">Parar</string> | ||||||
|     <string name="max_retry_msg">Tentativas Máximas</string> |     <string name="max_retry_msg">Tentativas Máximas</string> | ||||||
|     <string name="max_retry_desc">Número máximo de tentativas antes de cancelar o download</string> |     <string name="max_retry_desc">Número máximo de tentativas antes de cancelar o download</string> | ||||||
|   | |||||||
| @@ -452,11 +452,9 @@ | |||||||
|     <string name="error_connect_host">Não é possível ligar ao servidor</string> |     <string name="error_connect_host">Não é possível ligar ao servidor</string> | ||||||
|     <string name="error_http_no_content">O servidor não envia dados</string> |     <string name="error_http_no_content">O servidor não envia dados</string> | ||||||
|     <string name="error_http_unsupported_range">O servidor não aceita transferências de vários processos, tente novamente com @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">O servidor não aceita transferências de vários processos, tente novamente com @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Intervalo solicitado não satisfatório</string> |  | ||||||
|     <string name="error_http_not_found">Não encontrado</string> |     <string name="error_http_not_found">Não encontrado</string> | ||||||
|     <string name="error_postprocessing_failed">Pós-processamento falhado</string> |     <string name="error_postprocessing_failed">Pós-processamento falhado</string> | ||||||
|     <string name="clear_finished_download">Limpar transferências concluídas</string> |     <string name="clear_finished_download">Limpar transferências concluídas</string> | ||||||
|     <string name="msg_pending_downloads">Continue as suas %s transferências pendentes das Transferências</string> |  | ||||||
|     <string name="stop">Parar</string> |     <string name="stop">Parar</string> | ||||||
|     <string name="max_retry_msg">Tentativas máximas</string> |     <string name="max_retry_msg">Tentativas máximas</string> | ||||||
|     <string name="max_retry_desc">Número máximo de tentativas antes de cancelar a transferência</string> |     <string name="max_retry_desc">Número máximo de tentativas antes de cancelar a transferência</string> | ||||||
|   | |||||||
| @@ -454,7 +454,6 @@ | |||||||
|     <string name="error_permission_denied">Доступ запрещён системой</string> |     <string name="error_permission_denied">Доступ запрещён системой</string> | ||||||
|     <string name="error_unknown_host">Сервер не найден</string> |     <string name="error_unknown_host">Сервер не найден</string> | ||||||
|     <string name="error_http_unsupported_range">Сервер не принимает многопоточные загрузки, повторная попытка с @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Сервер не принимает многопоточные загрузки, повторная попытка с @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Запрашиваемый диапазон недопустим</string> |  | ||||||
|     <string name="error_http_not_found">Не найдено</string> |     <string name="error_http_not_found">Не найдено</string> | ||||||
|     <string name="clear_finished_download">Очистить завершённые</string> |     <string name="clear_finished_download">Очистить завершённые</string> | ||||||
|     <string name="stop">Остановить</string> |     <string name="stop">Остановить</string> | ||||||
| @@ -465,7 +464,6 @@ | |||||||
|     <string name="download_finished">Загрузка завершена</string> |     <string name="download_finished">Загрузка завершена</string> | ||||||
|     <string name="download_finished_more">%s загрузок завершено</string> |     <string name="download_finished_more">%s загрузок завершено</string> | ||||||
|     <string name="generate_unique_name">Создать уникальное имя</string> |     <string name="generate_unique_name">Создать уникальное имя</string> | ||||||
|     <string name="msg_pending_downloads">Возобновить приостановленные загрузки (%s)</string> |  | ||||||
|     <string name="max_retry_msg">Максимум попыток</string> |     <string name="max_retry_msg">Максимум попыток</string> | ||||||
|     <string name="max_retry_desc">Количество попыток перед отменой загрузки</string> |     <string name="max_retry_desc">Количество попыток перед отменой загрузки</string> | ||||||
|     <string name="pause_downloads_on_mobile_desc">Некоторые загрузки не поддерживают докачку и начнутся с начала</string> |     <string name="pause_downloads_on_mobile_desc">Некоторые загрузки не поддерживают докачку и начнутся с начала</string> | ||||||
|   | |||||||
| @@ -462,11 +462,9 @@ | |||||||
|     <string name="error_connect_host">Nepodarilo sa pripojiť k serveru</string> |     <string name="error_connect_host">Nepodarilo sa pripojiť k serveru</string> | ||||||
|     <string name="error_http_no_content">Server neposiela údaje</string> |     <string name="error_http_no_content">Server neposiela údaje</string> | ||||||
|     <string name="error_http_unsupported_range">Server neakceptuje preberanie viacerých vlákien, zopakujte s @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">Server neakceptuje preberanie viacerých vlákien, zopakujte s @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Požadovaný rozsah nie je uspokojivý</string> |  | ||||||
|     <string name="error_http_not_found">Nenájdené</string> |     <string name="error_http_not_found">Nenájdené</string> | ||||||
|     <string name="error_postprocessing_failed">Post-spracovanie zlyhalo</string> |     <string name="error_postprocessing_failed">Post-spracovanie zlyhalo</string> | ||||||
|     <string name="clear_finished_download">Vyčistiť dokončené sťahovania</string> |     <string name="clear_finished_download">Vyčistiť dokončené sťahovania</string> | ||||||
|     <string name="msg_pending_downloads">Pokračujte v preberaní %s zo súborov na prevzatie</string> |  | ||||||
|     <string name="stop">Stop</string> |     <string name="stop">Stop</string> | ||||||
|     <string name="max_retry_msg">Maximum opakovaní</string> |     <string name="max_retry_msg">Maximum opakovaní</string> | ||||||
|     <string name="max_retry_desc">Maximálny počet pokusov pred zrušením stiahnutia</string> |     <string name="max_retry_desc">Maximálny počet pokusov pred zrušením stiahnutia</string> | ||||||
|   | |||||||
| @@ -449,11 +449,9 @@ | |||||||
|     <string name="error_connect_host">Sunucuya bağlanılamıyor</string> |     <string name="error_connect_host">Sunucuya bağlanılamıyor</string> | ||||||
|     <string name="error_http_no_content">Sunucu veri göndermiyor</string> |     <string name="error_http_no_content">Sunucu veri göndermiyor</string> | ||||||
|     <string name="error_http_unsupported_range">Sunucu, çok iş parçacıklı indirmeleri kabul etmez, @string/msg_threads = 1 ile yeniden deneyin</string> |     <string name="error_http_unsupported_range">Sunucu, çok iş parçacıklı indirmeleri kabul etmez, @string/msg_threads = 1 ile yeniden deneyin</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">İstenen aralık karşılanamıyor</string> |  | ||||||
|     <string name="error_http_not_found">Bulunamadı</string> |     <string name="error_http_not_found">Bulunamadı</string> | ||||||
|     <string name="error_postprocessing_failed">İşlem sonrası başarısız</string> |     <string name="error_postprocessing_failed">İşlem sonrası başarısız</string> | ||||||
|     <string name="clear_finished_download">Tamamlanan indirmeleri temizle</string> |     <string name="clear_finished_download">Tamamlanan indirmeleri temizle</string> | ||||||
|     <string name="msg_pending_downloads">Beklemedeki %s transferinize İndirmeler\'den devam edin</string> |  | ||||||
|     <string name="stop">Durdur</string> |     <string name="stop">Durdur</string> | ||||||
|     <string name="max_retry_msg">Azami deneme sayısı</string> |     <string name="max_retry_msg">Azami deneme sayısı</string> | ||||||
|     <string name="max_retry_desc">İndirmeyi iptal etmeden önce maksimum deneme sayısı</string> |     <string name="max_retry_desc">İndirmeyi iptal etmeden önce maksimum deneme sayısı</string> | ||||||
|   | |||||||
| @@ -471,8 +471,6 @@ | |||||||
|     <string name="saved_tabs_invalid_json">Помилка зчитування збережених вкладок. Використовую типові вкладки.</string> |     <string name="saved_tabs_invalid_json">Помилка зчитування збережених вкладок. Використовую типові вкладки.</string> | ||||||
|     <string name="main_page_content_summary">Вкладки, що відображаються на головній сторінці</string> |     <string name="main_page_content_summary">Вкладки, що відображаються на головній сторінці</string> | ||||||
|     <string name="updates_setting_description">Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії</string> |     <string name="updates_setting_description">Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Запитуваний діапазон неприпустимий</string> |  | ||||||
|     <string name="msg_pending_downloads">Продовжити ваші %s відкладених переміщень із Завантажень</string> |  | ||||||
|     <string name="pause_downloads_on_mobile_desc">Корисно під час переходу на мобільні дані, хоча деякі завантаження не можуть бути призупинені</string> |     <string name="pause_downloads_on_mobile_desc">Корисно під час переходу на мобільні дані, хоча деякі завантаження не можуть бути призупинені</string> | ||||||
|     <string name="show_comments_title">Показувати коментарі</string> |     <string name="show_comments_title">Показувати коментарі</string> | ||||||
|     <string name="show_comments_summary">Вимнути відображення дописів</string> |     <string name="show_comments_summary">Вимнути відображення дописів</string> | ||||||
|   | |||||||
| @@ -31,6 +31,25 @@ | |||||||
|         <item name="colorAccent">@color/dark_soundcloud_accent_color</item> |         <item name="colorAccent">@color/dark_soundcloud_accent_color</item> | ||||||
|     </style> |     </style> | ||||||
|  |  | ||||||
|  |     <!-- PeerTube --> | ||||||
|  |     <style name="LightTheme.PeerTube" parent="LightTheme.Switchable"> | ||||||
|  |         <item name="colorPrimary">@color/light_peertube_primary_color</item> | ||||||
|  |         <item name="colorPrimaryDark">@color/light_peertube_dark_color</item> | ||||||
|  |         <item name="colorAccent">@color/light_peertube_accent_color</item> | ||||||
|  |     </style> | ||||||
|  |  | ||||||
|  |     <style name="DarkTheme.PeerTube" parent="DarkTheme.Switchable"> | ||||||
|  |         <item name="colorPrimary">@color/dark_peertube_primary_color</item> | ||||||
|  |         <item name="colorPrimaryDark">@color/dark_peertube_dark_color</item> | ||||||
|  |         <item name="colorAccent">@color/dark_peertube_accent_color</item> | ||||||
|  |     </style> | ||||||
|  |  | ||||||
|  |     <style name="BlackTheme.PeerTube" parent="BlackTheme.Switchable"> | ||||||
|  |         <item name="colorPrimary">@color/dark_peertube_primary_color</item> | ||||||
|  |         <item name="colorPrimaryDark">@color/dark_peertube_dark_color</item> | ||||||
|  |         <item name="colorAccent">@color/dark_peertube_accent_color</item> | ||||||
|  |     </style> | ||||||
|  |  | ||||||
|     <!-- Media.ccc --> |     <!-- Media.ccc --> | ||||||
|     <style name="LightTheme.MediaCCC" parent="LightTheme.Switchable"> |     <style name="LightTheme.MediaCCC" parent="LightTheme.Switchable"> | ||||||
|         <item name="colorPrimary">@color/light_media_ccc_primary_color</item> |         <item name="colorPrimary">@color/light_media_ccc_primary_color</item> | ||||||
| @@ -49,4 +68,5 @@ | |||||||
|         <item name="colorPrimaryDark">@color/dark_media_ccc_statusbar_color</item> |         <item name="colorPrimaryDark">@color/dark_media_ccc_statusbar_color</item> | ||||||
|         <item name="colorAccent">@color/dark_media_ccc_accent_color</item> |         <item name="colorAccent">@color/dark_media_ccc_accent_color</item> | ||||||
|     </style> |     </style> | ||||||
|  |  | ||||||
| </resources> | </resources> | ||||||
| @@ -449,11 +449,9 @@ | |||||||
|     <string name="error_connect_host">Không thế kết nối với máy chủ</string> |     <string name="error_connect_host">Không thế kết nối với máy chủ</string> | ||||||
|     <string name="error_http_no_content">Máy chủ không gửi dữ liệu về</string> |     <string name="error_http_no_content">Máy chủ không gửi dữ liệu về</string> | ||||||
|     <string name="error_http_unsupported_range">Máy chủ không chấp nhận tải đa luồng, thử lại với số luồng = 1</string> |     <string name="error_http_unsupported_range">Máy chủ không chấp nhận tải đa luồng, thử lại với số luồng = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">(HTTP) Không thể đáp ứng khoảng dữ liệu đã yêu cầu</string> |  | ||||||
|     <string name="error_http_not_found">Không tìm thấy</string> |     <string name="error_http_not_found">Không tìm thấy</string> | ||||||
|     <string name="error_postprocessing_failed">Xử lý thất bại</string> |     <string name="error_postprocessing_failed">Xử lý thất bại</string> | ||||||
|     <string name="clear_finished_download">Dọn các tải về đã hoàn thành</string> |     <string name="clear_finished_download">Dọn các tải về đã hoàn thành</string> | ||||||
|     <string name="msg_pending_downloads">Hãy tiếp tục %s tải về đang chờ</string> |  | ||||||
|     <string name="stop">Dừng</string> |     <string name="stop">Dừng</string> | ||||||
|     <string name="max_retry_msg">Số lượt thử lại tối đa</string> |     <string name="max_retry_msg">Số lượt thử lại tối đa</string> | ||||||
|     <string name="max_retry_desc">Số lượt thử lại trước khi hủy tải về</string> |     <string name="max_retry_desc">Số lượt thử lại trước khi hủy tải về</string> | ||||||
|   | |||||||
| @@ -447,11 +447,9 @@ | |||||||
|     <string name="error_connect_host">無法連線到伺服器</string> |     <string name="error_connect_host">無法連線到伺服器</string> | ||||||
|     <string name="error_http_no_content">伺服器沒有傳送資料</string> |     <string name="error_http_no_content">伺服器沒有傳送資料</string> | ||||||
|     <string name="error_http_unsupported_range">伺服器不接受多執行緒下載,請以 @string/msg_threads = 1 重試</string> |     <string name="error_http_unsupported_range">伺服器不接受多執行緒下載,請以 @string/msg_threads = 1 重試</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">請求範圍無法滿足</string> |  | ||||||
|     <string name="error_http_not_found">找不到</string> |     <string name="error_http_not_found">找不到</string> | ||||||
|     <string name="error_postprocessing_failed">後處理失敗</string> |     <string name="error_postprocessing_failed">後處理失敗</string> | ||||||
|     <string name="clear_finished_download">清除已結束的下載</string> |     <string name="clear_finished_download">清除已結束的下載</string> | ||||||
|     <string name="msg_pending_downloads">繼續從您所擱置中的下載 %s 傳輸</string> |  | ||||||
|     <string name="stop">停止</string> |     <string name="stop">停止</string> | ||||||
|     <string name="max_retry_msg">最大重試次數</string> |     <string name="max_retry_msg">最大重試次數</string> | ||||||
|     <string name="max_retry_desc">在取消下載前的最大嘗試數</string> |     <string name="max_retry_desc">在取消下載前的最大嘗試數</string> | ||||||
|   | |||||||
| @@ -29,6 +29,8 @@ | |||||||
|     <attr name="bug" format="reference"/> |     <attr name="bug" format="reference"/> | ||||||
|     <attr name="settings" format="reference"/> |     <attr name="settings" format="reference"/> | ||||||
|     <attr name="ic_hot" format="reference"/> |     <attr name="ic_hot" format="reference"/> | ||||||
|  |     <attr name="ic_kiosk_local" format="reference"/> | ||||||
|  |     <attr name="ic_kiosk_recent" format="reference"/> | ||||||
|     <attr name="ic_channel" format="reference"/> |     <attr name="ic_channel" format="reference"/> | ||||||
|     <attr name="ic_bookmark" format="reference"/> |     <attr name="ic_bookmark" format="reference"/> | ||||||
|     <attr name="ic_playlist_add" format="reference"/> |     <attr name="ic_playlist_add" format="reference"/> | ||||||
|   | |||||||
| @@ -22,6 +22,17 @@ | |||||||
|     <color name="dark_soundcloud_accent_color">#FFFFFF</color> |     <color name="dark_soundcloud_accent_color">#FFFFFF</color> | ||||||
|     <color name="dark_soundcloud_statusbar_color">#ff9100</color> |     <color name="dark_soundcloud_statusbar_color">#ff9100</color> | ||||||
|  |  | ||||||
|  |     <!-- PeerTube --> | ||||||
|  |     <color name="light_peertube_primary_color">#ff6f00</color> | ||||||
|  |     <color name="light_peertube_dark_color">#c43e00</color> | ||||||
|  |     <color name="light_peertube_accent_color">#000000</color> | ||||||
|  |     <color name="light_peertube_statusbar_color">#ff833a</color> | ||||||
|  |  | ||||||
|  |     <color name="dark_peertube_primary_color">#ff6f00</color> | ||||||
|  |     <color name="dark_peertube_dark_color">#c43e00</color> | ||||||
|  |     <color name="dark_peertube_accent_color">#FFFFFF</color> | ||||||
|  |     <color name="dark_peertube_statusbar_color">#ff833a</color> | ||||||
|  |  | ||||||
|     <!-- Media.CCC --> |     <!-- Media.CCC --> | ||||||
|     <color name="light_media_ccc_primary_color">#9e9e9e</color> |     <color name="light_media_ccc_primary_color">#9e9e9e</color> | ||||||
|     <color name="light_media_ccc_dark_color">#616161</color> |     <color name="light_media_ccc_dark_color">#616161</color> | ||||||
|   | |||||||
| @@ -133,6 +133,7 @@ | |||||||
|  |  | ||||||
|     <!-- Caption Size --> |     <!-- Caption Size --> | ||||||
|     <string name="caption_settings_key" translatable="false">caption_settings_key</string> |     <string name="caption_settings_key" translatable="false">caption_settings_key</string> | ||||||
|  |     <string name="caption_user_set_key" translatable="false">caption_user_set_key</string> | ||||||
|  |  | ||||||
|     <!-- Content & History --> |     <!-- Content & History --> | ||||||
|     <string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string> |     <string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string> | ||||||
| @@ -144,6 +145,9 @@ | |||||||
|     <string name="default_language_value">en</string> |     <string name="default_language_value">en</string> | ||||||
|     <string name="default_country_value">GB</string> |     <string name="default_country_value">GB</string> | ||||||
|     <string name="content_language_key" translatable="false">content_language</string> |     <string name="content_language_key" translatable="false">content_language</string> | ||||||
|  |     <string name="peertube_instance_setup_key" translatable="false">peertube_instance_setup</string> | ||||||
|  |     <string name="peertube_selected_instance_key" translatable="false">peertube_selected_instance</string> | ||||||
|  |     <string name="peertube_instance_list_key" translatable="false">peertube_instance_list</string> | ||||||
|     <string name="content_country_key" translatable="false">content_country</string> |     <string name="content_country_key" translatable="false">content_country</string> | ||||||
|     <string name="show_age_restricted_content" translatable="false">show_age_restricted_content</string> |     <string name="show_age_restricted_content" translatable="false">show_age_restricted_content</string> | ||||||
|     <string name="use_tor_key" translatable="false">use_tor</string> |     <string name="use_tor_key" translatable="false">use_tor</string> | ||||||
|   | |||||||
| @@ -109,6 +109,14 @@ | |||||||
|     <string name="default_content_country_title">Default content country</string> |     <string name="default_content_country_title">Default content country</string> | ||||||
|     <string name="service_title">Service</string> |     <string name="service_title">Service</string> | ||||||
|     <string name="content_language_title">Default content language</string> |     <string name="content_language_title">Default content language</string> | ||||||
|  |     <string name="peertube_instance_url_title">PeerTube instances</string> | ||||||
|  |     <string name="peertube_instance_url_summary">Set your favorite peertube instances</string> | ||||||
|  |     <string name="peertube_instance_url_help">Find the instances that best suit you on https://joinpeertube.org/instances#instances-list</string> | ||||||
|  |     <string name="peertube_instance_add_title">Add instance</string> | ||||||
|  |     <string name="peertube_instance_add_help">Enter instance url</string> | ||||||
|  |     <string name="peertube_instance_add_fail">Failed to validate instance</string> | ||||||
|  |     <string name="peertube_instance_add_https_only">Only https urls are supported</string> | ||||||
|  |     <string name="peertube_instance_add_exists">Instance already exists</string> | ||||||
|     <string name="settings_category_player_title">Player</string> |     <string name="settings_category_player_title">Player</string> | ||||||
|     <string name="settings_category_player_behavior_title">Behavior</string> |     <string name="settings_category_player_behavior_title">Behavior</string> | ||||||
|     <string name="settings_category_video_audio_title">Video & audio</string> |     <string name="settings_category_video_audio_title">Video & audio</string> | ||||||
| @@ -400,6 +408,9 @@ | |||||||
|     <string name="trending">Trending</string> |     <string name="trending">Trending</string> | ||||||
|     <string name="top_50">Top 50</string> |     <string name="top_50">Top 50</string> | ||||||
|     <string name="new_and_hot">New & hot</string> |     <string name="new_and_hot">New & hot</string> | ||||||
|  |     <string name="local">Local</string> | ||||||
|  |     <string name="recently_added">Recently added</string> | ||||||
|  |     <string name="most_liked">Most liked</string> | ||||||
|     <string name="conferences">Conferences</string> |     <string name="conferences">Conferences</string> | ||||||
|     <string name="service_kiosk_string" translatable="false">%1$s/%2$s</string> |     <string name="service_kiosk_string" translatable="false">%1$s/%2$s</string> | ||||||
|     <!-- Play Queue --> |     <!-- Play Queue --> | ||||||
| @@ -526,6 +537,7 @@ | |||||||
|     <string name="paused">paused</string> |     <string name="paused">paused</string> | ||||||
|     <string name="queued">queued</string> |     <string name="queued">queued</string> | ||||||
|     <string name="post_processing">post-processing</string> |     <string name="post_processing">post-processing</string> | ||||||
|  |     <string name="recovering">recovering</string> | ||||||
|     <string name="enqueue">Queue</string> |     <string name="enqueue">Queue</string> | ||||||
|     <string name="permission_denied">Action denied by the system</string> |     <string name="permission_denied">Action denied by the system</string> | ||||||
|     <!-- download notifications --> |     <!-- download notifications --> | ||||||
| @@ -551,16 +563,15 @@ | |||||||
|     <string name="error_connect_host">Can not connect to the server</string> |     <string name="error_connect_host">Can not connect to the server</string> | ||||||
|     <string name="error_http_no_content">The server does not send data</string> |     <string name="error_http_no_content">The server does not send data</string> | ||||||
|     <string name="error_http_unsupported_range">The server does not accept multi-threaded downloads, retry with @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">The server does not accept multi-threaded downloads, retry with @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">Requested range not satisfiable</string> |  | ||||||
|     <string name="error_http_not_found">Not found</string> |     <string name="error_http_not_found">Not found</string> | ||||||
|     <string name="error_postprocessing_failed">Post-processing failed</string> |     <string name="error_postprocessing_failed">Post-processing failed</string> | ||||||
|     <string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string> |     <string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string> | ||||||
|     <string name="error_insufficient_storage">No space left on device</string> |     <string name="error_insufficient_storage">No space left on device</string> | ||||||
|     <string name="error_progress_lost">Progress lost, because the file was deleted</string> |     <string name="error_progress_lost">Progress lost, because the file was deleted</string> | ||||||
|     <string name="error_timeout">Connection timeout</string> |     <string name="error_timeout">Connection timeout</string> | ||||||
|  |     <string name="error_download_resource_gone">Cannot recover this download</string> | ||||||
|     <string name="clear_finished_download">Clear finished downloads</string> |     <string name="clear_finished_download">Clear finished downloads</string> | ||||||
|     <string name="confirm_prompt">Are you sure?</string> |     <string name="confirm_prompt">Are you sure?</string> | ||||||
|     <string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string> |  | ||||||
|     <string name="stop">Stop</string> |     <string name="stop">Stop</string> | ||||||
|     <string name="max_retry_msg">Maximum retries</string> |     <string name="max_retry_msg">Maximum retries</string> | ||||||
|     <string name="max_retry_desc">Maximum number of attempts before canceling the download</string> |     <string name="max_retry_desc">Maximum number of attempts before canceling the download</string> | ||||||
| @@ -576,4 +587,6 @@ | |||||||
|     <string name="downloads_storage_ask_summary_kitkat">You will be asked where to save each download.\nChoose SAF if you want to download to an external SD card</string> |     <string name="downloads_storage_ask_summary_kitkat">You will be asked where to save each download.\nChoose SAF if you want to download to an external SD card</string> | ||||||
|     <string name="downloads_storage_use_saf_title">Use SAF</string> |     <string name="downloads_storage_use_saf_title">Use SAF</string> | ||||||
|     <string name="downloads_storage_use_saf_summary">The Storage Access Framework allows downloads to an external SD card.\nNote: some devices are not compatible</string> |     <string name="downloads_storage_use_saf_summary">The Storage Access Framework allows downloads to an external SD card.\nNote: some devices are not compatible</string> | ||||||
|  |     <string name="choose_instance_prompt">Choose an instance</string> | ||||||
|  |  | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -44,6 +44,8 @@ | |||||||
|         <item name="pause">@drawable/ic_pause_black_24dp</item> |         <item name="pause">@drawable/ic_pause_black_24dp</item> | ||||||
|         <item name="settings">@drawable/ic_settings_black_24dp</item> |         <item name="settings">@drawable/ic_settings_black_24dp</item> | ||||||
|         <item name="ic_hot">@drawable/ic_whatshot_black_24dp</item> |         <item name="ic_hot">@drawable/ic_whatshot_black_24dp</item> | ||||||
|  |         <item name="ic_kiosk_local">@drawable/ic_kiosk_local_black_24dp</item> | ||||||
|  |         <item name="ic_kiosk_recent">@drawable/ic_kiosk_recent_black_24dp</item> | ||||||
|         <item name="ic_channel">@drawable/ic_channel_black_24dp</item> |         <item name="ic_channel">@drawable/ic_channel_black_24dp</item> | ||||||
|         <item name="ic_bookmark">@drawable/ic_bookmark_black_24dp</item> |         <item name="ic_bookmark">@drawable/ic_bookmark_black_24dp</item> | ||||||
|         <item name="ic_playlist_add">@drawable/ic_playlist_add_black_24dp</item> |         <item name="ic_playlist_add">@drawable/ic_playlist_add_black_24dp</item> | ||||||
| @@ -108,6 +110,8 @@ | |||||||
|         <item name="play">@drawable/ic_play_arrow_white_24dp</item> |         <item name="play">@drawable/ic_play_arrow_white_24dp</item> | ||||||
|         <item name="settings">@drawable/ic_settings_white_24dp</item> |         <item name="settings">@drawable/ic_settings_white_24dp</item> | ||||||
|         <item name="ic_hot">@drawable/ic_whatshot_white_24dp</item> |         <item name="ic_hot">@drawable/ic_whatshot_white_24dp</item> | ||||||
|  |         <item name="ic_kiosk_local">@drawable/ic_kiosk_local_white_24dp</item> | ||||||
|  |         <item name="ic_kiosk_recent">@drawable/ic_kiosk_recent_white_24dp</item> | ||||||
|         <item name="ic_channel">@drawable/ic_channel_white_24dp</item> |         <item name="ic_channel">@drawable/ic_channel_white_24dp</item> | ||||||
|         <item name="ic_bookmark">@drawable/ic_bookmark_white_24dp</item> |         <item name="ic_bookmark">@drawable/ic_bookmark_white_24dp</item> | ||||||
|         <item name="ic_playlist_add">@drawable/ic_playlist_add_white_24dp</item> |         <item name="ic_playlist_add">@drawable/ic_playlist_add_white_24dp</item> | ||||||
| @@ -233,4 +237,8 @@ | |||||||
|         <item name="android:windowIsTranslucent">true</item> |         <item name="android:windowIsTranslucent">true</item> | ||||||
|         <item name="android:windowAnimationStyle">@null</item> |         <item name="android:windowAnimationStyle">@null</item> | ||||||
|     </style> |     </style> | ||||||
|  |  | ||||||
|  |     <style name="NavViewTextStyle"> | ||||||
|  |         <item name="android:ellipsize">end</item> | ||||||
|  |     </style> | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
 Alexander--
					Alexander--