package org.mian.gitnex.activities; import android.app.Activity; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; import android.text.method.ScrollingMovementMethod; import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.core.app.NotificationCompat; import com.vdurmont.emoji.EmojiParser; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.gitnex.tea4j.models.Files; import org.mian.gitnex.R; import org.mian.gitnex.clients.RetrofitClient; import org.mian.gitnex.databinding.ActivityFileViewBinding; import org.mian.gitnex.fragments.BottomSheetFileViewerFragment; import org.mian.gitnex.helpers.AlertDialogs; import org.mian.gitnex.helpers.AppUtil; import org.mian.gitnex.helpers.Constants; import org.mian.gitnex.helpers.Images; import org.mian.gitnex.helpers.Markdown; import org.mian.gitnex.helpers.Toasty; import org.mian.gitnex.helpers.contexts.RepositoryContext; import org.mian.gitnex.notifications.Notifications; import org.mian.gitnex.structs.BottomSheetListener; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Response; /** * Author M M Arif */ public class FileViewActivity extends BaseActivity implements BottomSheetListener { private ActivityFileViewBinding binding; private Files file; private RepositoryContext repository; private boolean renderMd = false; public ActivityResultLauncher editFileLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { if(result.getResultCode() == 200) { assert result.getData() != null; if(result.getData().getBooleanExtra("fileModified", false)) { getSingleFileContents(repository.getOwner(), repository.getName(), file.getPath(), repository.getBranchRef()); } } }); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityFileViewBinding.inflate(getLayoutInflater()); repository = RepositoryContext.fromIntent(getIntent()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); file = (Files) getIntent().getSerializableExtra("file"); binding.close.setOnClickListener(view -> finish()); binding.toolbarTitle.setMovementMethod(new ScrollingMovementMethod()); binding.toolbarTitle.setText(file.getPath()); getSingleFileContents(repository.getOwner(), repository.getName(), file.getPath(), repository.getBranchRef()); } private void getSingleFileContents(final String owner, String repo, final String filename, String ref) { Thread thread = new Thread(() -> { Call call = RetrofitClient .getWebInterface(ctx) .getFileContents(getAccount().getWebAuthorization(), owner, repo, ref, filename); try { Response response = call.execute(); if(response.code() == 200) { ResponseBody responseBody = response.body(); if(responseBody != null) { runOnUiThread(() -> binding.progressBar.setVisibility(View.GONE)); String fileExtension = FilenameUtils.getExtension(filename); boolean processable = false; switch(AppUtil.getFileType(fileExtension)) { case IMAGE: // See https://developer.android.com/guide/topics/media/media-formats#core if(Arrays.asList("bmp", "gif", "jpg", "jpeg", "png", "webp", "heic", "heif").contains(fileExtension.toLowerCase())) { processable = true; byte[] pictureBytes = responseBody.bytes(); runOnUiThread(() -> { binding.contents.setVisibility(View.GONE); binding.markdownFrame.setVisibility(View.GONE); binding.photoView.setVisibility(View.VISIBLE); binding.photoView.setImageBitmap(Images.scaleImage(pictureBytes, 1920)); }); } break; case UNKNOWN: case TEXT: if(file.getSize() > Constants.maximumFileViewerSize) { break; } processable = true; String text = responseBody.string(); runOnUiThread(() -> { binding.photoView.setVisibility(View.GONE); binding.contents.setContent(text, fileExtension); if(renderMd) { Markdown.render(ctx, EmojiParser.parseToUnicode(text), binding.markdown, repository); binding.contents.setVisibility(View.GONE); binding.markdownFrame.setVisibility(View.VISIBLE); } else { binding.markdownFrame.setVisibility(View.GONE); binding.contents.setVisibility(View.VISIBLE); } }); break; } if(!processable) { // While the file could still be non-binary, // it's better we don't show it (to prevent any crashes and/or unwanted behavior) and let the user download it instead. responseBody.close(); runOnUiThread(() -> { binding.photoView.setVisibility(View.GONE); binding.contents.setVisibility(View.GONE); binding.markdownFrame.setVisibility(View.VISIBLE); binding.markdown.setVisibility(View.GONE); binding.markdownTv.setVisibility(View.VISIBLE); binding.markdownTv.setText(getString(R.string.excludeFilesInFileViewer)); binding.markdownTv.setGravity(Gravity.CENTER); binding.markdownTv.setTypeface(null, Typeface.BOLD); }); } } else { runOnUiThread(() -> { binding.markdownTv.setText(""); binding.progressBar.setVisibility(View.GONE); }); } } else { switch(response.code()) { case 401: runOnUiThread(() -> AlertDialogs.authorizationTokenRevokedDialog(ctx, getResources().getString(R.string.alertDialogTokenRevokedTitle), getResources().getString(R.string.alertDialogTokenRevokedMessage), getResources().getString(R.string.cancelButton), getResources().getString(R.string.navLogout))); break; case 403: runOnUiThread(() -> Toasty.error(ctx, ctx.getString(R.string.authorizeError))); break; case 404: runOnUiThread(() -> Toasty.warning(ctx, ctx.getString(R.string.apiNotFound))); break; default: runOnUiThread(() -> Toasty.error(ctx, getString(R.string.genericError))); } } } catch(IOException ignored) {} }); thread.start(); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.generic_nav_dotted_menu, menu); inflater.inflate(R.menu.files_view_menu, menu); if(!FilenameUtils.getExtension(file.getName()) .equalsIgnoreCase("md")) { menu.getItem(0) .setVisible(false); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if(id == android.R.id.home) { finish(); return true; } else if(id == R.id.genericMenu) { BottomSheetFileViewerFragment bottomSheet = new BottomSheetFileViewerFragment(); bottomSheet.setArguments(repository.getBundle()); bottomSheet.show(getSupportFragmentManager(), "fileViewerBottomSheet"); return true; } else if(id == R.id.markdown) { if(!renderMd) { if(binding.markdown.getAdapter() == null) { Markdown.render(ctx, EmojiParser.parseToUnicode(binding.contents.getContent()), binding.markdown, repository); } binding.contents.setVisibility(View.GONE); binding.markdownFrame.setVisibility(View.VISIBLE); renderMd = true; } else { binding.markdownFrame.setVisibility(View.GONE); binding.contents.setVisibility(View.VISIBLE); renderMd = false; } return true; } else { return super.onOptionsItemSelected(item); } } @Override public void onButtonClicked(String text) { if("downloadFile".equals(text)) { requestFileDownload(); } if("deleteFile".equals(text)) { Intent intent = repository.getIntent(ctx, CreateFileActivity.class); intent.putExtra("fileAction", CreateFileActivity.FILE_ACTION_DELETE); intent.putExtra("filePath", file.getPath()); intent.putExtra("fileSha", file.getSha()); editFileLauncher.launch(intent); } if("editFile".equals(text)) { if(binding.contents.getContent() != null && !binding.contents.getContent().isEmpty()) { Intent intent = repository.getIntent(ctx, CreateFileActivity.class); intent.putExtra("fileAction", CreateFileActivity.FILE_ACTION_EDIT); intent.putExtra("filePath", file.getPath()); intent.putExtra("fileSha", file.getSha()); intent.putExtra("fileContents", binding.contents.getContent()); ctx.startActivity(intent); } else { Toasty.error(ctx, getString(R.string.fileTypeCannotBeEdited)); } } } private void requestFileDownload() { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.putExtra(Intent.EXTRA_TITLE, file.getName()); intent.setType("*/*"); activityResultLauncher.launch(intent); } ActivityResultLauncher activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK) { assert result.getData() != null; try { OutputStream outputStream = getContentResolver().openOutputStream(result.getData().getData()); NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, ctx.getPackageName()) .setContentTitle(getString(R.string.fileViewerNotificationTitleStarted)) .setContentText(getString(R.string.fileViewerNotificationDescriptionStarted, file.getName())) .setSmallIcon(R.drawable.gitnex_transparent) .setPriority(NotificationCompat.PRIORITY_LOW) .setChannelId(Constants.downloadNotificationChannelId) .setProgress(100, 0, false) .setOngoing(true); int notificationId = Notifications.uniqueNotificationId(ctx); NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);; notificationManager.notify(notificationId, builder.build()); Thread thread = new Thread(() -> { try { Call call = RetrofitClient .getWebInterface(ctx) .getFileContents(getAccount().getWebAuthorization(), repository.getOwner(), repository.getName(), repository.getBranchRef(), file.getPath()); Response response = call.execute(); assert response.body() != null; AppUtil.copyProgress(response.body().byteStream(), outputStream, file.getSize(), progress -> { builder.setProgress(100, progress, false); notificationManager.notify(notificationId, builder.build()); }); builder.setContentTitle(getString(R.string.fileViewerNotificationTitleFinished)) .setContentText(getString(R.string.fileViewerNotificationDescriptionFinished, file.getName())); } catch(IOException ignored) { builder.setContentTitle(getString(R.string.fileViewerNotificationTitleFailed)) .setContentText(getString(R.string.fileViewerNotificationDescriptionFailed, file.getName())); } finally { builder.setProgress(0,0,false) .setOngoing(false); notificationManager.notify(notificationId, builder.build()); } }); thread.start(); } catch(IOException ignored) {} } }); @Override public void onResume() { super.onResume(); repository.checkAccountSwitch(this); } }