mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Merge branch '1081-share-app-via-wifi-hotspot' into 'master'
Share app via Wi-Fi hotspot Closes #1081 See merge request briar/briar!1515
This commit is contained in:
@@ -14,4 +14,6 @@ public interface FeatureFlags {
|
||||
boolean shouldEnableConnectViaBluetooth();
|
||||
|
||||
boolean shouldEnableTransferData();
|
||||
|
||||
boolean shouldEnableShareAppViaOfflineHotspot();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ public class TestFeatureFlagModule {
|
||||
public boolean shouldEnableTransferData() {
|
||||
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" />
|
||||
@@ -456,6 +457,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">To install the downloaded app,
|
||||
you might need to allow your browser to install unknown apps.
|
||||
We recommend to undo that after successful installation.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,6 +36,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.removabledrive.ChooserFragment;
|
||||
@@ -216,6 +221,16 @@ public interface AndroidComponent
|
||||
|
||||
void inject(NotificationsFragment notificationsFragment);
|
||||
|
||||
void inject(HotspotIntroFragment hotspotIntroFragment);
|
||||
|
||||
void inject(AbstractTabsFragment abstractTabsFragment);
|
||||
|
||||
void inject(QrHotspotFragment qrHotspotFragment);
|
||||
|
||||
void inject(ManualHotspotFragment manualHotspotFragment);
|
||||
|
||||
void inject(FallbackFragment fallbackFragment);
|
||||
|
||||
void inject(ChooserFragment chooserFragment);
|
||||
|
||||
void inject(SendFragment sendFragment);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,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;
|
||||
@@ -94,6 +95,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
GroupListModule.class,
|
||||
GroupConversationModule.class,
|
||||
SharingModule.class,
|
||||
HotspotModule.class,
|
||||
TransferDataModule.class,
|
||||
})
|
||||
public class AppModule {
|
||||
@@ -318,6 +320,11 @@ public class AppModule {
|
||||
public boolean shouldEnableTransferData() {
|
||||
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;
|
||||
@@ -177,6 +178,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(CrashReportActivity crashReportActivity);
|
||||
|
||||
void inject(HotspotActivity hotspotActivity);
|
||||
|
||||
void inject(RemovableDriveActivity activity);
|
||||
|
||||
// Fragments
|
||||
|
||||
@@ -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,84 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.wifi.WifiManager;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
|
||||
/**
|
||||
* Abstract base class for the ConditionManagers that ensure that the conditions
|
||||
* to open a hotspot are fulfilled. There are different extensions of this for
|
||||
* API levels lower than 29 and 29+.
|
||||
*/
|
||||
abstract class AbstractConditionManager {
|
||||
|
||||
enum Permission {
|
||||
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
|
||||
}
|
||||
|
||||
protected final Consumer<Boolean> permissionUpdateCallback;
|
||||
protected FragmentActivity ctx;
|
||||
WifiManager wifiManager;
|
||||
|
||||
AbstractConditionManager(Consumer<Boolean> permissionUpdateCallback) {
|
||||
this.permissionUpdateCallback = permissionUpdateCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a FragmentActivity context here during `onCreateView()`.
|
||||
*/
|
||||
void init(FragmentActivity ctx) {
|
||||
this.ctx = ctx;
|
||||
this.wifiManager = (WifiManager) ctx.getApplicationContext()
|
||||
.getSystemService(WIFI_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this during onStart() in the fragment where the ConditionManager
|
||||
* is used.
|
||||
*/
|
||||
abstract void onStart();
|
||||
|
||||
/**
|
||||
* Check if all required conditions are met such that the hotspot can be
|
||||
* started. If any precondition is not met yet, bring up relevant dialogs
|
||||
* asking the user to grant relevant permissions or take relevant actions.
|
||||
*
|
||||
* @return true if conditions are fulfilled and flow can continue.
|
||||
*/
|
||||
abstract boolean checkAndRequestConditions();
|
||||
|
||||
void showDenialDialog(FragmentActivity ctx,
|
||||
@StringRes int title, @StringRes int body,
|
||||
DialogInterface.OnClickListener onOkClicked, Runnable onDismiss) {
|
||||
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.setOnDismissListener(dialog -> onDismiss.run());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
void showRationale(Context ctx, @StringRes int title,
|
||||
@StringRes int body, Runnable onContinueClicked,
|
||||
Runnable onDismiss) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setNeutralButton(R.string.continue_button,
|
||||
(dialog, which) -> onContinueClicked.run());
|
||||
builder.setOnDismissListener(dialog -> onDismiss.run());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,83 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.activity.result.ActivityResultCaller;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
/**
|
||||
* This class ensures that the conditions to open a hotspot are fulfilled on
|
||||
* API levels < 29.
|
||||
* <p>
|
||||
* As soon as {@link #checkAndRequestConditions()} returns true,
|
||||
* all conditions are fulfilled.
|
||||
*/
|
||||
class ConditionManager extends AbstractConditionManager {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ConditionManager.class.getName());
|
||||
|
||||
private final ActivityResultLauncher<Intent> wifiRequest;
|
||||
|
||||
ConditionManager(ActivityResultCaller arc,
|
||||
Consumer<Boolean> permissionUpdateCallback) {
|
||||
super(permissionUpdateCallback);
|
||||
wifiRequest = arc.registerForActivityResult(
|
||||
new StartActivityForResult(),
|
||||
result -> permissionUpdateCallback
|
||||
.accept(wifiManager.isWifiEnabled()));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onStart() {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
private boolean areEssentialPermissionsGranted() {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(String.format("areEssentialPermissionsGranted(): " +
|
||||
"wifiManager.isWifiEnabled()? %b",
|
||||
wifiManager.isWifiEnabled()));
|
||||
}
|
||||
return wifiManager.isWifiEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean checkAndRequestConditions() {
|
||||
if (areEssentialPermissionsGranted()) return true;
|
||||
|
||||
if (!wifiManager.isWifiEnabled()) {
|
||||
// Try enabling the Wifi and return true if that seems to have been
|
||||
// successful, i.e. "Wifi is either already in the requested state, or
|
||||
// in progress toward the requested state".
|
||||
if (wifiManager.setWifiEnabled(true)) {
|
||||
LOG.info("Enabled wifi");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wifi is not enabled and we can't seem to enable it, so ask the user
|
||||
// to enable it for us.
|
||||
showRationale(ctx, R.string.wifi_settings_title,
|
||||
R.string.wifi_settings_request_enable_body,
|
||||
this::requestEnableWiFi,
|
||||
() -> permissionUpdateCallback.accept(false));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void requestEnableWiFi() {
|
||||
wifiRequest.launch(new Intent(Settings.ACTION_WIFI_SETTINGS));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.activity.result.ActivityResultCaller;
|
||||
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.annotation.RequiresApi;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
|
||||
|
||||
/**
|
||||
* This class ensures that the conditions to open a hotspot are fulfilled on
|
||||
* API levels >= 29.
|
||||
* <p>
|
||||
* As soon as {@link #checkAndRequestConditions()} returns true,
|
||||
* all conditions are fulfilled.
|
||||
*/
|
||||
@RequiresApi(29)
|
||||
class ConditionManager29 extends AbstractConditionManager {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ConditionManager29.class.getName());
|
||||
|
||||
private Permission locationPermission = Permission.UNKNOWN;
|
||||
|
||||
private final ActivityResultLauncher<String> locationRequest;
|
||||
private final ActivityResultLauncher<Intent> wifiRequest;
|
||||
|
||||
ConditionManager29(ActivityResultCaller arc,
|
||||
Consumer<Boolean> permissionUpdateCallback) {
|
||||
super(permissionUpdateCallback);
|
||||
locationRequest = arc.registerForActivityResult(
|
||||
new RequestPermission(), granted -> {
|
||||
onRequestPermissionResult(granted);
|
||||
permissionUpdateCallback.accept(TRUE.equals(granted));
|
||||
});
|
||||
wifiRequest = arc.registerForActivityResult(
|
||||
new StartActivityForResult(),
|
||||
result -> permissionUpdateCallback
|
||||
.accept(wifiManager.isWifiEnabled())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onStart() {
|
||||
locationPermission = Permission.UNKNOWN;
|
||||
}
|
||||
|
||||
private boolean areEssentialPermissionsGranted() {
|
||||
boolean isWifiEnabled = wifiManager.isWifiEnabled();
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(String.format("areEssentialPermissionsGranted(): " +
|
||||
"locationPermission? %s, " +
|
||||
"wifiManager.isWifiEnabled()? %b",
|
||||
locationPermission, isWifiEnabled));
|
||||
}
|
||||
return locationPermission == Permission.GRANTED && isWifiEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean checkAndRequestConditions() {
|
||||
if (areEssentialPermissionsGranted()) return true;
|
||||
|
||||
if (locationPermission == Permission.UNKNOWN) {
|
||||
locationRequest.launch(ACCESS_FINE_LOCATION);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the location permission has been permanently denied, ask the
|
||||
// user to change the setting
|
||||
if (locationPermission == Permission.PERMANENTLY_DENIED) {
|
||||
showDenialDialog(ctx, R.string.permission_location_title,
|
||||
R.string.permission_hotspot_location_denied_body,
|
||||
getGoToSettingsListener(ctx),
|
||||
() -> permissionUpdateCallback.accept(false));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should we show the rationale for location permission?
|
||||
if (locationPermission == Permission.SHOW_RATIONALE) {
|
||||
showRationale(ctx, R.string.permission_location_title,
|
||||
R.string.permission_hotspot_location_request_body,
|
||||
this::requestPermissions,
|
||||
() -> permissionUpdateCallback.accept(false));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Wifi is not enabled, we show the rationale for enabling Wifi?
|
||||
if (!wifiManager.isWifiEnabled()) {
|
||||
showRationale(ctx, R.string.wifi_settings_title,
|
||||
R.string.wifi_settings_request_enable_body,
|
||||
this::requestEnableWiFi,
|
||||
() -> permissionUpdateCallback.accept(false));
|
||||
return false;
|
||||
}
|
||||
|
||||
// we shouldn't usually reach this point, but if we do, return false
|
||||
// anyway to force a recheck. Maybe some condition changed in the
|
||||
// meantime.
|
||||
return false;
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
|
||||
private void requestPermissions() {
|
||||
locationRequest.launch(ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
private void requestEnableWiFi() {
|
||||
wifiRequest.launch(new Intent(Settings.Panel.ACTION_WIFI));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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 org.briarproject.briar.android.util.ActivityLaunchers.CreateDocumentAdvanced;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.content.Intent.ACTION_SEND;
|
||||
import static android.content.Intent.EXTRA_STREAM;
|
||||
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static androidx.transition.TransitionManager.beginDelayedTransition;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
import static org.briarproject.briar.android.hotspot.HotspotViewModel.getApkFileName;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
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 CreateDocumentAdvanced(),
|
||||
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_fallback, 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, this::shareUri);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void shareUri(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 = 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);
|
||||
}
|
||||
}
|
||||
startActivity(Intent.createChooser(i, null));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
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.wasNotYetConsumed()) {
|
||||
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,52 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
Toast.makeText(requireContext(), R.string.hotspot_peer_connected,
|
||||
Toast.LENGTH_LONG).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,127 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
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.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.os.Build.VERSION.SDK_INT;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static androidx.transition.TransitionManager.beginDelayedTransition;
|
||||
import static 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 Button startButton;
|
||||
private ProgressBar progressBar;
|
||||
private TextView progressTextView;
|
||||
|
||||
private final AbstractConditionManager conditionManager = SDK_INT < 29 ?
|
||||
new ConditionManager(this, this::onPermissionUpdate) :
|
||||
new ConditionManager29(this, this::onPermissionUpdate);
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
FragmentActivity activity = requireActivity();
|
||||
getAndroidComponent(activity).inject(this);
|
||||
viewModel = new ViewModelProvider(activity, viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@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(this::onButtonClick);
|
||||
|
||||
conditionManager.init(requireActivity());
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
conditionManager.onStart();
|
||||
}
|
||||
|
||||
private void onButtonClick(View view) {
|
||||
startButton.setEnabled(false);
|
||||
startHotspotIfConditionsFulfilled();
|
||||
}
|
||||
|
||||
private void startHotspotIfConditionsFulfilled() {
|
||||
if (conditionManager.checkAndRequestConditions()) {
|
||||
showInstallWarningIfNeeded();
|
||||
beginDelayedTransition((ViewGroup) requireView());
|
||||
startButton.setVisibility(INVISIBLE);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
progressTextView.setVisibility(VISIBLE);
|
||||
viewModel.startHotspot();
|
||||
}
|
||||
}
|
||||
|
||||
private void onPermissionUpdate(boolean recheckPermissions) {
|
||||
startButton.setEnabled(true);
|
||||
if (recheckPermissions) {
|
||||
startHotspotIfConditionsFulfilled();
|
||||
}
|
||||
}
|
||||
|
||||
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,453 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
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.os.PowerManager;
|
||||
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.POWER_SERVICE;
|
||||
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 android.os.PowerManager.FULL_WAKE_LOCK;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
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.briar.android.util.UiUtils.handleException;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class HotspotManager {
|
||||
|
||||
interface HotspotListener {
|
||||
@UiThread
|
||||
void onStartingHotspot();
|
||||
|
||||
@IoExecutor
|
||||
void onHotspotStarted(NetworkConfig networkConfig);
|
||||
|
||||
@UiThread
|
||||
void onDeviceConnected();
|
||||
|
||||
@UiThread
|
||||
void onHotspotError(String error);
|
||||
}
|
||||
|
||||
private static final Logger LOG = getLogger(HotspotManager.class.getName());
|
||||
|
||||
private static final int MAX_FRAMEWORK_ATTEMPTS = 5;
|
||||
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 PowerManager powerManager;
|
||||
private final Handler handler;
|
||||
private final String lockTag;
|
||||
|
||||
private HotspotListener listener;
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private WifiP2pManager.Channel channel;
|
||||
@Nullable
|
||||
@RequiresApi(29)
|
||||
private volatile NetworkConfig savedNetworkConfig = null;
|
||||
|
||||
@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);
|
||||
powerManager = (PowerManager) ctx.getSystemService(POWER_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();
|
||||
acquireLocks();
|
||||
startWifiP2pFramework(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* As soon as Wifi is enabled, we try starting the WifiP2p framework.
|
||||
* If Wifi has just been enabled, it is possible that will fail. If that
|
||||
* happens we try again for MAX_FRAMEWORK_ATTEMPTS times after a delay of
|
||||
* RETRY_DELAY_MILLIS after each attempt.
|
||||
* <p>
|
||||
* Rationale: it can take a few milliseconds for WifiP2p to become available
|
||||
* after enabling Wifi. Depending on the API level it is possible to check this
|
||||
* using {@link WifiP2pManager#requestP2pState} or register a BroadcastReceiver
|
||||
* on the WIFI_P2P_STATE_CHANGED_ACTION to get notified when WifiP2p is really
|
||||
* available. Trying to implement a solution that works reliably using these
|
||||
* checks turned out to be a long rabbit-hole with lots of corner cases and
|
||||
* workarounds for specific situations.
|
||||
* Instead we now rely on this trial-and-error approach of just starting
|
||||
* the framework and retrying if it fails.
|
||||
* <p>
|
||||
* We'll realize that the framework is busy when the ActionListener passed
|
||||
* to {@link WifiP2pManager#createGroup} is called with onFailure(BUSY)
|
||||
*/
|
||||
@UiThread
|
||||
private void startWifiP2pFramework(int attempt) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("startWifiP2pFramework attempt: " + attempt);
|
||||
}
|
||||
/*
|
||||
* It is important that we call WifiP2pManager#initialize again
|
||||
* for every attempt to starting the framework because otherwise,
|
||||
* createGroup() will continue to fail with a BUSY state.
|
||||
*/
|
||||
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
|
||||
if (channel == null) {
|
||||
releaseHotspotWithError(
|
||||
ctx.getString(R.string.hotspot_error_no_wifi_direct));
|
||||
return;
|
||||
}
|
||||
|
||||
ActionListener listener = new ActionListener() {
|
||||
@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 (LOG.isLoggable(INFO)) {
|
||||
LOG.info("onFailure: " + reason);
|
||||
}
|
||||
if (reason == BUSY) {
|
||||
// WifiP2p not ready yet or hotspot already running
|
||||
restartWifiP2pFramework(attempt);
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (SDK_INT >= 29) {
|
||||
Runnable createGroup = () -> {
|
||||
NetworkConfig c = requireNonNull(savedNetworkConfig);
|
||||
WifiP2pConfig config = new WifiP2pConfig.Builder()
|
||||
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
|
||||
.setNetworkName(c.ssid)
|
||||
.setPassphrase(c.password)
|
||||
.build();
|
||||
wifiP2pManager.createGroup(channel, config, listener);
|
||||
};
|
||||
if (savedNetworkConfig == null) {
|
||||
// load savedNetworkConfig before starting hotspot
|
||||
dbExecutor.execute(() -> {
|
||||
loadSavedNetworkConfig();
|
||||
androidExecutor.runOnUiThread(createGroup);
|
||||
});
|
||||
} else {
|
||||
// savedNetworkConfig was already loaded, create group now
|
||||
createGroup.run();
|
||||
}
|
||||
} else {
|
||||
wifiP2pManager.createGroup(channel, listener);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
// this should never happen, because we request permissions before
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void restartWifiP2pFramework(int attempt) {
|
||||
LOG.info("retrying to start WifiP2p framework");
|
||||
if (attempt < MAX_FRAMEWORK_ATTEMPTS) {
|
||||
if (SDK_INT >= 27 && channel != null) channel.close();
|
||||
channel = null;
|
||||
handler.postDelayed(() -> startWifiP2pFramework(attempt + 1),
|
||||
RETRY_DELAY_MILLIS);
|
||||
} else {
|
||||
releaseHotspotWithError(
|
||||
ctx.getString(R.string.hotspot_error_framework_busy));
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void stopWifiP2pHotspot() {
|
||||
if (channel == null) return;
|
||||
wifiP2pManager.removeGroup(channel, new ActionListener() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
closeChannelAndReleaseLocks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(int reason) {
|
||||
// not propagating back error
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Error removing Wifi P2P group: " + reason);
|
||||
}
|
||||
closeChannelAndReleaseLocks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("WakelockTimeout")
|
||||
private void acquireLocks() {
|
||||
// FLAG_KEEP_SCREEN_ON is not respected on some Huawei devices.
|
||||
wakeLock = powerManager.newWakeLock(FULL_WAKE_LOCK, lockTag);
|
||||
wakeLock.acquire();
|
||||
// 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();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void releaseHotspotWithError(String error) {
|
||||
listener.onHotspotError(error);
|
||||
closeChannelAndReleaseLocks();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void closeChannelAndReleaseLocks() {
|
||||
if (SDK_INT >= 27 && channel != null) channel.close();
|
||||
channel = null;
|
||||
if (wakeLock.isHeld()) wakeLock.release();
|
||||
if (wifiLock.isHeld()) wifiLock.release();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
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-")) {
|
||||
LOG.info("received networkName without prefix 'DIRECT-'");
|
||||
return false;
|
||||
} else if (SDK_INT >= 29) {
|
||||
// if we get here, the savedNetworkConfig must have a value
|
||||
String networkName = requireNonNull(savedNetworkConfig).ssid;
|
||||
if (!networkName.equals(group.getNetworkName())) {
|
||||
LOG.info("expected networkName does not match received one");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void retryRequestingGroupInfo(int attempt) {
|
||||
LOG.info("retrying to request group info");
|
||||
// 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() {
|
||||
LOG.info("requestGroupInfo for connection");
|
||||
GroupInfoListener groupListener = group -> {
|
||||
if (group == null || group.getClientList().isEmpty()) {
|
||||
handler.postDelayed(this::requestGroupInfoForConnection,
|
||||
RETRY_DELAY_MILLIS);
|
||||
} else {
|
||||
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 + ";;";
|
||||
}
|
||||
|
||||
// exclude chars that are easy to confuse: 0 O, 5 S, 1 l I
|
||||
private static final String chars =
|
||||
"2346789ABCDEFGHJKLMNPQRTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
private String getRandomString(int length) {
|
||||
char[] c = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
c[i] = chars.charAt(random.nextInt(chars.length()));
|
||||
}
|
||||
return new String(c);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,88 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
boolean wasNotYetConsumed() {
|
||||
return !consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
void consume() {
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
static class HotspotError extends HotspotState {
|
||||
private final String error;
|
||||
|
||||
HotspotError(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
String getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
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 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)));
|
||||
stopHotspot();
|
||||
}
|
||||
|
||||
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,119 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
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 androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
|
||||
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);
|
||||
|
||||
Consumer<HotspotStarted> consumer;
|
||||
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
|
||||
linkify(manualIntroView, 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);
|
||||
};
|
||||
} else {
|
||||
linkify(manualIntroView, R.string.hotspot_manual_site);
|
||||
ssidLabelView.setText(R.string.hotspot_manual_site_address);
|
||||
consumer = state -> ssidView.setText(state.getWebsiteConfig().url);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void linkify(TextView textView, int resPattern) {
|
||||
String pattern = getString(resPattern);
|
||||
String replacement = getString(R.string.hotspot_scanning_a_qr_code);
|
||||
String text = String.format(pattern, replacement);
|
||||
int start = pattern.indexOf("%s");
|
||||
int end = start + replacement.length();
|
||||
SpannableString spannable = new SpannableString(text);
|
||||
ClickableSpan clickable = new ClickableSpan() {
|
||||
|
||||
@Override
|
||||
public void onClick(View textView) {
|
||||
ViewPager2 pager = requireActivity().findViewById(R.id.pager);
|
||||
pager.setCurrentItem(1);
|
||||
}
|
||||
|
||||
};
|
||||
spannable.setSpan(clickable, start, end, SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
textView.setText(spannable);
|
||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
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 android.widget.Toast;
|
||||
|
||||
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.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);
|
||||
|
||||
boolean forWifi = requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT);
|
||||
|
||||
qrIntroView.setText(forWifi ? R.string.hotspot_qr_wifi :
|
||||
R.string.hotspot_qr_site);
|
||||
|
||||
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
|
||||
if (state instanceof HotspotStarted) {
|
||||
HotspotStarted s = (HotspotStarted) state;
|
||||
Bitmap qrCode = forWifi ? s.getNetworkConfig().qrCode :
|
||||
s.getWebsiteConfig().qrCode;
|
||||
if (qrCode == null) {
|
||||
Toast.makeText(requireContext(), R.string.error,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
qrCodeView.setImageResource(R.drawable.ic_image_broken);
|
||||
} else {
|
||||
qrCodeView.setImageBitmap(qrCode);
|
||||
}
|
||||
}
|
||||
});
|
||||
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,127 @@
|
||||
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 androidx.annotation.UiThread;
|
||||
|
||||
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 volatile WebServerListener listener;
|
||||
|
||||
@Inject
|
||||
WebServerManager(Application ctx) {
|
||||
webServer = new WebServer(ctx);
|
||||
dm = ctx.getResources().getDisplayMetrics();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,7 +38,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;
|
||||
|
||||
@@ -36,6 +36,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;
|
||||
@@ -85,6 +86,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
|
||||
@@ -61,6 +61,7 @@ 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;
|
||||
@@ -111,6 +112,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
|
||||
@@ -137,13 +139,18 @@ public class UiUtils {
|
||||
|
||||
public static void showFragment(FragmentManager fm, Fragment f,
|
||||
@Nullable String tag) {
|
||||
fm.beginTransaction()
|
||||
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)
|
||||
.addToBackStack(tag)
|
||||
.commit();
|
||||
.replace(R.id.fragmentContainer, f, tag);
|
||||
if (addToBackStack) ta.addToBackStack(tag);
|
||||
ta.commit();
|
||||
}
|
||||
|
||||
public static String getContactDisplayName(Author author,
|
||||
@@ -410,17 +417,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);
|
||||
}
|
||||
|
||||
@@ -546,4 +561,5 @@ public class UiUtils {
|
||||
activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE |
|
||||
SOFT_INPUT_STATE_HIDDEN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -42,12 +43,15 @@ public interface AndroidNotificationManager {
|
||||
String ONGOING_CHANNEL_OLD_ID = "zForegroundService";
|
||||
String ONGOING_CHANNEL_ID = "zForegroundService2";
|
||||
String REMINDER_CHANNEL_ID = "zSignInReminder";
|
||||
String HOTSPOT_CHANNEL_ID = "zHotspot";
|
||||
|
||||
// This channel is no longer used - keep the ID so we can remove the
|
||||
// channel from existing installations
|
||||
String FAILURE_CHANNEL_ID = "zStartupFailure";
|
||||
|
||||
// Actions for pending intents
|
||||
String ACTION_DISMISS_REMINDER = "dismissReminder";
|
||||
String ACTION_STOP_HOTSPOT = "stopHotspot";
|
||||
|
||||
Notification getForegroundNotification();
|
||||
|
||||
@@ -96,4 +100,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_fallback" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -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="16dp"
|
||||
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>
|
||||
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_fallback" />
|
||||
|
||||
</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_wifi_tethering"
|
||||
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,85 @@
|
||||
<?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" />
|
||||
|
||||
</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>
|
||||
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>
|
||||
@@ -615,7 +616,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 -->
|
||||
@@ -690,6 +692,73 @@
|
||||
<!-- 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 start a Wi-Fi hotspot. People nearby can connect to the hotspot and download the Briar app from your phone.</string>
|
||||
<string name="hotspot_button_start_sharing">Start hotspot</string>
|
||||
<string name="hotspot_button_stop_sharing">Stop hotspot</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">Next</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 permission to enable Wi-Fi, but Briar needs to use Wi-Fi.\n\nPlease consider enabling it.</string>
|
||||
|
||||
<string name="hotspot_tab_manual">Manual</string>
|
||||
<!-- The placeholder to be inserted into the string 'hotspot_manual_wifi': People can connect by %s -->
|
||||
<string name="hotspot_scanning_a_qr_code">scanning a QR code</string>
|
||||
|
||||
<!-- Wi-Fi setup -->
|
||||
<!-- The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code' -->
|
||||
<string name="hotspot_manual_wifi">Your phone is providing a Wi-Fi hotspot. People who want to download Briar can connect to the hotspot by entering the details below or %s. When they have connected to the hotspot, press \'Next\'.</string>
|
||||
<string name="hotspot_manual_wifi_ssid">Network name (SSID)</string>
|
||||
<string name="hotspot_qr_wifi">Your phone is providing a Wi-Fi hotspot. People who want to download Briar can connect to the hotspot by scanning this QR code. When they have connected to the hotspot, press \'Next\'.</string>
|
||||
<string name="hotspot_peer_connected">Successfully connected</string>
|
||||
|
||||
<!-- Download link -->
|
||||
<!-- The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code' -->
|
||||
<string name="hotspot_manual_site">Your phone is providing a Wi-Fi hotspot. People who are connected to the hotspot can download Briar by typing the following link in a web browser or %s.</string>
|
||||
<string name="hotspot_manual_site_address">Address (URL)</string>
|
||||
<string name="hotspot_qr_site">Your phone is providing a Wi-Fi hotspot. People who are connected to the hotspot can download Briar by scanning this QR code.</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 disabling the \"Unknown sources\" setting after installing the app.</string>
|
||||
<string name="website_troubleshooting_2_new">To install the downloaded app, you might need to allow your browser to install unknown apps. After installing the app, we recommend removing the browser\'s permission to install unknown apps.</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">If you have a firewall app, check that it isn\'t blocking 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 the file has been transferred to 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</string>
|
||||
|
||||
<!-- error handling -->
|
||||
<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_error_framework_busy">Unable to start the hotspot.\n\nIf you have another hotspot running or are sharing your Internet connection via Wi-Fi, try stopping that and try again afterwards.</string>
|
||||
|
||||
|
||||
<!-- Transfer Data via Removable Drives -->
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -97,5 +97,6 @@ internal class HeadlessModule(private val appDir: File) {
|
||||
override fun shouldEnableDisappearingMessages() = false
|
||||
override fun shouldEnableConnectViaBluetooth() = false
|
||||
override fun shouldEnableTransferData() = false
|
||||
override fun shouldEnableShareAppViaOfflineHotspot() = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user