diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java index e7582b377..232ff7474 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java @@ -36,6 +36,7 @@ import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.conversation.glide.BriarModelLoader; import org.briarproject.briar.android.hotspot.AbstractTabsFragment; +import org.briarproject.briar.android.hotspot.HotspotHelpFragment; import org.briarproject.briar.android.hotspot.HotspotIntroFragment; import org.briarproject.briar.android.hotspot.ManualHotspotFragment; import org.briarproject.briar.android.hotspot.QrHotspotFragment; @@ -222,4 +223,6 @@ public interface AndroidComponent void inject(QrHotspotFragment qrHotspotFragment); void inject(ManualHotspotFragment manualHotspotFragment); + + void inject(HotspotHelpFragment hotspotHelpFragment); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotHelpFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotHelpFragment.java index 8cacfcaee..11adf5071 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotHelpFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotHelpFragment.java @@ -1,16 +1,41 @@ package org.briarproject.briar.android.hotspot; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; +import java.util.List; + +import javax.inject.Inject; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; + +import static android.content.Intent.ACTION_SEND; +import static android.content.Intent.EXTRA_STREAM; +import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; +import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; +import static android.os.Build.VERSION.SDK_INT; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static androidx.transition.TransitionManager.beginDelayedTransition; +import static org.briarproject.briar.android.AppModule.getAndroidComponent; +import static org.briarproject.briar.android.hotspot.HotspotViewModel.getApkFileName; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -18,6 +43,26 @@ public class HotspotHelpFragment extends Fragment { public final static String TAG = HotspotHelpFragment.class.getName(); + @Inject + ViewModelProvider.Factory viewModelFactory; + + private HotspotViewModel viewModel; + private final ActivityResultLauncher launcher = + registerForActivityResult(new CreateDocument(), uri -> + viewModel.exportApk(uri) + ); + private Button button; + private ProgressBar progressBar; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + FragmentActivity activity = requireActivity(); + getAndroidComponent(activity).inject(this); + viewModel = new ViewModelProvider(activity, viewModelFactory) + .get(HotspotViewModel.class); + } + @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @@ -25,4 +70,45 @@ public class HotspotHelpFragment extends Fragment { return inflater .inflate(R.layout.fragment_hotspot_help, container, false); } + + @Override + public void onViewCreated(View v, @Nullable Bundle savedInstanceState) { + super.onViewCreated(v, savedInstanceState); + button = v.findViewById(R.id.fallbackButton); + progressBar = v.findViewById(R.id.progressBar); + button.setOnClickListener(view -> { + beginDelayedTransition((ViewGroup) requireView()); + button.setVisibility(INVISIBLE); + progressBar.setVisibility(VISIBLE); + + if (SDK_INT >= 19) launcher.launch(getApkFileName()); + else viewModel.exportApk(); + }); + viewModel.getSavedApkToUri().observeEvent(this, this::shareUri); + } + + private void shareUri(Uri uri) { + beginDelayedTransition((ViewGroup) requireView()); + button.setVisibility(VISIBLE); + progressBar.setVisibility(INVISIBLE); + + Intent i = new Intent(ACTION_SEND); + i.putExtra(EXTRA_STREAM, uri); + i.setType("application/zip"); + i.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + Context ctx = requireContext(); + if (SDK_INT <= 19) { + // Workaround for Android bug: + // ctx.grantUriPermission also needed for Android 4 + List resInfoList = ctx.getPackageManager() + .queryIntentActivities(i, MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + ctx.grantUriPermission(packageName, uri, + FLAG_GRANT_READ_URI_PERMISSION); + } + } + startActivity(Intent.createChooser(i, null)); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java index 33681a70e..f231147e3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java @@ -1,6 +1,7 @@ package org.briarproject.briar.android.hotspot; import android.app.Application; +import android.net.Uri; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.TransactionManager; @@ -21,6 +22,12 @@ import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.api.android.AndroidNotificationManager; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -31,8 +38,14 @@ import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Environment.DIRECTORY_DOWNLOADS; +import static android.os.Environment.getExternalStoragePublicDirectory; import static java.util.Objects.requireNonNull; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.IoUtils.copyAndClose; +import static org.briarproject.briar.BuildConfig.DEBUG; +import static org.briarproject.briar.BuildConfig.VERSION_NAME; @NotNullByDefault class HotspotViewModel extends DbViewModel @@ -51,6 +64,8 @@ class HotspotViewModel extends DbViewModel new MutableLiveData<>(); private final MutableLiveEvent peerConnected = new MutableLiveEvent<>(); + private final MutableLiveEvent savedApkToUri = + new MutableLiveEvent<>(); @Nullable // Field to temporarily store the network config received via onHotspotStarted() @@ -150,6 +165,48 @@ class HotspotViewModel extends DbViewModel hotspotManager.stopWifiP2pHotspot(); } + void exportApk(Uri uri) { + if (SDK_INT < 19) throw new IllegalStateException(); + try { + OutputStream out = getApplication().getContentResolver() + .openOutputStream(uri, "wt"); + writeApk(out, uri); + } catch (FileNotFoundException e) { + handleException(e); + } + } + + void exportApk() { + if (SDK_INT >= 19) throw new IllegalStateException(); + File path = getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS); + //noinspection ResultOfMethodCallIgnored + path.mkdirs(); + File file = new File(path, getApkFileName()); + try { + OutputStream out = new FileOutputStream(file); + writeApk(out, Uri.fromFile(file)); + } catch (FileNotFoundException e) { + handleException(e); + } + } + + static String getApkFileName() { + return "briar" + (DEBUG ? "-debug-" : "-") + VERSION_NAME + ".apk"; + } + + private void writeApk(OutputStream out, Uri uriToShare) { + File apk = new File(getApplication().getPackageCodePath()); + ioExecutor.execute(() -> { + try { + FileInputStream in = new FileInputStream(apk); + copyAndClose(in, out); + savedApkToUri.postEvent(uriToShare); + } catch (IOException e) { + handleException(e); + } + }); + } + LiveData getState() { return state; } @@ -158,4 +215,8 @@ class HotspotViewModel extends DbViewModel return peerConnected; } + LiveEvent getSavedApkToUri() { + return savedApkToUri; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServer.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServer.java index c4cca881e..b9e9b6133 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServer.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServer.java @@ -3,7 +3,6 @@ package org.briarproject.briar.android.hotspot; import android.content.Context; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.BuildConfig; import org.briarproject.briar.R; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -29,6 +28,7 @@ import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.BuildConfig.VERSION_NAME; +import static org.briarproject.briar.android.hotspot.HotspotViewModel.getApkFileName; @NotNullByDefault class WebServer extends NanoHTTPD { @@ -80,8 +80,7 @@ class WebServer extends NanoHTTPD { } String app = ctx.getString(R.string.app_name); String appV = app + " " + VERSION_NAME; - String filename = "briar" + (BuildConfig.DEBUG ? "-debug-" : "-") + - VERSION_NAME + ".apk"; + String filename = getApkFileName(); doc.select("#download_title").first() .text(ctx.getString(R.string.website_download_title, appV)); doc.select("#download_intro").first() diff --git a/briar-android/src/main/res/layout/fragment_hotspot_help.xml b/briar-android/src/main/res/layout/fragment_hotspot_help.xml index 4251ce594..03a54d006 100644 --- a/briar-android/src/main/res/layout/fragment_hotspot_help.xml +++ b/briar-android/src/main/res/layout/fragment_hotspot_help.xml @@ -108,6 +108,49 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/site3View" /> + + + + +