mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
Merge remote-tracking branch 'origin/1081-share-app-via-wifi-hotspot' into offline-testing
This commit is contained in:
@@ -12,4 +12,6 @@ public interface FeatureFlags {
|
||||
boolean shouldEnableDisappearingMessages();
|
||||
|
||||
boolean shouldEnableConnectViaBluetooth();
|
||||
|
||||
boolean shouldEnableShareAppViaOfflineHotspot();
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ public class BrambleCoreIntegrationTestModule {
|
||||
public boolean shouldEnableConnectViaBluetooth() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnableShareAppViaOfflineHotspot() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ dependencies {
|
||||
exclude group: 'com.android.support'
|
||||
exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it
|
||||
}
|
||||
implementation 'org.nanohttpd:nanohttpd:2.3.1'
|
||||
|
||||
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
@@ -441,6 +442,11 @@
|
||||
android:label="@string/pending_contact_requests"
|
||||
android:theme="@style/BriarTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".android.hotspot.HotspotActivity"
|
||||
android:label="@string/hotspot_title"
|
||||
android:theme="@style/BriarTheme" />
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
|
||||
103
briar-android/src/main/assets/hotspot.html
Normal file
103
briar-android/src/main/assets/hotspot.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
background-color: #F2F2F2;
|
||||
font-family: Roboto,Arial,Helvetica,sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
div#top {
|
||||
background-color: #FFFFFF;
|
||||
padding: 16px;
|
||||
}
|
||||
div#bottom {
|
||||
padding: 16px 32px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
a.button {
|
||||
background-color: #82C91E;
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 32px !important;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
color: #000000 !important;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
margin: 20px auto 20px auto;
|
||||
}
|
||||
ol {
|
||||
list-style: none;
|
||||
counter-reset: briar-counter;
|
||||
padding-left: 40px;
|
||||
}
|
||||
ol li {
|
||||
counter-increment: briar-counter;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
ol li::before {
|
||||
content: counter(briar-counter);
|
||||
background-color: #82C91E;
|
||||
color: #000000 !important;
|
||||
font-weight: bold;
|
||||
border-radius: 70px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 32px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="top">
|
||||
<svg style="width:156px;height:47px;" viewBox="0 0 778 235">
|
||||
<path style="fill:#87c214"
|
||||
d="m 64.900391,0 c -9.7,0 -17.701172,7.9992183 -17.701172,17.699219 v 22.5 h 43.601562 v -22.5 C 90.800781,7.9992183 82.899219,0 73.199219,0 Z m 96.999999,0 c -9.7,0 -17.70117,7.9992183 -17.70117,17.699219 V 137.19922 h 43.60156 V 17.699219 C 187.80078,7.9992183 179.89922,0 170.19922,0 Z M 47.199219,97.800781 V 217.30078 c 0,9.7 7.901172,17.69922 17.701172,17.69922 h 8.298828 c 9.7,0 17.701172,-7.99922 17.701172,-17.69922 V 97.800781 Z m 97.000001,96.999999 v 22.5 c 0,9.7 8.00117,17.69922 17.70117,17.69922 h 8.29883 c 9.7,0 17.70117,-7.99922 17.70117,-17.69922 v -22.5 z"/>
|
||||
<path style="fill:#95d220"
|
||||
d="M 17.699219,47.199219 C 7.9992186,47.199219 0,55.100391 0,64.900391 v 8.298828 c 0,9.7 7.8992186,17.701172 17.699219,17.701172 H 137.19922 V 47.199219 Z m 177.101561,0 v 43.701172 h 22.5 c 9.7,0 17.69922,-7.901172 17.69922,-17.701172 v -8.298828 c 0,-9.8 -7.99922,-17.701172 -17.69922,-17.701172 z M 17.699219,144.19922 C 7.9992186,144.19922 0,152.10039 0,161.90039 v 8.29883 c 0,9.7 7.8992186,17.70117 17.699219,17.70117 h 22.5 v -43.70117 z m 80.101562,0 v 43.70117 H 217.30078 c 9.7,0 17.69922,-8.00117 17.69922,-17.70117 v -8.29883 c 0,-9.8 -7.99922,-17.70117 -17.69922,-17.70117 z"/>
|
||||
<path d="M 301,60.564864 V 174.43514 h 53.31362 c 25.13729,0 38.31622,-12.58548 38.31622,-32.27441 0,-12.78766 -5.88,-22.32687 -17.63776,-27.60431 v -0.20217 c 8.91968,-5.48043 12.77339,-12.38249 12.77339,-23.140374 0,-16.238294 -11.14945,-30.648991 -34.66495,-30.648991 z m 110.68683,0 V 174.43514 h 13.37598 v -45.67022 l -1.41529,-1.41926 h 26.95811 c 15.00127,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 h 15.00139 l -16.82503,-35.52128 c -3.64896,-7.91617 -9.52848,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36593,-3.24765 22.70429,-14.41228 22.70429,-29.229734 0,-22.530633 -17.43208,-33.693671 -38.31224,-33.693671 z m 111.08726,0 V 174.43514 h 13.37992 V 60.564864 Z m 78.65821,0 -50.07469,113.870276 h 14.59701 l 12.16287,-27.40213 -0.60656,-1.41926 h 62.2336 l -0.60655,1.41926 12.16286,27.40213 h 14.59701 L 615.62098,60.564864 Z m 79.463,0 V 174.43514 h 13.37994 v -45.67022 l -1.41927,-1.41926 h 26.96209 c 15.00128,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 H 778 l -16.82503,-35.52128 c -3.64895,-7.91617 -9.52851,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36591,-3.24765 22.70427,-14.41228 22.70427,-29.229734 0,-22.530633 -17.43209,-33.693671 -38.31223,-33.693671 z M 312.96068,73.147961 h 38.72057 c 14.59584,0 22.29593,5.887175 22.29593,18.065895 0,10.148944 -6.07834,18.268094 -22.29593,18.268094 h -38.72057 l 1.41927,-1.41927 V 74.571187 Z m 110.68684,0 h 37.90786 c 13.78495,0 24.32519,5.684988 24.52791,20.908395 0,12.178724 -9.52687,20.702244 -25.94718,20.702244 h -36.48859 l 1.41529,-1.41927 V 74.571187 Z m 269.00626,0 h 37.90788 c 13.98769,0 24.53187,5.684988 24.53187,20.908395 0,12.178724 -9.52688,20.702244 -25.94718,20.702244 h -36.49257 l 1.41927,-1.41927 V 74.571187 Z m -83.92693,1.423226 h 0.20615 l 3.44509,11.366019 20.06794,45.670224 1.41924,1.41926 h -50.07071 l 1.41926,-1.41926 20.06793,-45.670224 z M 312.96068,122.06505 h 41.35294 c 16.82575,0 24.53189,7.71398 24.53189,20.09568 0,12.58468 -7.09797,19.69131 -24.53189,19.69131 h -41.35294 l 1.41927,-1.42322 v -36.94055 z"/>
|
||||
</svg>
|
||||
|
||||
<h2 id="download_title">Download Briar 1.2.20</h2>
|
||||
|
||||
<span id="download_intro">Someone nearby shared Briar with you.</span>
|
||||
|
||||
<a href="/app.apk" class="button">
|
||||
<svg aria-hidden="true"
|
||||
style="width:24px;height:24px;margin-right:6px;vertical-align:middle;"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
|
||||
</svg>
|
||||
<span id="download_button">Download Briar</span>
|
||||
</a>
|
||||
|
||||
<span id="download_outro">After the download is complete, open the downloaded file and install it.</span>
|
||||
</div>
|
||||
|
||||
<div id="bottom">
|
||||
<h3 id="troubleshooting_title">Troubleshooting</h3>
|
||||
<ol>
|
||||
<li id="troubleshooting_1">If you can't download the app, try it with a different web
|
||||
browser app.
|
||||
</li>
|
||||
<li id="troubleshooting_2">Ensure that your browser is allowed to download apps directly by
|
||||
giving it the permission or enabling the installation of apps from "Unknown Sources" in
|
||||
system settings.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,6 +35,11 @@ import org.briarproject.briar.BriarCoreModule;
|
||||
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.FallbackFragment;
|
||||
import org.briarproject.briar.android.hotspot.HotspotIntroFragment;
|
||||
import org.briarproject.briar.android.hotspot.ManualHotspotFragment;
|
||||
import org.briarproject.briar.android.hotspot.QrHotspotFragment;
|
||||
import org.briarproject.briar.android.logging.CachingLogHandler;
|
||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||
import org.briarproject.briar.android.settings.ConnectionsFragment;
|
||||
@@ -210,4 +215,14 @@ public interface AndroidComponent
|
||||
void inject(SecurityFragment securityFragment);
|
||||
|
||||
void inject(NotificationsFragment notificationsFragment);
|
||||
|
||||
void inject(HotspotIntroFragment hotspotIntroFragment);
|
||||
|
||||
void inject(AbstractTabsFragment abstractTabsFragment);
|
||||
|
||||
void inject(QrHotspotFragment qrHotspotFragment);
|
||||
|
||||
void inject(ManualHotspotFragment manualHotspotFragment);
|
||||
|
||||
void inject(FallbackFragment fallbackFragment);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.briarproject.bramble.util.StringUtils;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||
import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.hotspot.HotspotActivity;
|
||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
||||
@@ -63,9 +64,11 @@ import static android.app.Notification.DEFAULT_SOUND;
|
||||
import static android.app.Notification.DEFAULT_VIBRATE;
|
||||
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
|
||||
import static android.app.NotificationManager.IMPORTANCE_LOW;
|
||||
import static android.app.PendingIntent.getActivity;
|
||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
|
||||
import static androidx.core.app.NotificationCompat.CATEGORY_SERVICE;
|
||||
@@ -274,7 +277,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
b.setWhen(0); // Don't show the time
|
||||
b.setOngoing(true);
|
||||
Intent i = new Intent(appContext, SplashScreenActivity.class);
|
||||
b.setContentIntent(PendingIntent.getActivity(appContext, 0, i, 0));
|
||||
b.setContentIntent(getActivity(appContext, 0, i, 0));
|
||||
if (SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_SERVICE);
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
@@ -619,13 +622,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
public void showSignInNotification() {
|
||||
if (blockSignInReminder) return;
|
||||
if (SDK_INT >= 26) {
|
||||
NotificationChannel channel =
|
||||
new NotificationChannel(REMINDER_CHANNEL_ID, appContext
|
||||
.getString(
|
||||
R.string.reminder_notification_channel_title),
|
||||
IMPORTANCE_LOW);
|
||||
channel.setLockscreenVisibility(
|
||||
NotificationCompat.VISIBILITY_SECRET);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
REMINDER_CHANNEL_ID, appContext
|
||||
.getString(R.string.reminder_notification_channel_title),
|
||||
IMPORTANCE_LOW);
|
||||
channel.setLockscreenVisibility(VISIBILITY_SECRET);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
@@ -652,7 +653,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
|
||||
Intent i = new Intent(appContext, SplashScreenActivity.class);
|
||||
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
|
||||
b.setContentIntent(PendingIntent.getActivity(appContext, 0, i, 0));
|
||||
b.setContentIntent(getActivity(appContext, 0, i, 0));
|
||||
|
||||
notificationManager.notify(REMINDER_NOTIFICATION_ID, b.build());
|
||||
}
|
||||
@@ -720,4 +721,40 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
public void unblockAllBlogPostNotifications() {
|
||||
androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showHotspotNotification() {
|
||||
if (SDK_INT >= 26) {
|
||||
String channelTitle = appContext
|
||||
.getString(R.string.hotspot_notification_channel_title);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
HOTSPOT_CHANNEL_ID, channelTitle, IMPORTANCE_LOW);
|
||||
channel.setLockscreenVisibility(VISIBILITY_SECRET);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
BriarNotificationBuilder b =
|
||||
new BriarNotificationBuilder(appContext, HOTSPOT_CHANNEL_ID);
|
||||
b.setSmallIcon(R.drawable.notification_hotspot);
|
||||
b.setColorRes(R.color.briar_brand_green);
|
||||
b.setContentTitle(
|
||||
appContext.getText(R.string.hotspot_notification_title));
|
||||
b.setNotificationCategory(CATEGORY_SERVICE);
|
||||
b.setOngoing(true);
|
||||
b.setShowWhen(true);
|
||||
|
||||
String actionTitle =
|
||||
appContext.getString(R.string.hotspot_button_stop_sharing);
|
||||
Intent i = new Intent(appContext, HotspotActivity.class);
|
||||
i.addFlags(FLAG_ACTIVITY_SINGLE_TOP);
|
||||
i.setAction(ACTION_STOP_HOTSPOT);
|
||||
PendingIntent actionIntent = getActivity(appContext, 0, i, 0);
|
||||
int icon = SDK_INT >= 21 ? R.drawable.ic_portable_wifi_off : 0;
|
||||
b.addAction(icon, actionTitle, actionIntent);
|
||||
notificationManager.notify(HOTSPOT_NOTIFICATION_ID, b.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearHotspotNotification() {
|
||||
notificationManager.cancel(HOTSPOT_NOTIFICATION_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.briarproject.briar.android.blog.BlogModule;
|
||||
import org.briarproject.briar.android.contact.ContactListModule;
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactModule;
|
||||
import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.hotspot.HotspotModule;
|
||||
import org.briarproject.briar.android.introduction.IntroductionModule;
|
||||
import org.briarproject.briar.android.logging.LoggingModule;
|
||||
import org.briarproject.briar.android.login.LoginModule;
|
||||
@@ -92,6 +93,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
GroupListModule.class,
|
||||
GroupConversationModule.class,
|
||||
SharingModule.class,
|
||||
HotspotModule.class
|
||||
})
|
||||
public class AppModule {
|
||||
|
||||
@@ -303,6 +305,11 @@ public class AppModule {
|
||||
public boolean shouldEnableConnectViaBluetooth() {
|
||||
return IS_DEBUG_BUILD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnableShareAppViaOfflineHotspot() {
|
||||
return IS_DEBUG_BUILD;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.briarproject.briar.android.forum.CreateForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumListFragment;
|
||||
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
||||
import org.briarproject.briar.android.hotspot.HotspotActivity;
|
||||
import org.briarproject.briar.android.introduction.ContactChooserFragment;
|
||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
|
||||
@@ -176,6 +177,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(CrashReportActivity crashReportActivity);
|
||||
|
||||
void inject(HotspotActivity hotspotActivity);
|
||||
|
||||
// Fragments
|
||||
|
||||
void inject(SetupFragment fragment);
|
||||
|
||||
@@ -46,6 +46,7 @@ import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
|
||||
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
|
||||
/**
|
||||
* Warning: Some activities don't extend {@link BaseActivity}.
|
||||
@@ -177,13 +178,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
|
||||
public void showNextFragment(BaseFragment f) {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(STARTED)) return;
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.step_next_in,
|
||||
R.anim.step_previous_out, R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.addToBackStack(f.getUniqueTag())
|
||||
.commit();
|
||||
showFragment(getSupportFragmentManager(), f, f.getUniqueTag());
|
||||
}
|
||||
|
||||
protected boolean isFragmentAdded(String fragmentTag) {
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.briarproject.briar.android.contact.add.nearby.AddContactState.Contact
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementListening;
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementStarted;
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementWaiting;
|
||||
import org.briarproject.briar.android.util.QrCodeUtils;
|
||||
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import androidx.annotation.Nullable;
|
||||
@ParametersNotNullByDefault
|
||||
public class ErrorFragment extends BaseFragment {
|
||||
|
||||
private static final String TAG = ErrorFragment.class.getName();
|
||||
public static final String TAG = ErrorFragment.class.getName();
|
||||
|
||||
private static final String ERROR_MSG = "errorMessage";
|
||||
|
||||
@@ -40,8 +40,7 @@ public class ErrorFragment extends BaseFragment {
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle args = getArguments();
|
||||
if (args == null) throw new AssertionError();
|
||||
Bundle args = requireArguments();
|
||||
errorMessage = args.getString(ERROR_MSG);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import static androidx.core.app.ActivityCompat.finishAfterTransition;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class AbstractTabsFragment extends Fragment {
|
||||
|
||||
static String ARG_FOR_WIFI_CONNECT = "forWifiConnect";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
protected HotspotViewModel viewModel;
|
||||
|
||||
protected Button stopButton;
|
||||
protected Button connectedButton;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
getAndroidComponent(requireContext()).inject(this);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
setHasOptionsMenu(true);
|
||||
return inflater
|
||||
.inflate(R.layout.fragment_hotspot_tabs, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
TabAdapter tabAdapter = new TabAdapter(this);
|
||||
ViewPager2 viewPager = view.findViewById(R.id.pager);
|
||||
viewPager.setAdapter(tabAdapter);
|
||||
TabLayout tabLayout = view.findViewById(R.id.tabLayout);
|
||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||
// tabs are set in XML, but are just dummies that don't get added
|
||||
if (position == 0) {
|
||||
tab.setText(R.string.hotspot_tab_manual);
|
||||
tab.setIcon(R.drawable.forum_item_create_white);
|
||||
} else if (position == 1) {
|
||||
tab.setText(R.string.qr_code);
|
||||
tab.setIcon(R.drawable.ic_qr_code);
|
||||
} else throw new AssertionError();
|
||||
}).attach();
|
||||
|
||||
stopButton = view.findViewById(R.id.stopButton);
|
||||
stopButton.setOnClickListener(v -> {
|
||||
// also clears hotspot
|
||||
finishAfterTransition(requireActivity());
|
||||
});
|
||||
connectedButton = view.findViewById(R.id.connectedButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.hotspot_help_action, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_help) {
|
||||
Fragment f = new HotspotHelpFragment();
|
||||
String tag = HotspotHelpFragment.TAG;
|
||||
showFragment(getParentFragmentManager(), f, tag);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
protected abstract Fragment getFirstFragment();
|
||||
|
||||
protected abstract Fragment getSecondFragment();
|
||||
|
||||
private class TabAdapter extends FragmentStateAdapter {
|
||||
private TabAdapter(Fragment fragment) {
|
||||
super(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
if (position == 0) return getFirstFragment();
|
||||
if (position == 1) return getSecondFragment();
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
|
||||
|
||||
/**
|
||||
* This class ensures that the conditions to open a hotspot are fulfilled.
|
||||
* <p>
|
||||
* Be sure to call {@link #onRequestPermissionResult(Boolean)} and
|
||||
* {@link #onRequestWifiEnabledResult()} when you get the
|
||||
* {@link ActivityResult}.
|
||||
* <p>
|
||||
* As soon as {@link #checkAndRequestConditions()} returns true,
|
||||
* all conditions are fulfilled.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
class ConditionManager {
|
||||
|
||||
private enum Permission {
|
||||
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
|
||||
}
|
||||
|
||||
private Permission locationPermission = Permission.UNKNOWN;
|
||||
private Permission wifiSetting = Permission.SHOW_RATIONALE;
|
||||
|
||||
private final FragmentActivity ctx;
|
||||
private final WifiManager wifiManager;
|
||||
private final ActivityResultLauncher<String> locationRequest;
|
||||
private final ActivityResultLauncher<Intent> wifiRequest;
|
||||
|
||||
ConditionManager(FragmentActivity ctx,
|
||||
ActivityResultLauncher<String> locationRequest,
|
||||
ActivityResultLauncher<Intent> wifiRequest) {
|
||||
this.ctx = ctx;
|
||||
this.wifiManager = (WifiManager) ctx.getApplicationContext()
|
||||
.getSystemService(WIFI_SERVICE);
|
||||
this.locationRequest = locationRequest;
|
||||
this.wifiRequest = wifiRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to reset state when UI starts,
|
||||
* because state might have changed.
|
||||
*/
|
||||
void resetPermissions() {
|
||||
locationPermission = Permission.UNKNOWN;
|
||||
wifiSetting = Permission.SHOW_RATIONALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes a request for location permission.
|
||||
* If {@link #checkAndRequestConditions()} returns true, you can continue.
|
||||
*/
|
||||
void startConditionChecks() {
|
||||
locationRequest.launch(ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if conditions are fulfilled and flow can continue.
|
||||
*/
|
||||
boolean checkAndRequestConditions() {
|
||||
if (areEssentialPermissionsGranted()) return true;
|
||||
|
||||
// If an essential permission has been permanently denied, ask the
|
||||
// user to change the setting
|
||||
if (locationPermission == Permission.PERMANENTLY_DENIED) {
|
||||
showDenialDialog(R.string.permission_location_title,
|
||||
R.string.permission_hotspot_location_denied_body,
|
||||
getGoToSettingsListener(ctx));
|
||||
return false;
|
||||
}
|
||||
if (wifiSetting == Permission.PERMANENTLY_DENIED) {
|
||||
showDenialDialog(R.string.wifi_settings_title,
|
||||
R.string.wifi_settings_request_denied_body,
|
||||
(d, w) -> requestEnableWiFi());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should we show the rationale for location permission or Wi-Fi?
|
||||
if (locationPermission == Permission.SHOW_RATIONALE) {
|
||||
showRationale(R.string.permission_location_title,
|
||||
R.string.permission_hotspot_location_request_body,
|
||||
this::requestPermissions);
|
||||
} else if (wifiSetting == Permission.SHOW_RATIONALE) {
|
||||
showRationale(R.string.wifi_settings_title,
|
||||
R.string.wifi_settings_request_enable_body,
|
||||
this::requestEnableWiFi);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void onRequestPermissionResult(@Nullable Boolean granted) {
|
||||
if (granted != null && granted) {
|
||||
locationPermission = Permission.GRANTED;
|
||||
} else if (shouldShowRequestPermissionRationale(ctx,
|
||||
ACCESS_FINE_LOCATION)) {
|
||||
locationPermission = Permission.SHOW_RATIONALE;
|
||||
} else {
|
||||
locationPermission = Permission.PERMANENTLY_DENIED;
|
||||
}
|
||||
}
|
||||
|
||||
void onRequestWifiEnabledResult() {
|
||||
wifiSetting = wifiManager.isWifiEnabled() ? Permission.GRANTED :
|
||||
Permission.PERMANENTLY_DENIED;
|
||||
}
|
||||
|
||||
private boolean areEssentialPermissionsGranted() {
|
||||
if (SDK_INT < 29) {
|
||||
if (!wifiManager.isWifiEnabled()) {
|
||||
//noinspection deprecation
|
||||
return wifiManager.setWifiEnabled(true);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return locationPermission == Permission.GRANTED
|
||||
&& wifiManager.isWifiEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
private void showDenialDialog(@StringRes int title, @StringRes int body,
|
||||
OnClickListener onOkClicked) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setPositiveButton(R.string.ok, onOkClicked);
|
||||
builder.setNegativeButton(R.string.cancel,
|
||||
(dialog, which) -> ctx.supportFinishAfterTransition());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void showRationale(@StringRes int title, @StringRes int body,
|
||||
Runnable onContinueClicked) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setNeutralButton(R.string.continue_button,
|
||||
(dialog, which) -> onContinueClicked.run());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void requestPermissions() {
|
||||
locationRequest.launch(ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
private void requestEnableWiFi() {
|
||||
Intent i = SDK_INT < 29 ?
|
||||
new Intent(Settings.ACTION_WIFI_SETTINGS) :
|
||||
new Intent(Settings.Panel.ACTION_WIFI);
|
||||
wifiRequest.launch(i);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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 org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
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.activity.result.contract.ActivityResultContracts.CreateDocument;
|
||||
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
|
||||
public class FallbackFragment extends BaseFragment {
|
||||
|
||||
public static final String TAG = FallbackFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private HotspotViewModel viewModel;
|
||||
private final ActivityResultLauncher<String> launcher =
|
||||
registerForActivityResult(new CreateDocument(),
|
||||
this::onDocumentCreated);
|
||||
private Button fallbackButton;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
FragmentActivity activity = requireActivity();
|
||||
getAndroidComponent(activity).inject(this);
|
||||
viewModel = new ViewModelProvider(activity, viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater
|
||||
.inflate(R.layout.fragment_hotspot_save_apk, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(v, savedInstanceState);
|
||||
|
||||
fallbackButton = v.findViewById(R.id.fallbackButton);
|
||||
progressBar = v.findViewById(R.id.progressBar);
|
||||
fallbackButton.setOnClickListener(view -> {
|
||||
beginDelayedTransition((ViewGroup) v);
|
||||
fallbackButton.setVisibility(INVISIBLE);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
|
||||
if (SDK_INT >= 19) launcher.launch(getApkFileName());
|
||||
else viewModel.exportApk();
|
||||
});
|
||||
viewModel.getSavedApkToUri()
|
||||
.observeEvent(this, uri -> shareUri(this, uri));
|
||||
}
|
||||
|
||||
private void onDocumentCreated(@Nullable Uri uri) {
|
||||
showButton();
|
||||
if (uri != null) viewModel.exportApk(uri);
|
||||
}
|
||||
|
||||
private void showButton() {
|
||||
beginDelayedTransition((ViewGroup) requireView());
|
||||
fallbackButton.setVisibility(VISIBLE);
|
||||
progressBar.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
static void shareUri(Fragment fragment, Uri uri) {
|
||||
Intent i = new Intent(ACTION_SEND);
|
||||
i.putExtra(EXTRA_STREAM, uri);
|
||||
i.setType("*/*"); // gives us all sharing options
|
||||
i.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
||||
Context ctx = fragment.requireContext();
|
||||
if (SDK_INT <= 19) {
|
||||
// Workaround for Android bug:
|
||||
// ctx.grantUriPermission also needed for Android 4
|
||||
List<ResolveInfo> 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);
|
||||
}
|
||||
}
|
||||
fragment.startActivity(Intent.createChooser(i, null));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.ACTION_STOP_HOTSPOT;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class HotspotActivity extends BriarActivity
|
||||
implements BaseFragmentListener {
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private HotspotViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_fragment_container);
|
||||
|
||||
ActionBar ab = getSupportActionBar();
|
||||
if (ab != null) {
|
||||
ab.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
viewModel.getState().observe(this, hotspotState -> {
|
||||
if (hotspotState instanceof HotspotStarted) {
|
||||
HotspotStarted started = (HotspotStarted) hotspotState;
|
||||
String tag = HotspotFragment.TAG;
|
||||
// check if fragment is already added
|
||||
// to not lose state on configuration changes
|
||||
if (fm.findFragmentByTag(tag) == null) {
|
||||
if (!started.consume()) {
|
||||
showFragment(fm, new HotspotFragment(), tag);
|
||||
}
|
||||
}
|
||||
} else if (hotspotState instanceof HotspotError) {
|
||||
HotspotError error = ((HotspotError) hotspotState);
|
||||
showErrorFragment(error.getError());
|
||||
}
|
||||
});
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// If there is no saved instance state, just start with the intro fragment.
|
||||
fm.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, new HotspotIntroFragment(),
|
||||
HotspotIntroFragment.TAG)
|
||||
.commit();
|
||||
} else if (viewModel.getState().getValue() == null) {
|
||||
// If there is saved instance state, then there's either been an
|
||||
// configuration change like rotated device or the activity has been
|
||||
// destroyed and is now being re-created.
|
||||
// In the latter case, the view model will have been destroyed, too.
|
||||
// The activity can only have been destroyed if the user navigated
|
||||
// away from the HotspotActivity which is nothing we
|
||||
// intend to support, so we want to detect that and start from scratch
|
||||
// in this case. We need to clean up existing fragments in order not
|
||||
// to stack new fragments on top of old ones.
|
||||
|
||||
// If it is a configuration change and we moved past the intro
|
||||
// fragment already, then the view model state will be != null,
|
||||
// hence we can use this check for null to determine the destroyed
|
||||
// activity. It can also be null if the user has not pressed
|
||||
// "start sharing" yet, but in that case it won't harm to start from
|
||||
// scratch.
|
||||
|
||||
Fragment current = fm.findFragmentById(R.id.fragmentContainer);
|
||||
if (current instanceof HotspotIntroFragment) {
|
||||
// If the currently displayed fragment is the intro fragment,
|
||||
// there's nothing we need to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove everything from the back stack.
|
||||
fm.popBackStackImmediate(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
|
||||
// Start fresh with the intro fragment.
|
||||
fm.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, new HotspotIntroFragment(),
|
||||
HotspotIntroFragment.TAG)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void showErrorFragment(String error) {
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
String tag = HotspotErrorFragment.TAG;
|
||||
if (fm.findFragmentByTag(tag) == null) {
|
||||
Fragment f = HotspotErrorFragment.newInstance(error);
|
||||
showFragment(fm, f, tag, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (ACTION_STOP_HOTSPOT.equals(intent.getAction())) {
|
||||
// also closes hotspot
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static org.briarproject.briar.android.util.UiUtils.triggerFeedback;
|
||||
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class HotspotErrorFragment extends BaseFragment {
|
||||
|
||||
public static final String TAG = HotspotErrorFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private static final String ERROR_MSG = "errorMessage";
|
||||
|
||||
public static HotspotErrorFragment newInstance(String message) {
|
||||
HotspotErrorFragment f = new HotspotErrorFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ERROR_MSG, message);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
}
|
||||
|
||||
private String errorMessage;
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle args = requireArguments();
|
||||
errorMessage = args.getString(ERROR_MSG);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
requireActivity().setTitle(R.string.error);
|
||||
return inflater
|
||||
.inflate(R.layout.fragment_hotspot_error, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(v, savedInstanceState);
|
||||
TextView msg = v.findViewById(R.id.errorMessageDetail);
|
||||
msg.setText(errorMessage);
|
||||
|
||||
Button feedbackButton = v.findViewById(R.id.feedbackButton);
|
||||
feedbackButton.setOnClickListener(
|
||||
button -> triggerFeedback(requireContext(), errorMessage));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class HotspotFragment extends AbstractTabsFragment {
|
||||
|
||||
public final static String TAG = HotspotFragment.class.getName();
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
connectedButton.setOnClickListener(v -> showNextFragment());
|
||||
viewModel.getPeerConnectedEvent().observeEvent(getViewLifecycleOwner(),
|
||||
this::onPeerConnected);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Fragment getFirstFragment() {
|
||||
return ManualHotspotFragment.newInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Fragment getSecondFragment() {
|
||||
return QrHotspotFragment.newInstance(true);
|
||||
}
|
||||
|
||||
private void onPeerConnected(boolean connected) {
|
||||
if (!connected) return;
|
||||
new BriarSnackbarBuilder()
|
||||
.setAction(R.string.hotspot_peer_connected_action, v ->
|
||||
showNextFragment())
|
||||
.make(connectedButton, R.string.hotspot_peer_connected,
|
||||
Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(connectedButton)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showNextFragment() {
|
||||
Fragment f = new WebsiteFragment();
|
||||
String tag = WebsiteFragment.TAG;
|
||||
showFragment(getParentFragmentManager(), f, tag);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class HotspotHelpFragment extends Fragment {
|
||||
|
||||
public final static String TAG = HotspotHelpFragment.class.getName();
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater
|
||||
.inflate(R.layout.fragment_hotspot_help, container, false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
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 android.widget.TextView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static androidx.transition.TransitionManager.beginDelayedTransition;
|
||||
import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class HotspotIntroFragment extends Fragment {
|
||||
|
||||
public final static String TAG = HotspotIntroFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private HotspotViewModel viewModel;
|
||||
private ConditionManager conditionManager;
|
||||
|
||||
private Button startButton;
|
||||
private ProgressBar progressBar;
|
||||
private TextView progressTextView;
|
||||
|
||||
private final ActivityResultLauncher<String> locationRequest =
|
||||
registerForActivityResult(new RequestPermission(), granted -> {
|
||||
conditionManager.onRequestPermissionResult(granted);
|
||||
startHotspot();
|
||||
});
|
||||
private final ActivityResultLauncher<Intent> wifiRequest =
|
||||
registerForActivityResult(new StartActivityForResult(), result -> {
|
||||
conditionManager.onRequestWifiEnabledResult();
|
||||
startHotspot();
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
FragmentActivity activity = requireActivity();
|
||||
getAndroidComponent(activity).inject(this);
|
||||
viewModel = new ViewModelProvider(activity, viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
conditionManager =
|
||||
new ConditionManager(activity, locationRequest, wifiRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View v = inflater
|
||||
.inflate(R.layout.fragment_hotspot_intro, container, false);
|
||||
|
||||
startButton = v.findViewById(R.id.startButton);
|
||||
progressBar = v.findViewById(R.id.progressBar);
|
||||
progressTextView = v.findViewById(R.id.progressTextView);
|
||||
|
||||
startButton.setOnClickListener(button -> {
|
||||
startButton.setEnabled(false);
|
||||
conditionManager.startConditionChecks();
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
conditionManager.resetPermissions();
|
||||
}
|
||||
|
||||
private void startHotspot() {
|
||||
startButton.setEnabled(true);
|
||||
if (conditionManager.checkAndRequestConditions()) {
|
||||
showInstallWarningIfNeeded();
|
||||
beginDelayedTransition((ViewGroup) requireView());
|
||||
startButton.setVisibility(INVISIBLE);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
progressTextView.setVisibility(VISIBLE);
|
||||
viewModel.startHotspot();
|
||||
}
|
||||
}
|
||||
|
||||
private void showInstallWarningIfNeeded() {
|
||||
Context ctx = requireContext();
|
||||
ApplicationInfo applicationInfo;
|
||||
try {
|
||||
applicationInfo = ctx.getPackageManager()
|
||||
.getApplicationInfo(ctx.getPackageName(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
// test only apps can not be installed
|
||||
if ((applicationInfo.flags & FLAG_TEST_ONLY) == FLAG_TEST_ONLY) {
|
||||
int color = getResources().getColor(R.color.briar_red_500);
|
||||
Snackbar.make(requireView(), R.string.hotspot_flag_test,
|
||||
LENGTH_LONG).setBackgroundTint(color).show();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.net.wifi.p2p.WifiP2pConfig;
|
||||
import android.net.wifi.p2p.WifiP2pGroup;
|
||||
import android.net.wifi.p2p.WifiP2pManager;
|
||||
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
|
||||
import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
|
||||
import android.os.Handler;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
|
||||
import org.briarproject.briar.android.util.QrCodeUtils;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import static android.content.Context.WIFI_P2P_SERVICE;
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
|
||||
import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
|
||||
import static android.net.wifi.p2p.WifiP2pConfig.GROUP_OWNER_BAND_2GHZ;
|
||||
import static android.net.wifi.p2p.WifiP2pManager.BUSY;
|
||||
import static android.net.wifi.p2p.WifiP2pManager.ERROR;
|
||||
import static android.net.wifi.p2p.WifiP2pManager.NO_SERVICE_REQUESTS;
|
||||
import static android.net.wifi.p2p.WifiP2pManager.P2P_UNSUPPORTED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.briar.android.util.UiUtils.handleException;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class HotspotManager implements ActionListener {
|
||||
|
||||
interface HotspotListener {
|
||||
void onStartingHotspot();
|
||||
|
||||
@IoExecutor
|
||||
void onHotspotStarted(NetworkConfig networkConfig);
|
||||
|
||||
@UiThread
|
||||
void onDeviceConnected();
|
||||
|
||||
void onHotspotStopped();
|
||||
|
||||
void onHotspotError(String error);
|
||||
}
|
||||
|
||||
private static final Logger LOG = getLogger(HotspotManager.class.getName());
|
||||
|
||||
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
|
||||
private static final int RETRY_DELAY_MILLIS = 1000;
|
||||
private static final String HOTSPOT_NAMESPACE = "hotspot";
|
||||
private static final String HOTSPOT_KEY_SSID = "ssid";
|
||||
private static final String HOTSPOT_KEY_PASS = "pass";
|
||||
|
||||
private final Context ctx;
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
@IoExecutor
|
||||
private final Executor ioExecutor;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final SettingsManager settingsManager;
|
||||
private final SecureRandom random;
|
||||
private final WifiManager wifiManager;
|
||||
private final WifiP2pManager wifiP2pManager;
|
||||
private final Handler handler;
|
||||
private final String lockTag;
|
||||
|
||||
private HotspotListener listener;
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
private WifiP2pManager.Channel channel;
|
||||
@RequiresApi(29)
|
||||
private volatile NetworkConfig savedNetworkConfig;
|
||||
|
||||
@Inject
|
||||
HotspotManager(Application ctx,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
@IoExecutor Executor ioExecutor,
|
||||
AndroidExecutor androidExecutor,
|
||||
SettingsManager settingsManager,
|
||||
SecureRandom random) {
|
||||
this.ctx = ctx.getApplicationContext();
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.settingsManager = settingsManager;
|
||||
this.random = random;
|
||||
wifiManager = (WifiManager) ctx.getApplicationContext()
|
||||
.getSystemService(WIFI_SERVICE);
|
||||
wifiP2pManager =
|
||||
(WifiP2pManager) ctx.getSystemService(WIFI_P2P_SERVICE);
|
||||
handler = new Handler(ctx.getMainLooper());
|
||||
lockTag = ctx.getPackageName() + ":app-sharing-hotspot";
|
||||
}
|
||||
|
||||
void setHotspotListener(HotspotListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void startWifiP2pHotspot() {
|
||||
if (wifiP2pManager == null) {
|
||||
listener.onHotspotError(
|
||||
ctx.getString(R.string.hotspot_error_no_wifi_direct));
|
||||
return;
|
||||
}
|
||||
listener.onStartingHotspot();
|
||||
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
|
||||
if (channel == null) {
|
||||
listener.onHotspotError(
|
||||
ctx.getString(R.string.hotspot_error_no_wifi_direct));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (SDK_INT >= 29) {
|
||||
dbExecutor.execute(() -> {
|
||||
// load savedNetworkConfig before starting hotspot
|
||||
loadSavedNetworkConfig();
|
||||
androidExecutor.runOnUiThread(() -> {
|
||||
WifiP2pConfig config = new WifiP2pConfig.Builder()
|
||||
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
|
||||
.setNetworkName(savedNetworkConfig.ssid)
|
||||
.setPassphrase(savedNetworkConfig.password)
|
||||
.build();
|
||||
acquireLock();
|
||||
wifiP2pManager.createGroup(channel, config, this);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
acquireLock();
|
||||
wifiP2pManager.createGroup(channel, this);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
// this should never happen, because we request permissions before
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
|
||||
public void onSuccess() {
|
||||
requestGroupInfo(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
|
||||
public void onFailure(int reason) {
|
||||
if (reason == BUSY) {
|
||||
// Hotspot already running
|
||||
requestGroupInfo(1);
|
||||
} else if (reason == P2P_UNSUPPORTED) {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed,
|
||||
"p2p unsupported"));
|
||||
} else if (reason == ERROR) {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed, "p2p error"));
|
||||
} else if (reason == NO_SERVICE_REQUESTS) {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed,
|
||||
"no service requests"));
|
||||
} else {
|
||||
// all cases covered, in doubt set to error
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed_unknown,
|
||||
reason));
|
||||
}
|
||||
}
|
||||
|
||||
void stopWifiP2pHotspot() {
|
||||
if (channel == null) return;
|
||||
wifiP2pManager.removeGroup(channel, new ActionListener() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
releaseHotspot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(int reason) {
|
||||
// not propagating back error
|
||||
releaseHotspot();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void acquireLock() {
|
||||
// WIFI_MODE_FULL has no effect on API >= 29
|
||||
int lockType =
|
||||
SDK_INT >= 29 ? WIFI_MODE_FULL_HIGH_PERF : WIFI_MODE_FULL;
|
||||
wifiLock = wifiManager.createWifiLock(lockType, lockTag);
|
||||
wifiLock.acquire();
|
||||
}
|
||||
|
||||
private void releaseHotspot() {
|
||||
listener.onHotspotStopped();
|
||||
closeChannelAndReleaseLock();
|
||||
}
|
||||
|
||||
private void releaseHotspotWithError(String error) {
|
||||
listener.onHotspotError(error);
|
||||
closeChannelAndReleaseLock();
|
||||
}
|
||||
|
||||
private void closeChannelAndReleaseLock() {
|
||||
if (SDK_INT >= 27) channel.close();
|
||||
channel = null;
|
||||
wifiLock.release();
|
||||
}
|
||||
|
||||
private void requestGroupInfo(int attempt) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("requestGroupInfo attempt: " + attempt);
|
||||
}
|
||||
GroupInfoListener groupListener = group -> {
|
||||
boolean valid = isGroupValid(group);
|
||||
// If the group is valid, set the hotspot to started. If we don't
|
||||
// have any attempts left, we try what we got
|
||||
if (valid || attempt >= MAX_GROUP_INFO_ATTEMPTS) {
|
||||
onHotspotStarted(group);
|
||||
} else {
|
||||
retryRequestingGroupInfo(attempt);
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (channel == null) return;
|
||||
wifiP2pManager.requestGroupInfo(channel, groupListener);
|
||||
} catch (SecurityException e) {
|
||||
// this should never happen, because we request permissions before
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onHotspotStarted(WifiP2pGroup group) {
|
||||
DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
|
||||
ioExecutor.execute(() -> {
|
||||
String content = createWifiLoginString(group.getNetworkName(),
|
||||
group.getPassphrase());
|
||||
Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
|
||||
NetworkConfig config = new NetworkConfig(group.getNetworkName(),
|
||||
group.getPassphrase(), qrCode);
|
||||
listener.onHotspotStarted(config);
|
||||
});
|
||||
requestGroupInfoForConnection();
|
||||
}
|
||||
|
||||
private boolean isGroupValid(@Nullable WifiP2pGroup group) {
|
||||
if (group == null) {
|
||||
LOG.info("group is null");
|
||||
return false;
|
||||
} else if (!group.getNetworkName().startsWith("DIRECT-")) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("received networkName without prefix 'DIRECT-': " +
|
||||
group.getNetworkName());
|
||||
}
|
||||
return false;
|
||||
} else if (SDK_INT >= 29) {
|
||||
// if we get here, the savedNetworkConfig must have a value
|
||||
String networkName = savedNetworkConfig.ssid;
|
||||
if (!networkName.equals(group.getNetworkName())) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("expected networkName: " + networkName);
|
||||
LOG.info("received networkName: " + group.getNetworkName());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void retryRequestingGroupInfo(int attempt) {
|
||||
LOG.info("retrying");
|
||||
// On some devices we need to wait for the group info to become available
|
||||
if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
|
||||
handler.postDelayed(() -> requestGroupInfo(attempt + 1),
|
||||
RETRY_DELAY_MILLIS);
|
||||
} else {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_no_group_info));
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void requestGroupInfoForConnection() {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("requestGroupInfo for connection");
|
||||
}
|
||||
GroupInfoListener groupListener = group -> {
|
||||
if (group == null || group.getClientList().isEmpty()) {
|
||||
handler.postDelayed(this::requestGroupInfoForConnection,
|
||||
RETRY_DELAY_MILLIS);
|
||||
} else {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("client list " + group.getClientList());
|
||||
}
|
||||
listener.onDeviceConnected();
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (channel == null) return;
|
||||
wifiP2pManager.requestGroupInfo(channel, groupListener);
|
||||
} catch (SecurityException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store persistent Wi-Fi SSID and passphrase in Settings to improve UX
|
||||
* so that users don't have to change them when attempting to connect.
|
||||
* Works only on API 29 and above.
|
||||
*/
|
||||
@RequiresApi(29)
|
||||
@DatabaseExecutor
|
||||
private void loadSavedNetworkConfig() {
|
||||
try {
|
||||
Settings settings = settingsManager.getSettings(HOTSPOT_NAMESPACE);
|
||||
String ssid = settings.get(HOTSPOT_KEY_SSID);
|
||||
String pass = settings.get(HOTSPOT_KEY_PASS);
|
||||
if (ssid == null || pass == null) {
|
||||
ssid = getSsid();
|
||||
pass = getPassword();
|
||||
settings.put(HOTSPOT_KEY_SSID, ssid);
|
||||
settings.put(HOTSPOT_KEY_PASS, pass);
|
||||
settingsManager.mergeSettings(settings, HOTSPOT_NAMESPACE);
|
||||
}
|
||||
savedNetworkConfig = new NetworkConfig(ssid, pass, null);
|
||||
} catch (DbException e) {
|
||||
handleException(ctx, androidExecutor, LOG, e);
|
||||
// probably never happens, but if lets use non-persistent data
|
||||
String ssid = getSsid();
|
||||
String pass = getPassword();
|
||||
savedNetworkConfig = new NetworkConfig(ssid, pass, null);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private String getSsid() {
|
||||
return "DIRECT-" + getRandomString(2) + "-" +
|
||||
getRandomString(10);
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private String getPassword() {
|
||||
return getRandomString(8);
|
||||
}
|
||||
|
||||
private static String createWifiLoginString(String ssid, String password) {
|
||||
// https://en.wikipedia.org/wiki/QR_code#WiFi_network_login
|
||||
// do not remove the dangling ';', it can cause problems to omit it
|
||||
return "WIFI:S:" + ssid + ";T:WPA;P:" + password + ";;";
|
||||
}
|
||||
|
||||
private static final String digits = "123456789"; // avoid 0
|
||||
private static final String letters = "abcdefghijkmnopqrstuvwxyz"; // no l
|
||||
private static final String LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // no I, O
|
||||
|
||||
private String getRandomString(int length) {
|
||||
char[] c = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (random.nextBoolean()) {
|
||||
c[i] = random(digits);
|
||||
} else if (random.nextBoolean()) {
|
||||
c[i] = random(letters);
|
||||
} else {
|
||||
c[i] = random(LETTERS);
|
||||
}
|
||||
}
|
||||
return new String(c);
|
||||
}
|
||||
|
||||
private char random(String universe) {
|
||||
return universe.charAt(random.nextInt(universe.length()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
@Module
|
||||
public interface HotspotModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(HotspotViewModel.class)
|
||||
ViewModel bindHotspotViewModel(HotspotViewModel hotspotViewModel);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@NotNullByDefault
|
||||
abstract class HotspotState {
|
||||
|
||||
static class StartingHotspot extends HotspotState {
|
||||
}
|
||||
|
||||
static class NetworkConfig {
|
||||
final String ssid, password;
|
||||
@Nullable
|
||||
final Bitmap qrCode;
|
||||
|
||||
NetworkConfig(String ssid, String password, @Nullable Bitmap qrCode) {
|
||||
this.ssid = ssid;
|
||||
this.password = password;
|
||||
this.qrCode = qrCode;
|
||||
}
|
||||
}
|
||||
|
||||
static class WebsiteConfig {
|
||||
final String url;
|
||||
@Nullable
|
||||
final Bitmap qrCode;
|
||||
|
||||
WebsiteConfig(String url, @Nullable Bitmap qrCode) {
|
||||
this.url = url;
|
||||
this.qrCode = qrCode;
|
||||
}
|
||||
}
|
||||
|
||||
static class HotspotStarted extends HotspotState {
|
||||
private final NetworkConfig networkConfig;
|
||||
private final WebsiteConfig websiteConfig;
|
||||
// 'consumed' is set to true once this state triggered a UI change, i.e.
|
||||
// moving to the next fragment.
|
||||
private boolean consumed = false;
|
||||
|
||||
HotspotStarted(NetworkConfig networkConfig,
|
||||
WebsiteConfig websiteConfig) {
|
||||
this.networkConfig = networkConfig;
|
||||
this.websiteConfig = websiteConfig;
|
||||
}
|
||||
|
||||
NetworkConfig getNetworkConfig() {
|
||||
return networkConfig;
|
||||
}
|
||||
|
||||
WebsiteConfig getWebsiteConfig() {
|
||||
return websiteConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this state as consumed, i.e. the UI has already done something
|
||||
* as a result of the state changing to this. This can be used in order
|
||||
* to not repeat actions such as showing fragments on rotation changes.
|
||||
*/
|
||||
@UiThread
|
||||
boolean consume() {
|
||||
boolean old = consumed;
|
||||
consumed = true;
|
||||
return old;
|
||||
}
|
||||
}
|
||||
|
||||
static class HotspotError extends HotspotState {
|
||||
private final String error;
|
||||
|
||||
HotspotError(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
String getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
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;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.hotspot.HotspotManager.HotspotListener;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.StartingHotspot;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
|
||||
import org.briarproject.briar.android.hotspot.WebServerManager.WebServerListener;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
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;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
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.Level.WARNING;
|
||||
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
|
||||
implements HotspotListener, WebServerListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(HotspotViewModel.class.getName());
|
||||
|
||||
@IoExecutor
|
||||
private final Executor ioExecutor;
|
||||
private final AndroidNotificationManager notificationManager;
|
||||
private final HotspotManager hotspotManager;
|
||||
private final WebServerManager webServerManager;
|
||||
|
||||
private final MutableLiveData<HotspotState> state =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveEvent<Boolean> peerConnected =
|
||||
new MutableLiveEvent<>();
|
||||
private final MutableLiveEvent<Uri> savedApkToUri =
|
||||
new MutableLiveEvent<>();
|
||||
|
||||
@Nullable
|
||||
// Field to temporarily store the network config received via onHotspotStarted()
|
||||
// in order to post it along with a HotspotStarted status
|
||||
private volatile NetworkConfig networkConfig;
|
||||
|
||||
@Inject
|
||||
HotspotViewModel(Application app,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
@IoExecutor Executor ioExecutor,
|
||||
HotspotManager hotspotManager,
|
||||
WebServerManager webServerManager,
|
||||
AndroidNotificationManager notificationManager) {
|
||||
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.notificationManager = notificationManager;
|
||||
this.hotspotManager = hotspotManager;
|
||||
this.hotspotManager.setHotspotListener(this);
|
||||
this.webServerManager = webServerManager;
|
||||
this.webServerManager.setListener(this);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void startHotspot() {
|
||||
HotspotState s = state.getValue();
|
||||
if (s instanceof HotspotStarted) {
|
||||
// This can happen if the user navigates back to intro fragment and
|
||||
// taps 'start sharing' again. In this case, don't try to start the
|
||||
// hotspot again. Instead, just create a new, unconsumed HotspotStarted
|
||||
// event with the same config.
|
||||
HotspotStarted old = (HotspotStarted) s;
|
||||
state.setValue(new HotspotStarted(old.getNetworkConfig(),
|
||||
old.getWebsiteConfig()));
|
||||
} else {
|
||||
hotspotManager.startWifiP2pHotspot();
|
||||
notificationManager.showHotspotNotification();
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void stopHotspot() {
|
||||
ioExecutor.execute(webServerManager::stopWebServer);
|
||||
hotspotManager.stopWifiP2pHotspot();
|
||||
notificationManager.clearHotspotNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
stopHotspot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartingHotspot() {
|
||||
state.setValue(new StartingHotspot());
|
||||
}
|
||||
|
||||
@Override
|
||||
@IoExecutor
|
||||
public void onHotspotStarted(NetworkConfig networkConfig) {
|
||||
this.networkConfig = networkConfig;
|
||||
LOG.info("starting webserver");
|
||||
webServerManager.startWebServer();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onDeviceConnected() {
|
||||
peerConnected.setEvent(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHotspotStopped() {
|
||||
LOG.info("stopping webserver");
|
||||
ioExecutor.execute(webServerManager::stopWebServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHotspotError(String error) {
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Hotspot error: " + error);
|
||||
}
|
||||
state.postValue(new HotspotError(error));
|
||||
ioExecutor.execute(webServerManager::stopWebServer);
|
||||
notificationManager.clearHotspotNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
@IoExecutor
|
||||
public void onWebServerStarted(WebsiteConfig websiteConfig) {
|
||||
NetworkConfig nc = requireNonNull(networkConfig);
|
||||
state.postValue(new HotspotStarted(nc, websiteConfig));
|
||||
networkConfig = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@IoExecutor
|
||||
public void onWebServerError() {
|
||||
state.postValue(new HotspotError(getApplication()
|
||||
.getString(R.string.hotspot_error_web_server_start)));
|
||||
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<HotspotState> getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
LiveEvent<Boolean> getPeerConnectedEvent() {
|
||||
return peerConnected;
|
||||
}
|
||||
|
||||
LiveEvent<Uri> getSavedApkToUri() {
|
||||
return savedApkToUri;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
import static org.briarproject.briar.android.hotspot.AbstractTabsFragment.ARG_FOR_WIFI_CONNECT;
|
||||
import static org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ManualHotspotFragment extends Fragment {
|
||||
|
||||
public final static String TAG = ManualHotspotFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private HotspotViewModel viewModel;
|
||||
|
||||
static ManualHotspotFragment newInstance(boolean forWifiConnect) {
|
||||
ManualHotspotFragment f = new ManualHotspotFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(ARG_FOR_WIFI_CONNECT, forWifiConnect);
|
||||
f.setArguments(bundle);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
getAndroidComponent(requireContext()).inject(this);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater
|
||||
.inflate(R.layout.fragment_hotspot_manual, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(v, savedInstanceState);
|
||||
|
||||
TextView manualIntroView = v.findViewById(R.id.manualIntroView);
|
||||
TextView ssidLabelView = v.findViewById(R.id.ssidLabelView);
|
||||
TextView ssidView = v.findViewById(R.id.ssidView);
|
||||
TextView passwordView = v.findViewById(R.id.passwordView);
|
||||
TextView altView = v.findViewById(R.id.altView);
|
||||
|
||||
Consumer<HotspotStarted> consumer;
|
||||
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
|
||||
manualIntroView.setText(R.string.hotspot_manual_wifi);
|
||||
ssidLabelView.setText(R.string.hotspot_manual_wifi_ssid);
|
||||
consumer = state -> {
|
||||
ssidView.setText(state.getNetworkConfig().ssid);
|
||||
passwordView.setText(state.getNetworkConfig().password);
|
||||
};
|
||||
altView.setText(R.string.hotspot_manual_wifi_alt);
|
||||
} else {
|
||||
manualIntroView.setText(R.string.hotspot_manual_site);
|
||||
ssidLabelView.setText(R.string.hotspot_manual_site_address);
|
||||
consumer = state -> ssidView.setText(state.getWebsiteConfig().url);
|
||||
altView.setText(R.string.hotspot_manual_site_alt);
|
||||
v.findViewById(R.id.passwordLabelView).setVisibility(GONE);
|
||||
passwordView.setVisibility(GONE);
|
||||
}
|
||||
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
|
||||
// we only expect to be in this state here
|
||||
if (state instanceof HotspotStarted) {
|
||||
consumer.accept((HotspotStarted) state);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
import static org.briarproject.briar.android.hotspot.AbstractTabsFragment.ARG_FOR_WIFI_CONNECT;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class QrHotspotFragment extends Fragment {
|
||||
|
||||
public final static String TAG = QrHotspotFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private HotspotViewModel viewModel;
|
||||
|
||||
static QrHotspotFragment newInstance(boolean forWifiConnect) {
|
||||
QrHotspotFragment f = new QrHotspotFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(ARG_FOR_WIFI_CONNECT, forWifiConnect);
|
||||
f.setArguments(bundle);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
getAndroidComponent(requireContext()).inject(this);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View v = inflater
|
||||
.inflate(R.layout.fragment_hotspot_qr, container, false);
|
||||
|
||||
TextView qrIntroView = v.findViewById(R.id.qrIntroView);
|
||||
ImageView qrCodeView = v.findViewById(R.id.qrCodeView);
|
||||
|
||||
Consumer<HotspotStarted> consumer;
|
||||
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
|
||||
qrIntroView.setText(R.string.hotspot_qr_wifi);
|
||||
consumer = state ->
|
||||
qrCodeView.setImageBitmap(state.getNetworkConfig().qrCode);
|
||||
} else {
|
||||
qrIntroView.setText(R.string.hotspot_qr_site);
|
||||
consumer = state ->
|
||||
qrCodeView.setImageBitmap(state.getWebsiteConfig().qrCode);
|
||||
}
|
||||
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
|
||||
if (state instanceof HotspotStarted) {
|
||||
consumer.accept((HotspotStarted) state);
|
||||
}
|
||||
});
|
||||
return v;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
|
||||
import static android.util.Xml.Encoding.UTF_8;
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND;
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
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 {
|
||||
|
||||
final static int PORT = 9999;
|
||||
|
||||
private static final Logger LOG = getLogger(WebServer.class.getName());
|
||||
private static final String FILE_HTML = "hotspot.html";
|
||||
private static final Pattern REGEX_AGENT =
|
||||
Pattern.compile("Android ([0-9]+)");
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
WebServer(Context ctx) {
|
||||
super(PORT);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws IOException {
|
||||
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
if (session.getUri().endsWith("favicon.ico")) {
|
||||
return newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
|
||||
NOT_FOUND.getDescription());
|
||||
}
|
||||
if (session.getUri().endsWith(".apk")) {
|
||||
return serveApk();
|
||||
}
|
||||
Response res;
|
||||
try {
|
||||
String html = getHtml(session.getHeaders().get("user-agent"));
|
||||
res = newFixedLengthResponse(OK, MIME_HTML, html);
|
||||
} catch (Exception e) {
|
||||
logException(LOG, WARNING, e);
|
||||
res = newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT,
|
||||
ctx.getString(R.string.hotspot_error_web_server_serve));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private String getHtml(@Nullable String userAgent) throws Exception {
|
||||
Document doc;
|
||||
try (InputStream is = ctx.getAssets().open(FILE_HTML)) {
|
||||
doc = Jsoup.parse(is, UTF_8.name(), "");
|
||||
}
|
||||
String app = ctx.getString(R.string.app_name);
|
||||
String appV = app + " " + VERSION_NAME;
|
||||
String filename = getApkFileName();
|
||||
doc.select("#download_title").first()
|
||||
.text(ctx.getString(R.string.website_download_title, appV));
|
||||
doc.select("#download_intro").first()
|
||||
.text(ctx.getString(R.string.website_download_intro, app));
|
||||
doc.select(".button").first().attr("href", filename);
|
||||
doc.select("#download_button").first()
|
||||
.text(ctx.getString(R.string.website_download_title, app));
|
||||
doc.select("#download_outro").first()
|
||||
.text(ctx.getString(R.string.website_download_outro));
|
||||
doc.select("#troubleshooting_title").first()
|
||||
.text(ctx.getString(R.string.website_troubleshooting_title));
|
||||
doc.select("#troubleshooting_1").first()
|
||||
.text(ctx.getString(R.string.website_troubleshooting_1));
|
||||
doc.select("#troubleshooting_2").first()
|
||||
.text(getUnknownSourcesString(userAgent));
|
||||
return doc.outerHtml();
|
||||
}
|
||||
|
||||
private String getUnknownSourcesString(@Nullable String userAgent) {
|
||||
boolean is8OrHigher = false;
|
||||
if (userAgent != null) {
|
||||
Matcher matcher = REGEX_AGENT.matcher(userAgent);
|
||||
if (matcher.find()) {
|
||||
int androidMajorVersion =
|
||||
Integer.parseInt(requireNonNull(matcher.group(1)));
|
||||
is8OrHigher = androidMajorVersion >= 8;
|
||||
}
|
||||
}
|
||||
return is8OrHigher ?
|
||||
ctx.getString(R.string.website_troubleshooting_2_new) :
|
||||
ctx.getString(R.string.website_troubleshooting_2_old);
|
||||
}
|
||||
|
||||
private Response serveApk() {
|
||||
String mime = "application/vnd.android.package-archive";
|
||||
|
||||
File file = new File(ctx.getPackageCodePath());
|
||||
long fileLen = file.length();
|
||||
|
||||
Response res;
|
||||
try {
|
||||
FileInputStream fis = new FileInputStream(file);
|
||||
res = newFixedLengthResponse(OK, mime, fis, fileLen);
|
||||
res.addHeader("Content-Length", "" + fileLen);
|
||||
} catch (FileNotFoundException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
res = newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
|
||||
ctx.getString(R.string.hotspot_error_web_server_serve));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.app.Application;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
|
||||
import org.briarproject.briar.android.util.QrCodeUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.list;
|
||||
import static java.util.logging.Level.INFO;
|
||||
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.android.hotspot.WebServer.PORT;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class WebServerManager {
|
||||
|
||||
interface WebServerListener {
|
||||
@IoExecutor
|
||||
void onWebServerStarted(WebsiteConfig websiteConfig);
|
||||
|
||||
@IoExecutor
|
||||
void onWebServerError();
|
||||
}
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(WebServerManager.class.getName());
|
||||
|
||||
private final WebServer webServer;
|
||||
private final DisplayMetrics dm;
|
||||
|
||||
private WebServerListener listener;
|
||||
|
||||
@Inject
|
||||
WebServerManager(Application ctx) {
|
||||
webServer = new WebServer(ctx);
|
||||
dm = ctx.getResources().getDisplayMetrics();
|
||||
}
|
||||
|
||||
void setListener(WebServerListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
void startWebServer() {
|
||||
try {
|
||||
webServer.start();
|
||||
onWebServerStarted();
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
listener.onWebServerError();
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void onWebServerStarted() {
|
||||
String url = "http://192.168.49.1:" + PORT;
|
||||
InetAddress address = getAccessPointAddress();
|
||||
if (address == null) {
|
||||
LOG.info(
|
||||
"Could not find access point address, assuming 192.168.49.1");
|
||||
} else {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Access point address " + address.getHostAddress());
|
||||
}
|
||||
url = "http://" + address.getHostAddress() + ":" + PORT;
|
||||
}
|
||||
Bitmap qrCode = QrCodeUtils.createQrCode(dm, url);
|
||||
listener.onWebServerStarted(new WebsiteConfig(url, qrCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* It is safe to call this more than once and it won't throw.
|
||||
*/
|
||||
@IoExecutor
|
||||
void stopWebServer() {
|
||||
webServer.stop();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static InetAddress getAccessPointAddress() {
|
||||
for (NetworkInterface i : getNetworkInterfaces()) {
|
||||
if (i.getName().startsWith("p2p")) {
|
||||
for (InterfaceAddress a : i.getInterfaceAddresses()) {
|
||||
// we consider only IPv4 addresses
|
||||
if (a.getAddress().getAddress().length == 4)
|
||||
return a.getAddress();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<NetworkInterface> getNetworkInterfaces() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> ifaces =
|
||||
NetworkInterface.getNetworkInterfaces();
|
||||
return ifaces == null ? emptyList() : list(ifaces);
|
||||
} catch (SocketException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class WebsiteFragment extends AbstractTabsFragment {
|
||||
|
||||
public final static String TAG = WebsiteFragment.class.getName();
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
connectedButton.setVisibility(GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Fragment getFirstFragment() {
|
||||
return ManualHotspotFragment.newInstance(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Fragment getSecondFragment() {
|
||||
return QrHotspotFragment.newInstance(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class BriarExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
// activity runs in its own process, so we can kill the old one
|
||||
startDevReportActivity(app.getApplicationContext(),
|
||||
CrashReportActivity.class, e, appStartTime, logKey);
|
||||
CrashReportActivity.class, e, appStartTime, logKey, null);
|
||||
Process.killProcess(Process.myPid());
|
||||
System.exit(10);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import static java.util.Objects.requireNonNull;
|
||||
public class CrashReportActivity extends BaseActivity
|
||||
implements BaseFragmentListener {
|
||||
|
||||
public static final String EXTRA_INITIAL_COMMENT = "initialComment";
|
||||
public static final String EXTRA_THROWABLE = "throwable";
|
||||
public static final String EXTRA_APP_START_TIME = "appStartTime";
|
||||
public static final String EXTRA_APP_LOGCAT = "logcat";
|
||||
@@ -55,10 +56,11 @@ public class CrashReportActivity extends BaseActivity
|
||||
setContentView(R.layout.activity_dev_report);
|
||||
|
||||
Intent intent = getIntent();
|
||||
String initialComment = intent.getStringExtra(EXTRA_INITIAL_COMMENT);
|
||||
Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE);
|
||||
long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, -1);
|
||||
byte[] logKey = intent.getByteArrayExtra(EXTRA_APP_LOGCAT);
|
||||
viewModel.init(t, appStartTime, logKey);
|
||||
viewModel.init(t, appStartTime, logKey, initialComment);
|
||||
viewModel.getShowReport().observeEvent(this, show -> {
|
||||
if (show) displayFragment(true);
|
||||
});
|
||||
|
||||
@@ -78,6 +78,9 @@ public class ReportFormFragment extends BaseFragment {
|
||||
list = v.findViewById(R.id.list);
|
||||
progress = v.findViewById(R.id.progress_wheel);
|
||||
|
||||
if (viewModel.getInitialComment() != null)
|
||||
userCommentView.setText(viewModel.getInitialComment());
|
||||
|
||||
if (viewModel.isFeedback()) {
|
||||
includeDebugReport
|
||||
.setText(getString(R.string.include_debug_report_feedback));
|
||||
|
||||
@@ -64,6 +64,8 @@ class ReportViewModel extends AndroidViewModel {
|
||||
private final MutableLiveEvent<Integer> closeReport =
|
||||
new MutableLiveEvent<>();
|
||||
private boolean isFeedback;
|
||||
@Nullable
|
||||
private String initialComment;
|
||||
|
||||
@Inject
|
||||
ReportViewModel(@NonNull Application application,
|
||||
@@ -80,7 +82,8 @@ class ReportViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
void init(@Nullable Throwable t, long appStartTime,
|
||||
@Nullable byte[] logKey) {
|
||||
@Nullable byte[] logKey, @Nullable String initialComment) {
|
||||
this.initialComment = initialComment;
|
||||
isFeedback = t == null;
|
||||
if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> {
|
||||
String decryptedLogs;
|
||||
@@ -103,6 +106,11 @@ class ReportViewModel extends AndroidViewModel {
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getInitialComment() {
|
||||
return initialComment;
|
||||
}
|
||||
|
||||
boolean isFeedback() {
|
||||
return isFeedback;
|
||||
}
|
||||
@@ -140,7 +148,7 @@ class ReportViewModel extends AndroidViewModel {
|
||||
|
||||
/**
|
||||
* The content of the report that will be loaded after
|
||||
* {@link #init(Throwable, long, byte[])} was called.
|
||||
* {@link #init(Throwable, long, byte[], String)} was called.
|
||||
*/
|
||||
LiveData<ReportData> getReportData() {
|
||||
return reportData;
|
||||
|
||||
@@ -38,6 +38,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
private static final String PREF_KEY_FEEDBACK = "pref_key_send_feedback";
|
||||
private static final String PREF_KEY_DEV = "pref_key_dev";
|
||||
private static final String PREF_KEY_EXPLODE = "pref_key_explode";
|
||||
private static final String PREF_KEY_SHARE_APP = "pref_key_share_app";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
@@ -84,6 +85,12 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV));
|
||||
dev.setVisible(false);
|
||||
}
|
||||
|
||||
if (!viewModel.shouldEnableShareAppViaOfflineHotspot()) {
|
||||
Preference shareApp =
|
||||
requireNonNull(findPreference(PREF_KEY_SHARE_APP));
|
||||
shareApp.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -262,4 +262,8 @@ class SettingsViewModel extends DbViewModel implements EventListener {
|
||||
return screenLockTimeout;
|
||||
}
|
||||
|
||||
boolean shouldEnableShareAppViaOfflineHotspot() {
|
||||
return featureFlags.shouldEnableShareAppViaOfflineHotspot();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.briar.android.contact.add.nearby;
|
||||
package org.briarproject.briar.android.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -22,12 +22,12 @@ import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
class QrCodeUtils {
|
||||
public class QrCodeUtils {
|
||||
|
||||
private static final Logger LOG = getLogger(QrCodeUtils.class.getName());
|
||||
|
||||
@Nullable
|
||||
static Bitmap createQrCode(DisplayMetrics dm, String input) {
|
||||
public static Bitmap createQrCode(DisplayMetrics dm, String input) {
|
||||
int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels);
|
||||
try {
|
||||
// Generate QR code
|
||||
@@ -58,7 +58,10 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
@@ -115,6 +118,7 @@ import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
||||
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_LOGCAT;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_START_TIME;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_INITIAL_COMMENT;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@@ -139,6 +143,22 @@ public class UiUtils {
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
public static void showFragment(FragmentManager fm, Fragment f,
|
||||
@Nullable String tag) {
|
||||
showFragment(fm, f, tag, true);
|
||||
}
|
||||
|
||||
public static void showFragment(FragmentManager fm, Fragment f,
|
||||
@Nullable String tag, boolean addToBackStack) {
|
||||
FragmentTransaction ta = fm.beginTransaction()
|
||||
.setCustomAnimations(R.anim.step_next_in,
|
||||
R.anim.step_previous_out, R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, f, tag);
|
||||
if (addToBackStack) ta.addToBackStack(tag);
|
||||
ta.commit();
|
||||
}
|
||||
|
||||
public static String getContactDisplayName(Author author,
|
||||
@Nullable String alias) {
|
||||
String name = author.getName();
|
||||
@@ -415,17 +435,25 @@ public class UiUtils {
|
||||
}
|
||||
|
||||
public static void triggerFeedback(Context ctx) {
|
||||
startDevReportActivity(ctx, FeedbackActivity.class, null, null, null);
|
||||
triggerFeedback(ctx, null);
|
||||
}
|
||||
|
||||
public static void triggerFeedback(Context ctx,
|
||||
@Nullable String initialComment) {
|
||||
startDevReportActivity(ctx, FeedbackActivity.class, null, null, null,
|
||||
initialComment);
|
||||
}
|
||||
|
||||
public static void startDevReportActivity(Context ctx,
|
||||
Class<? extends FragmentActivity> activity, @Nullable Throwable t,
|
||||
@Nullable Long appStartTime, @Nullable byte[] logKey) {
|
||||
@Nullable Long appStartTime, @Nullable byte[] logKey, @Nullable
|
||||
String initialComment) {
|
||||
final Intent dialogIntent = new Intent(ctx, activity);
|
||||
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
dialogIntent.putExtra(EXTRA_THROWABLE, t);
|
||||
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
|
||||
dialogIntent.putExtra(EXTRA_APP_LOGCAT, logKey);
|
||||
dialogIntent.putExtra(EXTRA_INITIAL_COMMENT, initialComment);
|
||||
ctx.startActivity(dialogIntent);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public interface AndroidNotificationManager {
|
||||
int FORUM_POST_NOTIFICATION_ID = 6;
|
||||
int BLOG_POST_NOTIFICATION_ID = 7;
|
||||
int CONTACT_ADDED_NOTIFICATION_ID = 8;
|
||||
int HOTSPOT_NOTIFICATION_ID = 9;
|
||||
|
||||
// Channel IDs
|
||||
String CONTACT_CHANNEL_ID = "contacts";
|
||||
@@ -43,9 +44,11 @@ public interface AndroidNotificationManager {
|
||||
String ONGOING_CHANNEL_ID = "zForegroundService2";
|
||||
String FAILURE_CHANNEL_ID = "zStartupFailure";
|
||||
String REMINDER_CHANNEL_ID = "zSignInReminder";
|
||||
String HOTSPOT_CHANNEL_ID = "zHotspot";
|
||||
|
||||
// Actions for pending intents
|
||||
String ACTION_DISMISS_REMINDER = "dismissReminder";
|
||||
String ACTION_STOP_HOTSPOT = "stopHotspot";
|
||||
|
||||
Notification getForegroundNotification();
|
||||
|
||||
@@ -94,4 +97,8 @@ public interface AndroidNotificationManager {
|
||||
void blockAllBlogPostNotifications();
|
||||
|
||||
void unblockAllBlogPostNotifications();
|
||||
|
||||
void showHotspotNotification();
|
||||
|
||||
void clearHotspotNotification();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:scaleX="0.92"
|
||||
android:scaleY="0.92"
|
||||
android:translateX="0.96"
|
||||
android:translateY="0.96">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,11c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,13c0,-3.31 -2.69,-6 -6,-6s-6,2.69 -6,6c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,1.48 -0.81,2.75 -2,3.45l1,1.74c1.79,-1.04 3,-2.97 3,-5.19zM12,3C6.48,3 2,7.48 2,13c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,18.53 4,15.96 4,13c0,-4.42 3.58,-8 8,-8s8,3.58 8,8c0,2.96 -1.61,5.53 -4,6.92l1,1.73c2.99,-1.73 5,-4.95 5,-8.65 0,-5.52 -4.48,-10 -10,-10z" />
|
||||
</group>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 493 B |
Binary file not shown.
|
After Width: | Height: | Size: 316 B |
Binary file not shown.
|
After Width: | Height: | Size: 621 B |
Binary file not shown.
|
After Width: | Height: | Size: 975 B |
10
briar-android/src/main/res/drawable/ic_circle_small.xml
Normal file
10
briar-android/src/main/res/drawable/ic_circle_small.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="8dp"
|
||||
android:height="8dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z" />
|
||||
</vector>
|
||||
10
briar-android/src/main/res/drawable/ic_portable_wifi_off.xml
Normal file
10
briar-android/src/main/res/drawable/ic_portable_wifi_off.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.56,14.24c0.28,-0.69 0.44,-1.45 0.44,-2.24 0,-3.31 -2.69,-6 -6,-6 -0.79,0 -1.55,0.16 -2.24,0.44l1.62,1.62c0.2,-0.03 0.41,-0.06 0.62,-0.06 2.21,0 4,1.79 4,4 0,0.21 -0.02,0.42 -0.05,0.63l1.61,1.61zM12,4c4.42,0 8,3.58 8,8 0,1.35 -0.35,2.62 -0.95,3.74l1.47,1.47C21.46,15.69 22,13.91 22,12c0,-5.52 -4.48,-10 -10,-10 -1.91,0 -3.69,0.55 -5.21,1.47l1.46,1.46C9.37,4.34 10.65,4 12,4zM3.27,2.5L2,3.77l2.1,2.1C2.79,7.57 2,9.69 2,12c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,17.53 4,14.96 4,12c0,-1.76 0.57,-3.38 1.53,-4.69l1.43,1.44C6.36,9.68 6,10.8 6,12c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-0.65 0.17,-1.25 0.44,-1.79l1.58,1.58L10,12c0,1.1 0.9,2 2,2l0.21,-0.02 0.01,0.01 7.51,7.51L21,20.23 4.27,3.5l-1,-1z" />
|
||||
</vector>
|
||||
40
briar-android/src/main/res/drawable/ic_qr_code.xml
Normal file
40
briar-android/src/main/res/drawable/ic_qr_code.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,19h2v2h-2z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13,13h2v2h-2z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,15h2v2h-2z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13,17h2v2h-2z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,19h2v2h-2z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,17h2v2h-2z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,13h2v2h-2z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,15h2v2h-2z" />
|
||||
</vector>
|
||||
12
briar-android/src/main/res/drawable/ic_settings_share.xml
Normal file
12
briar-android/src/main/res/drawable/ic_settings_share.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:attr/textColorPrimary"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
tools:ignore="NewApi">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||
</vector>
|
||||
10
briar-android/src/main/res/drawable/ic_wifi_tethering.xml
Normal file
10
briar-android/src/main/res/drawable/ic_wifi_tethering.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,11c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,13c0,-3.31 -2.69,-6 -6,-6s-6,2.69 -6,6c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,1.48 -0.81,2.75 -2,3.45l1,1.74c1.79,-1.04 3,-2.97 3,-5.19zM12,3C6.48,3 2,7.48 2,13c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,18.53 4,15.96 4,13c0,-4.42 3.58,-8 8,-8s8,3.58 8,8c0,2.96 -1.61,5.53 -4,6.92l1,1.73c2.99,-1.73 5,-4.95 5,-8.65 0,-5.52 -4.48,-10 -10,-10z" />
|
||||
</vector>
|
||||
@@ -9,11 +9,7 @@
|
||||
android:id="@+id/errorIcon"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -25,11 +21,7 @@
|
||||
android:id="@+id/errorTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/sorry"
|
||||
android:textSize="@dimen/text_size_xlarge"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -49,6 +41,6 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/errorTitle"
|
||||
tools:text="@string/qr_code_unsupported" />
|
||||
tools:text="@string/startup_failed_service_error" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
80
briar-android/src/main/res/layout/fragment_hotspot_error.xml
Normal file
80
briar-android/src/main/res/layout/fragment_hotspot_error.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/errorIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/errorMessageIntro"
|
||||
app:layout_constraintEnd_toStartOf="@id/errorMessageIntro"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/alerts_and_states_error"
|
||||
app:tint="@color/briar_red_500"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorMessageIntro"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:text="@string/hotspot_error_intro"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/errorIcon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorMessageDetail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:background="@color/briar_orange_200"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:textColor="@color/briar_text_primary"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
android:typeface="monospace"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/errorMessageIntro"
|
||||
tools:text="@string/hotspot_error_no_wifi_direct" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/feedbackButton"
|
||||
style="@style/BriarButtonFlat.Positive"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/send_feedback"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/errorMessageDetail" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fallbackFragment"
|
||||
android:name="org.briarproject.briar.android.hotspot.FallbackFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/feedbackButton"
|
||||
tools:layout="@layout/fragment_hotspot_save_apk" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
126
briar-android/src/main/res/layout/fragment_hotspot_help.xml
Normal file
126
briar-android/src/main/res/layout/fragment_hotspot_help.xml
Normal file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wifiTitleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hotspot_help_wifi_title"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wifi1View"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/hotspot_help_wifi_1"
|
||||
app:drawableLeftCompat="@drawable/ic_circle_small"
|
||||
app:drawableStartCompat="@drawable/ic_circle_small"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/wifiTitleView" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/wifi2View"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/hotspot_help_wifi_2"
|
||||
app:drawableLeftCompat="@drawable/ic_circle_small"
|
||||
app:drawableStartCompat="@drawable/ic_circle_small"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/wifi1View" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/siteTitleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/hotspot_help_site_title"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/wifi2View" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/site1View"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/hotspot_help_site_1"
|
||||
app:drawableLeftCompat="@drawable/ic_circle_small"
|
||||
app:drawableStartCompat="@drawable/ic_circle_small"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/siteTitleView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/site2View"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/hotspot_help_site_2"
|
||||
app:drawableLeftCompat="@drawable/ic_circle_small"
|
||||
app:drawableStartCompat="@drawable/ic_circle_small"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/site1View" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/site3View"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/hotspot_help_site_3"
|
||||
app:drawableLeftCompat="@drawable/ic_circle_small"
|
||||
app:drawableStartCompat="@drawable/ic_circle_small"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/site2View" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/site4View"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/hotspot_help_site_4"
|
||||
app:drawableLeftCompat="@drawable/ic_circle_small"
|
||||
app:drawableStartCompat="@drawable/ic_circle_small"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/site3View" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fallbackFragment"
|
||||
android:name="org.briarproject.briar.android.hotspot.FallbackFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/site4View"
|
||||
tools:layout="@layout/fragment_hotspot_save_apk" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
86
briar-android/src/main/res/layout/fragment_hotspot_intro.xml
Normal file
86
briar-android/src/main/res/layout/fragment_hotspot_intro.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
tools:context=".android.hotspot.HotspotIntroFragment">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="64dp"
|
||||
android:layout_marginRight="64dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/introView"
|
||||
app:layout_constraintDimensionRatio="1,1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside"
|
||||
app:srcCompat="@drawable/ic_nickname"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/introView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/hotspot_intro"
|
||||
app:layout_constraintBottom_toTopOf="@+id/startButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/startButton"
|
||||
style="@style/BriarButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/hotspot_button_start_sharing"
|
||||
app:drawableLeftCompat="@drawable/ic_wifi_tethering"
|
||||
app:drawableStartCompat="@drawable/ic_wifi_tethering"
|
||||
app:drawableTint="@color/button_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.812"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/introView"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/startButton"
|
||||
app:layout_constraintEnd_toStartOf="@+id/progressTextView"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@+id/startButton"
|
||||
app:layout_constraintTop_toTopOf="@+id/startButton"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progressTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:text="@string/hotspot_progress_text_start"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/startButton"
|
||||
app:layout_constraintEnd_toEndOf="@+id/startButton"
|
||||
app:layout_constraintStart_toEndOf="@+id/progressBar"
|
||||
app:layout_constraintTop_toTopOf="@+id/startButton"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manualIntroView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/hotspot_manual_site"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssidLabelView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/hotspot_manual_wifi_ssid"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/manualIntroView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssidView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:background="@color/briar_primary"
|
||||
android:padding="8dp"
|
||||
android:textColor="@color/briar_text_primary_inverse"
|
||||
android:typeface="monospace"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssidLabelView"
|
||||
tools:text="DIRECT-42-dfoln3lncsoij23" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordLabelView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:text="@string/enter_password"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssidView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:background="@color/briar_primary"
|
||||
android:padding="8dp"
|
||||
android:textColor="@color/briar_text_primary_inverse"
|
||||
android:typeface="monospace"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/passwordLabelView"
|
||||
tools:text="sdfsdgt2334rfw" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/altView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/hotspot_manual_site_alt"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/passwordView" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
50
briar-android/src/main/res/layout/fragment_hotspot_qr.xml
Normal file
50
briar-android/src/main/res/layout/fragment_hotspot_qr.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/qrIntroView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/hotspot_qr_wifi"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cardView"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:cardBackgroundColor="#ffffffff"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/qrIntroView"
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qrCodeView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@id/fallbackTitleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/hotspot_help_fallback_title"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fallbackIntro"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/hotspot_help_fallback_intro"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/fallbackTitleView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/fallbackButton"
|
||||
style="@style/BriarButtonFlat.Positive"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/hotspot_help_fallback_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/fallbackIntro" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/fallbackButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/fallbackButton" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
93
briar-android/src/main/res/layout/fragment_hotspot_tabs.xml
Normal file
93
briar-android/src/main/res/layout/fragment_hotspot_tabs.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/connectedButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:tabBackground="@color/briar_primary"
|
||||
app:tabGravity="fill"
|
||||
app:tabIconTint="@color/action_bar_text"
|
||||
app:tabIndicatorColor="@color/briar_lime_400"
|
||||
app:tabIndicatorHeight="4dp"
|
||||
app:tabInlineLabel="true"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabMode="fixed"
|
||||
app:tabTextColor="@color/action_bar_text">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:icon="@drawable/forum_item_create_white"
|
||||
android:text="@string/hotspot_tab_manual" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:icon="@drawable/ic_qr_code"
|
||||
android:text="@string/qr_code" />
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintBottom_toTopOf="@+id/connectedButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/connectedButton"
|
||||
style="@style/BriarButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/hotspot_button_connected"
|
||||
app:drawableLeftCompat="@drawable/ic_check_white"
|
||||
app:drawableStartCompat="@drawable/ic_check_white"
|
||||
app:drawableTint="@color/button_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/stopButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/stopButton"
|
||||
style="@style/BriarButtonFlat.Negative"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:drawablePadding="8dp"
|
||||
android:text="@string/hotspot_button_stop_sharing"
|
||||
app:drawableLeftCompat="@drawable/ic_portable_wifi_off"
|
||||
app:drawableStartCompat="@drawable/ic_portable_wifi_off"
|
||||
app:drawableTint="@color/briar_red_500"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
11
briar-android/src/main/res/menu/hotspot_help_action.xml
Normal file
11
briar-android/src/main/res/menu/hotspot_help_action.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_help"
|
||||
android:icon="@drawable/ic_help_outline_white"
|
||||
android:title="@string/help"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
||||
@@ -7,6 +7,7 @@
|
||||
<color name="briar_blue_600">#1b69b6</color>
|
||||
<color name="briar_blue_400">#418cd8</color>
|
||||
|
||||
<color name="briar_orange_200">#fed69f</color>
|
||||
<color name="briar_orange_500">#fc9403</color>
|
||||
|
||||
<color name="briar_red_500">#db3b21</color>
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
<string name="sorry">Sorry</string>
|
||||
<string name="error_start_activity">Unavailable on your system</string>
|
||||
<string name="status_heading">Status:</string>
|
||||
<string name="error">Error</string>
|
||||
|
||||
<!-- Contacts and Private Conversations-->
|
||||
<string name="no_contacts">No contacts to show</string>
|
||||
@@ -614,7 +615,8 @@
|
||||
<string name="learn_more">Learn more</string>
|
||||
<string name="disappearing_messages_summary">Make future messages in this conversation automatically disappear after 7\u00A0days.</string>
|
||||
|
||||
<!-- Settings Feedback -->
|
||||
<!-- Settings Actions -->
|
||||
<string name="pref_category_actions">Actions</string>
|
||||
<string name="send_feedback">Send feedback</string>
|
||||
|
||||
<!-- Link Warning -->
|
||||
@@ -689,6 +691,67 @@
|
||||
<!-- Connections Screen -->
|
||||
<string name="transports_help_text">Briar can connect to your contacts via the Internet, Wi-Fi or Bluetooth.\n\nAll Internet connections go through the Tor network for privacy.\n\nIf a contact can be reached by multiple methods, Briar uses them in parallel.</string>
|
||||
|
||||
<!-- Share app offline -->
|
||||
<string name="hotspot_title">Share Briar offline</string>
|
||||
<string name="hotspot_intro">Share this app with someone nearby without internet connection
|
||||
by using your phone\'s Wi-Fi.
|
||||
\n\nYour phone will open a local hotspot and provide a small website with a download of this app.</string>
|
||||
<string name="hotspot_button_start_sharing">Start sharing</string>
|
||||
<string name="hotspot_button_stop_sharing">Stop sharing</string>
|
||||
<string name="hotspot_progress_text_start">Setting up hotspot…</string>
|
||||
<string name="hotspot_notification_channel_title">Wi-Fi hotspot</string>
|
||||
<string name="hotspot_notification_title">Sharing Briar offline</string>
|
||||
<string name="hotspot_button_connected">Start app sharing</string>
|
||||
|
||||
<string name="permission_hotspot_location_request_body">To create a Wi-Fi hotspot, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string>
|
||||
<string name="permission_hotspot_location_denied_body">You have denied access to your location, but Briar needs this permission to create a Wi-Fi hotspot.\n\nPlease consider granting access.</string>
|
||||
<string name="wifi_settings_title">Wi-Fi setting</string>
|
||||
<string name="wifi_settings_request_enable_body">To create a Wi-Fi hotspot, Briar needs to use Wi-Fi. Please enable it.</string>
|
||||
<string name="wifi_settings_request_denied_body">You have denied to enable Wi-Fi, but Briar needs to use Wi-Fi.\n\nPlease consider enabling it.</string>
|
||||
|
||||
<string name="hotspot_error_intro">Something went wrong while trying to share the app via Wi-Fi:</string>
|
||||
<string name="hotspot_error_no_wifi_direct">Device does not support Wi-Fi Direct</string>
|
||||
<string name="hotspot_error_start_callback_failed">Hotspot failed to start: error %s</string>
|
||||
<string name="hotspot_error_start_callback_failed_unknown">Hotspot failed to start with an unknown error, reason %d</string>
|
||||
<string name="hotspot_error_start_callback_no_group_info">Hotspot failed to start: no group info</string>
|
||||
<string name="hotspot_error_web_server_start">Error starting web server!</string>
|
||||
<string name="hotspot_error_web_server_serve">Error presenting website.\n\nPlease send feedback (with anonymous data) via the Briar app if the issue persists.</string>
|
||||
<string name="hotspot_flag_test">Warning: This app was installed with Android Studio and can NOT be installed on another device.</string>
|
||||
|
||||
<string name="hotspot_tab_manual">Manual</string>
|
||||
<string name="hotspot_manual_wifi">To download the app on another phone, please connect to this Wi-Fi network:</string>
|
||||
<string name="hotspot_manual_wifi_ssid">Network name (SSID)</string>
|
||||
<string name="hotspot_manual_wifi_alt">Instead of adding the network manually, you can also scan a QR code.</string>
|
||||
<string name="hotspot_peer_connected">Successfully connected</string>
|
||||
<string name="hotspot_peer_connected_action">Show download info</string>
|
||||
<string name="hotspot_manual_site">After you are connected to the Wi-Fi, carefully enter this address in your browser.</string>
|
||||
<string name="hotspot_manual_site_address">Address (URL)</string>
|
||||
<string name="hotspot_manual_site_alt">Instead of typing the address manually, you can also scan a QR code.</string>
|
||||
<string name="hotspot_qr_wifi">To download the app on another phone, please scan this QR code to connect to this Wi-Fi network:</string>
|
||||
<string name="hotspot_qr_site">After you are connected to the Wi-Fi, scan this QR code to download the app.</string>
|
||||
|
||||
<!-- e.g. Download Briar 1.2.20 -->
|
||||
<string name="website_download_title">Download %s</string>
|
||||
<string name="website_download_intro">Someone nearby shared %s with you.</string>
|
||||
<string name="website_download_outro">After the download is complete, open the downloaded file and install it.</string>
|
||||
<string name="website_troubleshooting_title">Troubleshooting</string>
|
||||
<string name="website_troubleshooting_1">If you cannot download the app, try it with a different web browser app.</string>
|
||||
<string name="website_troubleshooting_2_old">To install the downloaded app, you might need to allow installation of apps from \"Unknown sources\" in system settings. Afterwards, you may need to download the app again. We recommend to undo that after successful installation.</string>
|
||||
<string name="website_troubleshooting_2_new">To install the downloaded app, you might need to allow your browser to install unknown apps. We recommend to undo that after successful installation.</string>
|
||||
|
||||
<string name="hotspot_help_wifi_title">Problems with connecting to Wi-Fi:</string>
|
||||
<string name="hotspot_help_wifi_1">Try disabling and re-enabling Wi-Fi on both phones and try again.</string>
|
||||
<string name="hotspot_help_wifi_2">If your phone complains that the Wi-Fi has no internet, tell it that you want to stay connected anyway.</string>
|
||||
<string name="hotspot_help_site_title">Problems visiting the local website:</string>
|
||||
<string name="hotspot_help_site_1">Double check that you entered the address exactly as shown. A small error can make it fail.</string>
|
||||
<string name="hotspot_help_site_2">Ensure that your phone is still connected to the correct Wi-Fi (see above) when you try to access the site.</string>
|
||||
<string name="hotspot_help_site_3">Check that you don\'t have any active firewall apps that may block the access.</string>
|
||||
<string name="hotspot_help_site_4">If you can visit the site, but not download the Briar app, try it with a different web browser app.</string>
|
||||
<string name="hotspot_help_fallback_title">Nothing works?</string>
|
||||
<string name="hotspot_help_fallback_intro">You can try to save the app as an .apk file to share in some other way. Once on the other device, it can be used to install Briar.
|
||||
\n\nTip: For sharing via Bluetooth, you might need to rename the file to end with .zip first.</string>
|
||||
<string name="hotspot_help_fallback_button">Save app install file</string>
|
||||
|
||||
<!-- Screenshots -->
|
||||
|
||||
<!-- This is a name to be used in screenshots. Feel free to change it to a local name. -->
|
||||
|
||||
@@ -24,10 +24,24 @@
|
||||
app:fragment="org.briarproject.briar.android.settings.NotificationsFragment"
|
||||
app:icon="@drawable/ic_notifications" />
|
||||
|
||||
<Preference
|
||||
android:key="pref_key_send_feedback"
|
||||
android:title="@string/send_feedback"
|
||||
app:icon="@drawable/ic_feedback" />
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_actions"
|
||||
android:layout="@layout/preferences_category"
|
||||
android:title="@string/pref_category_actions"
|
||||
app:allowDividerAbove="true">
|
||||
<Preference
|
||||
android:key="pref_key_share_app"
|
||||
android:title="@string/hotspot_title"
|
||||
app:icon="@drawable/ic_settings_share">
|
||||
<intent
|
||||
android:targetClass="org.briarproject.briar.android.hotspot.HotspotActivity"
|
||||
android:targetPackage="@string/app_package" />
|
||||
</Preference>
|
||||
<Preference
|
||||
android:key="pref_key_send_feedback"
|
||||
android:title="@string/send_feedback"
|
||||
app:icon="@drawable/ic_feedback" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_dev"
|
||||
|
||||
@@ -225,6 +225,7 @@ dependencyVerification {
|
||||
'org.jmock:jmock:2.12.0:jmock-2.12.0.jar:266d07314c0cd343c46ff8a55601272de8cf406807caf55e6f313295f83d10be',
|
||||
'org.jvnet.staxex:stax-ex:1.8:stax-ex-1.8.jar:95b05d9590af4154c6513b9c5dc1fb2e55b539972ba0a9ef28e9a0c01d83ad77',
|
||||
'org.mockito:mockito-core:3.9.0:mockito-core-3.9.0.jar:a1f64211407b8dc4cf80b16e07cc11aa9e5228d53dc4a5357326d66825f6a4ac',
|
||||
'org.nanohttpd:nanohttpd:2.3.1:nanohttpd-2.3.1.jar:de864c47818157141a24c9acb36df0c47d7bf15b7ff48c90610f3eb4e5df0e58',
|
||||
'org.objenesis:objenesis:3.2:objenesis-3.2.jar:03d960bd5aef03c653eb000413ada15eb77cdd2b8e4448886edf5692805e35f3',
|
||||
'org.ow2.asm:asm-analysis:7.0:asm-analysis-7.0.jar:e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474',
|
||||
'org.ow2.asm:asm-commons:7.0:asm-commons-7.0.jar:fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d',
|
||||
|
||||
@@ -95,5 +95,6 @@ internal class HeadlessModule(private val appDir: File) {
|
||||
override fun shouldEnableProfilePictures() = false
|
||||
override fun shouldEnableDisappearingMessages() = false
|
||||
override fun shouldEnableConnectViaBluetooth() = false
|
||||
override fun shouldEnableShareAppViaOfflineHotspot() = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ internal class HeadlessTestModule(private val appDir: File) {
|
||||
override fun shouldEnableProfilePictures() = false
|
||||
override fun shouldEnableDisappearingMessages() = false
|
||||
override fun shouldEnableConnectViaBluetooth() = false
|
||||
override fun shouldEnableShareAppViaOfflineHotspot() = false
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
Reference in New Issue
Block a user