Merge pull request #1327 from kiwix/iadeelzafar/wifi-hotspot

Wifi Hotspot Feature
This commit is contained in:
Kelson 2019-08-31 19:43:49 +02:00 committed by GitHub
commit e7c3d5dcdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1424 additions and 21 deletions

View File

@ -130,6 +130,7 @@ dependencies {
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "io.objectbox:objectbox-kotlin:$objectboxVersion"
implementation "io.objectbox:objectbox-rxjava:$objectboxVersion"
implementation 'com.google.android.gms:play-services-location:17.0.0'
testImplementation "org.junit.jupiter:junit-jupiter:5.4.2"
testImplementation "io.mockk:mockk:1.9"
@ -248,9 +249,9 @@ android {
'LogConditional'
warning 'UnknownNullness',
'SelectableText',
'IconDensities',
'SyntheticAccessor'
'SelectableText',
'IconDensities',
'SyntheticAccessor'
baseline file("lint-baseline.xml")
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
package="org.kiwix.kiwixmobile">
@ -7,11 +8,18 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_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.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Devices with version >= Oreo need location permission to start/stop the hotspot -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- Device with versions >= Pie need this permission -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:name=".KiwixApplication"
android:allowBackup="true"
@ -140,6 +148,7 @@
android:name=".zim_manager.ZimManageActivity"
android:label="@string/choose_file"
android:launchMode="singleTop">
<!-- TODO -->
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT"/>
@ -159,6 +168,7 @@
<activity android:name=".settings.KiwixSettingsActivity"/>
<activity android:name=".search.SearchActivity"/>
<activity android:name=".bookmark.BookmarksActivity"/>
<activity android:name=".webserver.ZimHostActivity"/>
<provider
android:name=".data.ZimContentProvider"
@ -178,6 +188,8 @@
android:name="android.appwidget.provider"
android:resource="@xml/kiwix_widget_provider_info"/>
</receiver>
<service android:name=".wifi_hotspot.HotspotService"/>
<activity
android:name=".error.ErrorActivity"
@ -193,7 +205,6 @@
android:resource="@xml/provider_paths"/>
</provider>
<activity android:name=".intro.IntroActivity"/>
<activity android:name=".language.LanguageActivity"/>
<activity android:name=".history.HistoryActivity"/>
@ -202,12 +213,14 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="main.MainActivity"/>
</activity>
<activity android:name=".zim_manager.local_file_transfer.LocalFileTransferActivity"
<activity
android:name=".zim_manager.local_file_transfer.LocalFileTransferActivity"
android:label="Send to nearby device"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/octet-stream"/>
<data android:pathPattern=".*\\.zim"/>
<data android:pathPattern=".*\\..*\\.zim"/>
@ -223,4 +236,5 @@
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>

View File

@ -0,0 +1,25 @@
/*
* Kiwix Android
* Copyright (C) 2018 Kiwix <android.kiwix.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kiwix.kiwixmobile.di
import javax.inject.Scope
import kotlin.annotation.AnnotationRetention.RUNTIME
@Scope
@Retention(RUNTIME)
annotation class ServiceScope

View File

@ -54,6 +54,8 @@ public interface ApplicationComponent {
ActivityComponent.Builder activityComponent();
ServiceComponent.Builder serviceComponent();
void inject(KiwixApplication application);
void inject(DownloadService service);

View File

@ -0,0 +1,22 @@
package org.kiwix.kiwixmobile.di.components
import android.app.Service
import dagger.BindsInstance
import dagger.Subcomponent
import org.kiwix.kiwixmobile.di.ServiceScope
import org.kiwix.kiwixmobile.di.modules.ServiceModule
import org.kiwix.kiwixmobile.wifi_hotspot.HotspotService
@Subcomponent(modules = [ServiceModule::class])
@ServiceScope
interface ServiceComponent {
fun inject(hotspotService: HotspotService)
@Subcomponent.Builder
interface Builder {
@BindsInstance fun service(service: Service): Builder
fun build(): ServiceComponent
}
}

View File

@ -17,6 +17,8 @@ import org.kiwix.kiwixmobile.main.MainModule;
import org.kiwix.kiwixmobile.search.SearchActivity;
import org.kiwix.kiwixmobile.settings.KiwixSettingsActivity;
import org.kiwix.kiwixmobile.splash.SplashActivity;
import org.kiwix.kiwixmobile.webserver.ZimHostActivity;
import org.kiwix.kiwixmobile.webserver.ZimHostModule;
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity;
/**
@ -70,4 +72,8 @@ public abstract class ActivityBindingModule {
@PerActivity
@ContributesAndroidInjector
public abstract HelpActivity provideHelpActivity();
@PerActivity
@ContributesAndroidInjector(modules = ZimHostModule.class)
public abstract ZimHostActivity provideZimHostActivity();
}

View File

@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.di.modules;
import android.content.Context;
import androidx.annotation.NonNull;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
@ -30,7 +31,7 @@ import org.kiwix.kiwixlib.JNIKiwix;
@Module public class JNIModule {
@Provides
@Singleton
public JNIKiwix providesJNIKiwix(Context context) {
public JNIKiwix providesJNIKiwix(@NonNull Context context) {
return new JNIKiwix(context);
}
}

View File

@ -0,0 +1,70 @@
package org.kiwix.kiwixmobile.di.modules
import android.app.Application
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.net.wifi.WifiManager
import dagger.Module
import dagger.Provides
import org.kiwix.kiwixlib.JNIKiwixLibrary
import org.kiwix.kiwixlib.JNIKiwixServer
import org.kiwix.kiwixmobile.di.ServiceScope
import org.kiwix.kiwixmobile.webserver.WebServerHelper
import org.kiwix.kiwixmobile.wifi_hotspot.HotspotNotificationManager
import org.kiwix.kiwixmobile.wifi_hotspot.HotspotStateListener
import org.kiwix.kiwixmobile.wifi_hotspot.IpAddressCallbacks
import org.kiwix.kiwixmobile.wifi_hotspot.WifiHotspotManager
@Module
class ServiceModule {
@Provides
@ServiceScope
fun providesWebServerHelper(
jniKiwixLibrary: JNIKiwixLibrary,
kiwixServer: JNIKiwixServer,
ipAddressCallbacks: IpAddressCallbacks
): WebServerHelper = WebServerHelper(jniKiwixLibrary, kiwixServer, ipAddressCallbacks)
@Provides
@ServiceScope
fun providesWifiHotspotManager(
wifiManager: WifiManager,
hotspotStateListener: HotspotStateListener
): WifiHotspotManager =
WifiHotspotManager(wifiManager, hotspotStateListener)
@Provides
@ServiceScope
fun providesHotspotStateListener(service: Service): HotspotStateListener =
service as HotspotStateListener
@Provides
@ServiceScope
fun providesIpAddressCallbacks(service: Service): IpAddressCallbacks =
service as IpAddressCallbacks
@Provides
@ServiceScope
fun providesJNIKiwixLibrary(): JNIKiwixLibrary = JNIKiwixLibrary()
@Provides
@ServiceScope
fun providesJNIKiwixServer(jniKiwixLibrary: JNIKiwixLibrary): JNIKiwixServer =
JNIKiwixServer(jniKiwixLibrary)
@Provides
@ServiceScope
fun providesWifiManager(context: Application): WifiManager =
context.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Provides
@ServiceScope
fun providesHotspotNotificationManager(
notificationManager: NotificationManager,
context: Context
): HotspotNotificationManager =
HotspotNotificationManager(notificationManager, context)
}

View File

@ -86,6 +86,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import io.reactivex.android.schedulers.AndroidSchedulers;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -111,6 +112,7 @@ import org.kiwix.kiwixmobile.utils.LanguageUtils;
import org.kiwix.kiwixmobile.utils.NetworkUtils;
import org.kiwix.kiwixmobile.utils.StyleUtils;
import org.kiwix.kiwixmobile.utils.files.FileUtils;
import org.kiwix.kiwixmobile.webserver.ZimHostActivity;
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskDelegate;
@ -939,6 +941,12 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
Intent intentSupportKiwix = new Intent(Intent.ACTION_VIEW, uriSupportKiwix);
intentSupportKiwix.putExtra(EXTRA_EXTERNAL_LINK, true);
openExternalUrl(intentSupportKiwix);
break;
case R.id.menu_host_books:
Intent intent = new Intent(MainActivity.this, ZimHostActivity.class);
startActivity(intent);
break;
default:
break;
@ -1687,6 +1695,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
}
}
return;
default:
break;
}
@ -1756,6 +1765,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
menu.findItem(R.id.menu_home).setVisible(false);
menu.findItem(R.id.menu_random_article).setVisible(false);
menu.findItem(R.id.menu_searchintext).setVisible(false);
menu.findItem(R.id.menu_host_books).setVisible(true);
} else {
menu.findItem(R.id.menu_read_aloud).setVisible(true);
menu.findItem(R.id.menu_home).setVisible(true);

View File

@ -3,6 +3,7 @@ package org.kiwix.kiwixmobile.utils
import android.app.Activity
import android.app.AlertDialog
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.utils.KiwixDialog.StartHotspotManually
import javax.inject.Inject
class AlertDialogShower @Inject constructor(
@ -27,9 +28,17 @@ class AlertDialogShower @Inject constructor(
clickListeners.getOrNull(0)
?.invoke()
}
setNegativeButton(dialog.negativeMessage) { _, _ ->
clickListeners.getOrNull(1)
?.invoke()
dialog.negativeMessage?.let {
setNegativeButton(it) { _, _ ->
clickListeners.getOrNull(1)
?.invoke()
}
}
if (dialog is StartHotspotManually) {
setNeutralButton(dialog.neutralMessage) { _, _ ->
clickListeners.getOrNull(2)
?.invoke()
}
}
}
.show()

View File

@ -126,6 +126,8 @@ public final class Constants {
public static final String EXTRA_NOTIFICATION_ID = "notificationID";
public static final String HOTSPOT_SERVICE_CHANNEL_ID = "hotspotService";
public static final String EXTRA_WEBVIEWS_LIST = "webviewsList";
public static final String EXTRA_SEARCH_TEXT = "searchText";

View File

@ -1,13 +1,15 @@
package org.kiwix.kiwixmobile.utils
import android.net.wifi.WifiConfiguration
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
sealed class KiwixDialog(
val title: Int?,
val message: Int,
val positiveMessage: Int,
val negativeMessage: Int
val negativeMessage: Int?
) {
data class DeleteZim(override val args: Array<out Any>) : KiwixDialog(
@ -30,23 +32,55 @@ sealed class KiwixDialog(
}
object LocationPermissionRationale : KiwixDialog(
null, R.string.permission_rationale_location, android.R.string.yes, android.R.string.cancel
null,
R.string.permission_rationale_location,
android.R.string.yes,
android.R.string.cancel
)
object StoragePermissionRationale : KiwixDialog(
null, R.string.request_storage, android.R.string.yes, android.R.string.cancel
null, R.string.request_storage, android.R.string.yes, android.R.string.cancel
)
object EnableWifiP2pServices : KiwixDialog(
null, R.string.request_enable_wifi, R.string.yes, android.R.string.no
null, R.string.request_enable_wifi, R.string.yes, android.R.string.no
)
object EnableLocationServices : KiwixDialog(
null, R.string.request_enable_location, R.string.yes, android.R.string.no
null, R.string.request_enable_location, R.string.yes, android.R.string.no
)
object TurnOffHotspotManually : KiwixDialog(
R.string.hotspot_failed_title,
R.string.hotspot_failed_message,
R.string.go_to_wifi_settings_label,
null
)
data class ShowHotspotDetails(override val args: Array<out Any>) : KiwixDialog(
R.string.hotspot_turned_on,
R.string.hotspot_details_message,
android.R.string.ok,
null
), HasBodyFormatArgs {
constructor(wifiConfiguration: WifiConfiguration) : this(
arrayOf(
wifiConfiguration.SSID,
wifiConfiguration.preSharedKey
)
)
}
data class StartHotspotManually(val neutralMessage: Int = string.hotspot_dialog_neutral_button) :
KiwixDialog(
string.hotspot_dialog_title,
string.hotspot_dialog_message,
string.go_to_settings_label,
null
)
data class FileTransferConfirmation(override val args: Array<out Any>) : KiwixDialog(
null, R.string.transfer_to, R.string.yes, android.R.string.cancel
null, R.string.transfer_to, R.string.yes, android.R.string.cancel
), HasBodyFormatArgs {
constructor(selectedPeerDeviceName: String) : this(arrayOf(selectedPeerDeviceName))
}

View File

@ -0,0 +1,61 @@
package org.kiwix.kiwixmobile.utils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
public class ServerUtils {
public static int port;
public static boolean isServerStarted;
public static final String INVALID_IP = "-1";
// get Ip address of the device's wireless access point i.e. wifi hotspot OR wifi network
@Nullable public static String getIpAddress() {
String ip = "";
try {
Enumeration<NetworkInterface> enumNetworkInterfaces = NetworkInterface
.getNetworkInterfaces();
while (enumNetworkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = enumNetworkInterfaces
.nextElement();
Enumeration<InetAddress> enumInetAddress = networkInterface
.getInetAddresses();
while (enumInetAddress.hasMoreElements()) {
InetAddress inetAddress = enumInetAddress.nextElement();
if (inetAddress.isSiteLocalAddress()) {
ip += inetAddress.getHostAddress() + "\n";
}
}
}
//To remove extra characters from IP for Android Pie
if (ip.length() > 14) {
for (int i = 15; i < 18; i++) {
if ((ip.charAt(i) == '.')) {
ip = ip.substring(0, i - 2);
break;
}
}
}
} catch (SocketException e) {
e.printStackTrace();
ip += "Something Wrong! " + e.toString() + "\n";
}
return ip;
}
@NonNull public static String getSocketAddress() {
String address = "http://" + getIpAddress() + ":" + port;
address = address.replaceAll("\n", "");
return address;
}
@Nullable public static String getIp() {
String ip = getIpAddress();
ip = ip.replaceAll("\n", "");
return ip.length() == 0 ? INVALID_IP : ip;
}
}

View File

@ -0,0 +1,5 @@
package org.kiwix.kiwixmobile.webserver;
public interface LocationCallbacks {
void onLocationSet();
}

View File

@ -0,0 +1,107 @@
package org.kiwix.kiwixmobile.webserver;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.ResolvableApiException;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResponse;
import com.google.android.gms.location.LocationSettingsStates;
import com.google.android.gms.location.LocationSettingsStatusCodes;
import com.google.android.gms.tasks.Task;
import javax.inject.Inject;
public class LocationServicesHelper {
private static final String TAG = "LocationServicesHelper";
private final LocationCallbacks locationCallbacks;
private final Activity activity;
private static final int LOCATION_SETTINGS_PERMISSION_RESULT = 101;
@Inject
public LocationServicesHelper(@NonNull Activity activity,
@NonNull LocationCallbacks locationCallbacks) {
this.activity = activity;
this.locationCallbacks = locationCallbacks;
}
private Task<LocationSettingsResponse> task;
public void setupLocationServices() {
LocationRequest locationRequest = new LocationRequest();
locationRequest.setInterval(10);
locationRequest.setSmallestDisplacement(10);
locationRequest.setFastestInterval(10);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
LocationSettingsRequest.Builder builder = new
LocationSettingsRequest.Builder();
builder.addLocationRequest(locationRequest);
task = com.google.android.gms.location.LocationServices.getSettingsClient(activity)
.checkLocationSettings(builder.build());
locationSettingsResponseBuilder();
}
private void locationSettingsResponseBuilder() {
task.addOnCompleteListener(task -> {
try {
LocationSettingsResponse response = task.getResult(ApiException.class);
// All location settings are satisfied. The client can initialize location
// requests here.
locationCallbacks.onLocationSet();
//}
} catch (ApiException exception) {
switch (exception.getStatusCode()) {
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
// Location settings are not satisfied. But could be fixed by showing the
// user a dialog.
try {
// Cast to a resolvable exception.
ResolvableApiException resolvable = (ResolvableApiException) exception;
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
resolvable.startResolutionForResult(
activity,
LOCATION_SETTINGS_PERMISSION_RESULT);
} catch (IntentSender.SendIntentException e) {
// Ignore the error.
} catch (ClassCastException e) {
// Ignore, should be an impossible error.
}
break;
case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
// Location settings are not satisfied. However, we have no way to fix the
// settings so we won't show the dialog.
break;
default:
break;
}
}
});
}
public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data) {
//Checking the result code for LocationSettings resolution
if (requestCode == LOCATION_SETTINGS_PERMISSION_RESULT) {
final LocationSettingsStates states = LocationSettingsStates.fromIntent(data);
switch (resultCode) {
case Activity.RESULT_OK:
// All required changes were successfully made
Log.v(TAG, states.isLocationPresent() + "");
locationCallbacks.onLocationSet();
break;
case Activity.RESULT_CANCELED:
// The user was asked to change settings, but chose not to
Log.v(TAG, "Canceled");
break;
default:
break;
}
}
}
}

View File

@ -0,0 +1,99 @@
package org.kiwix.kiwixmobile.webserver;
import android.util.Log;
import androidx.annotation.NonNull;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.kiwix.kiwixlib.JNIKiwixException;
import org.kiwix.kiwixlib.JNIKiwixLibrary;
import org.kiwix.kiwixlib.JNIKiwixServer;
import org.kiwix.kiwixmobile.utils.ServerUtils;
import org.kiwix.kiwixmobile.wifi_hotspot.IpAddressCallbacks;
import static org.kiwix.kiwixmobile.utils.ServerUtils.INVALID_IP;
/**
* WebServerHelper class is used to set up the suitable environment i.e. getting the
* ip address and port no. before starting the WebServer
* Created by Adeel Zafar on 18/07/2019.
*/
public class WebServerHelper {
private static final String TAG = "WebServerHelper";
private JNIKiwixLibrary kiwixLibrary;
private JNIKiwixServer kiwixServer;
private IpAddressCallbacks ipAddressCallbacks;
private boolean isServerStarted;
@Inject public WebServerHelper(@NonNull JNIKiwixLibrary kiwixLibrary,
@NonNull JNIKiwixServer kiwixServer, @NonNull IpAddressCallbacks ipAddressCallbacks) {
this.kiwixLibrary = kiwixLibrary;
this.kiwixServer = kiwixServer;
this.ipAddressCallbacks = ipAddressCallbacks;
}
public boolean startServerHelper(@NonNull ArrayList<String> selectedBooksPath) {
String ip = ServerUtils.getIpAddress();
if (ip.length() == 0) {
return false;
} else if (startAndroidWebServer(selectedBooksPath)) {
return true;
}
return isServerStarted;
}
public void stopAndroidWebServer() {
if (isServerStarted) {
kiwixServer.stop();
updateServerState(false);
}
}
private boolean startAndroidWebServer(ArrayList<String> selectedBooksPath) {
if (!isServerStarted) {
int DEFAULT_PORT = 8080;
ServerUtils.port = DEFAULT_PORT;
for (String path : selectedBooksPath) {
try {
boolean isBookAdded = kiwixLibrary.addBook(path);
Log.v(TAG, "isBookAdded: " + isBookAdded + path);
} catch (JNIKiwixException e) {
Log.v(TAG, "Couldn't add book " + path);
}
}
kiwixServer.setPort(ServerUtils.port);
updateServerState(kiwixServer.start());
Log.v(TAG, "Server status" + isServerStarted);
}
return isServerStarted;
}
private void updateServerState(boolean isStarted) {
isServerStarted = isStarted;
ServerUtils.isServerStarted = isStarted;
}
//Keeps checking if hotspot has been turned using the ip address with an interval of 1 sec
//If no ip is found after 15 seconds, dismisses the progress dialog
public void pollForValidIpAddress() {
Flowable.interval(1, TimeUnit.SECONDS)
.map(__ -> ServerUtils.getIp())
.filter(s -> s != INVALID_IP)
.timeout(15, TimeUnit.SECONDS)
.take(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
s -> {
ipAddressCallbacks.onIpAddressValid();
Log.d(TAG, "onSuccess: " + s);
},
e -> {
Log.d(TAG, "Unable to turn on server", e);
ipAddressCallbacks.onIpAddressInvalid();
}
);
}
}

View File

@ -0,0 +1,367 @@
package org.kiwix.kiwixmobile.webserver;
import android.Manifest;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.wifi.WifiConfiguration;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import org.kiwix.kiwixmobile.R;
import org.kiwix.kiwixmobile.base.BaseActivity;
import org.kiwix.kiwixmobile.utils.AlertDialogShower;
import org.kiwix.kiwixmobile.utils.KiwixDialog;
import org.kiwix.kiwixmobile.utils.ServerUtils;
import org.kiwix.kiwixmobile.wifi_hotspot.HotspotService;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskDelegate;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskAdapter;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem;
import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_CHECK_IP_ADDRESS;
import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_LOCATION_ACCESS_GRANTED;
import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_START_SERVER;
import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_STOP_SERVER;
import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_TOGGLE_HOTSPOT;
public class ZimHostActivity extends BaseActivity implements
ZimHostCallbacks, ZimHostContract.View, LocationCallbacks {
@BindView(R.id.startServerButton)
Button startServerButton;
@BindView(R.id.server_textView)
TextView serverTextView;
@BindView(R.id.recycler_view_zim_host)
RecyclerView recyclerViewZimHost;
@Inject
ZimHostContract.Presenter presenter;
@Inject
AlertDialogShower alertDialogShower;
@Inject
LocationServicesHelper locationServicesHelper;
private static final String TAG = "ZimHostActivity";
private static final int MY_PERMISSIONS_ACCESS_FINE_LOCATION = 102;
private static final String IP_STATE_KEY = "ip_state_key";
public static final String SELECTED_ZIM_PATHS_KEY = "selected_zim_paths";
private BooksOnDiskAdapter booksAdapter;
private BookOnDiskDelegate.BookDelegate bookDelegate;
private HotspotService hotspotService;
private String ip;
private ServiceConnection serviceConnection;
private ProgressDialog progressDialog;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_zim_host);
setUpToolbar();
if (savedInstanceState != null) {
ip = savedInstanceState.getString(IP_STATE_KEY);
layoutServerStarted();
}
bookDelegate =
new BookOnDiskDelegate.BookDelegate(sharedPreferenceUtil,
null,
null,
bookOnDiskItem -> {
select(bookOnDiskItem);
return Unit.INSTANCE;
});
bookDelegate.setSelectionMode(SelectionMode.MULTI);
booksAdapter = new BooksOnDiskAdapter(bookDelegate,
BookOnDiskDelegate.LanguageDelegate.INSTANCE
);
presenter.attachView(this);
presenter.loadBooks();
recyclerViewZimHost.setAdapter(booksAdapter);
serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
hotspotService = ((HotspotService.HotspotBinder) service).getService();
hotspotService.registerCallBack(ZimHostActivity.this);
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
}
};
startServerButton.setOnClickListener(v -> {
//Get the path of ZIMs user has selected
if (!ServerUtils.isServerStarted) {
if (getSelectedBooksPath().size() > 0) {
startHotspotHelper();
} else {
Toast.makeText(ZimHostActivity.this, R.string.no_books_selected_toast_message,
Toast.LENGTH_SHORT).show();
}
} else {
startHotspotHelper();
}
});
}
private void startHotspotHelper() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
toggleHotspot();
} else {
if (ServerUtils.isServerStarted) {
startService(createHotspotIntent(ACTION_STOP_SERVER));
} else {
startHotspotManuallyDialog();
}
}
}
private ArrayList<String> getSelectedBooksPath() {
ArrayList<String> selectedBooksPath = new ArrayList<>();
for (BooksOnDiskListItem item : booksAdapter.getItems()) {
if (item.isSelected()) {
BooksOnDiskListItem.BookOnDisk bookOnDisk = (BooksOnDiskListItem.BookOnDisk) item;
File file = bookOnDisk.getFile();
selectedBooksPath.add(file.getAbsolutePath());
Log.v(TAG, "ZIM PATH : " + file.getAbsolutePath());
}
}
return selectedBooksPath;
}
private void select(@NonNull BooksOnDiskListItem.BookOnDisk bookOnDisk) {
ArrayList<BooksOnDiskListItem> booksList = new ArrayList<>();
for (BooksOnDiskListItem item : booksAdapter.getItems()) {
if (item.equals(bookOnDisk)) {
item.setSelected(!item.isSelected());
}
booksList.add(item);
}
booksAdapter.setItems(booksList);
}
@Override protected void onStart() {
super.onStart();
bindService();
}
@Override protected void onStop() {
super.onStop();
unbindService();
}
private void bindService() {
bindService(new Intent(this, HotspotService.class), serviceConnection,
Context.BIND_AUTO_CREATE);
}
private void unbindService() {
if (hotspotService != null) {
unbindService(serviceConnection);
}
}
private void toggleHotspot() {
//Check if location permissions are granted
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
//Toggle hotspot if location permissions are granted
startService(createHotspotIntent(
ACTION_TOGGLE_HOTSPOT));
} else {
//Ask location permission if not granted
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.ACCESS_FINE_LOCATION },
MY_PERMISSIONS_ACCESS_FINE_LOCATION);
}
}
@Override protected void onResume() {
super.onResume();
presenter.loadBooks();
if (ServerUtils.isServerStarted) {
ip = ServerUtils.getSocketAddress();
layoutServerStarted();
}
}
private void layoutServerStarted() {
serverTextView.setText(getString(R.string.server_started_message, ip));
startServerButton.setText(getString(R.string.stop_server_label));
startServerButton.setBackgroundColor(getResources().getColor(R.color.stopServer));
bookDelegate.setSelectionMode(SelectionMode.NORMAL);
for (BooksOnDiskListItem item : booksAdapter.getItems()) {
item.setSelected(false);
}
booksAdapter.notifyDataSetChanged();
}
private void layoutServerStopped() {
serverTextView.setText(getString(R.string.server_textview_default_message));
startServerButton.setText(getString(R.string.start_server_label));
startServerButton.setBackgroundColor(getResources().getColor(R.color.greenTick));
bookDelegate.setSelectionMode(SelectionMode.MULTI);
booksAdapter.notifyDataSetChanged();
}
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == MY_PERMISSIONS_ACCESS_FINE_LOCATION) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
toggleHotspot();
}
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
locationServicesHelper.onActivityResult(requestCode, resultCode, (data));
}
@Override protected void onDestroy() {
super.onDestroy();
presenter.detachView();
}
private void setUpToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle(getString(R.string.menu_host_books));
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(v -> onBackPressed());
}
//Advice user to turn on hotspot manually for API<26
private void startHotspotManuallyDialog() {
alertDialogShower.show(new KiwixDialog.StartHotspotManually(),
() -> {
launchTetheringSettingsScreen();
return Unit.INSTANCE;
},
null,
() -> {
progressDialog =
ProgressDialog.show(this,
getString(R.string.progress_dialog_starting_server), "",
true);
startService(createHotspotIntent(ACTION_CHECK_IP_ADDRESS));
return Unit.INSTANCE;
}
);
}
private Intent createHotspotIntent(String action) {
return new Intent(this, HotspotService.class).setAction(action);
}
@Override public void onServerStarted(@NonNull String ipAddress) {
this.ip = ipAddress;
layoutServerStarted();
}
@Override public void onServerStopped() {
layoutServerStopped();
}
@Override public void onServerFailedToStart() {
Toast.makeText(this, R.string.server_failed_toast_message, Toast.LENGTH_LONG).show();
}
@Override public void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration) {
alertDialogShower.show(new KiwixDialog.ShowHotspotDetails(wifiConfiguration),
(Function0<Unit>) () -> {
progressDialog =
ProgressDialog.show(this,
getString(R.string.progress_dialog_starting_server), "",
true);
startService(createHotspotIntent(ACTION_CHECK_IP_ADDRESS));
return Unit.INSTANCE;
});
}
private void launchTetheringSettingsScreen() {
final Intent intent = new Intent(Intent.ACTION_MAIN, null);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
final ComponentName cn =
new ComponentName("com.android.settings", "com.android.settings.TetherSettings");
intent.setComponent(cn);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
@Override public void onHotspotFailedToStart() {
//Show a dialog to turn off default hotspot
alertDialogShower.show(KiwixDialog.TurnOffHotspotManually.INSTANCE,
(Function0<Unit>) () -> {
launchTetheringSettingsScreen();
return Unit.INSTANCE;
});
}
@Override public void requestLocationAccess() {
locationServicesHelper.setupLocationServices();
}
@Override protected void onSaveInstanceState(@Nullable Bundle outState) {
super.onSaveInstanceState(outState);
if (ServerUtils.isServerStarted) {
outState.putString(IP_STATE_KEY, ip);
}
}
@Override public void addBooks(@Nullable List<BooksOnDiskListItem> books) {
booksAdapter.setItems(books);
}
@Override public void onLocationSet() {
startService(createHotspotIntent(ACTION_LOCATION_ACCESS_GRANTED));
}
@Override public void onIpAddressValid() {
progressDialog.dismiss();
startService(createHotspotIntent(ACTION_START_SERVER).putStringArrayListExtra(
SELECTED_ZIM_PATHS_KEY, getSelectedBooksPath()));
}
@Override public void onIpAddressInvalid() {
progressDialog.dismiss();
Toast.makeText(this, R.string.server_failed_message,
Toast.LENGTH_SHORT)
.show();
}
}

View File

@ -0,0 +1,23 @@
package org.kiwix.kiwixmobile.webserver;
import android.net.wifi.WifiConfiguration;
import androidx.annotation.NonNull;
public interface ZimHostCallbacks {
void onServerStarted(@NonNull String ip);
void onServerStopped();
void onServerFailedToStart();
void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration);
void onHotspotFailedToStart();
void requestLocationAccess();
void onIpAddressValid();
void onIpAddressInvalid();
}

View File

@ -0,0 +1,18 @@
package org.kiwix.kiwixmobile.webserver;
import java.util.List;
import org.kiwix.kiwixmobile.base.BaseContract;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem;
class ZimHostContract {
interface View
extends BaseContract.View<org.kiwix.kiwixmobile.webserver.ZimHostContract.Presenter> {
void addBooks(List<BooksOnDiskListItem> books);
}
interface Presenter
extends BaseContract.Presenter<org.kiwix.kiwixmobile.webserver.ZimHostContract.View> {
void loadBooks();
}
}

View File

@ -0,0 +1,33 @@
package org.kiwix.kiwixmobile.webserver;
import android.app.Activity;
import dagger.Module;
import dagger.Provides;
import org.kiwix.kiwixmobile.di.PerActivity;
@Module
public class ZimHostModule {
@PerActivity
@Provides
ZimHostContract.Presenter provideZimHostPresenter(ZimHostPresenter zimHostPresenter) {
return zimHostPresenter;
}
@PerActivity
@Provides Activity providesActivity(ZimHostActivity zimHostActivity) {
return zimHostActivity;
}
@PerActivity
@Provides LocationServicesHelper providesLocationServicesHelper(ZimHostActivity activity,
LocationCallbacks locationCallbacks) {
return new LocationServicesHelper(activity, locationCallbacks);
}
@PerActivity
@Provides LocationCallbacks providesLocationCallbacks(ZimHostActivity activity) {
return activity;
}
}

View File

@ -0,0 +1,44 @@
package org.kiwix.kiwixmobile.webserver;
import android.util.Log;
import io.reactivex.SingleObserver;
import io.reactivex.disposables.Disposable;
import java.util.List;
import javax.inject.Inject;
import org.kiwix.kiwixmobile.base.BasePresenter;
import org.kiwix.kiwixmobile.data.DataSource;
import org.kiwix.kiwixmobile.di.PerActivity;
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem;
@PerActivity
class ZimHostPresenter extends BasePresenter<ZimHostContract.View>
implements ZimHostContract.Presenter {
private static final String TAG = "ZimHostPresenter";
private final DataSource dataSource;
@Inject ZimHostPresenter(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void loadBooks() {
dataSource.getLanguageCategorizedBooks()
.subscribe(new SingleObserver<List<BooksOnDiskListItem>>() {
@Override
public void onSubscribe(Disposable d) {
compositeDisposable.add(d);
}
@Override
public void onSuccess(List<BooksOnDiskListItem> books) {
view.addBooks(books);
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "Unable to load books", e);
}
});
}
}

View File

@ -0,0 +1,74 @@
package org.kiwix.kiwixmobile.wifi_hotspot;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import javax.inject.Inject;
import org.kiwix.kiwixmobile.R;
import org.kiwix.kiwixmobile.utils.Constants;
import org.kiwix.kiwixmobile.webserver.ZimHostActivity;
import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotService.ACTION_STOP;
public class HotspotNotificationManager {
public static final int HOTSPOT_NOTIFICATION_ID = 666;
private Context context;
@Inject
NotificationManager notificationManager;
@Inject
public HotspotNotificationManager(@NonNull NotificationManager notificationManager,
@NonNull Context context) {
this.notificationManager = notificationManager;
this.context = context;
}
private void hotspotNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel hotspotServiceChannel = new NotificationChannel(
Constants.HOTSPOT_SERVICE_CHANNEL_ID,
context.getString(R.string.hotspot_service_channel_name),
NotificationManager.IMPORTANCE_DEFAULT);
hotspotServiceChannel.setDescription(context.getString(R.string.hotspot_channel_description));
hotspotServiceChannel.setSound(null, null);
notificationManager.createNotificationChannel(hotspotServiceChannel);
}
}
@NonNull public Notification buildForegroundNotification() {
Intent targetIntent = new Intent(context, ZimHostActivity.class);
targetIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent contentIntent =
PendingIntent.getActivity(context, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT);
hotspotNotificationChannel();
Intent stopIntent = new Intent(context, HotspotService.class).setAction(ACTION_STOP);
PendingIntent stopHotspot =
PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT);
return new NotificationCompat.Builder(context)
.setContentTitle(context.getString(R.string.hotspot_notification_content_title))
.setContentText(context.getString(R.string.hotspot_running))
.setContentIntent(contentIntent)
.setSmallIcon(R.mipmap.kiwix_icon)
.setWhen(System.currentTimeMillis())
.addAction(R.drawable.ic_close_white_24dp,
context.getString(R.string.stop_hotspot_button),
stopHotspot)
.setChannelId(Constants.HOTSPOT_SERVICE_CHANNEL_ID)
.build();
}
public void dismissNotification() {
notificationManager.cancel(HOTSPOT_NOTIFICATION_ID);
}
}

View File

@ -0,0 +1,171 @@
package org.kiwix.kiwixmobile.wifi_hotspot;
import android.app.Service;
import android.content.Intent;
import android.net.wifi.WifiConfiguration;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import javax.inject.Inject;
import org.kiwix.kiwixmobile.KiwixApplication;
import org.kiwix.kiwixmobile.R;
import org.kiwix.kiwixmobile.utils.ServerUtils;
import org.kiwix.kiwixmobile.webserver.ZimHostCallbacks;
import org.kiwix.kiwixmobile.webserver.WebServerHelper;
import static org.kiwix.kiwixmobile.webserver.ZimHostActivity.SELECTED_ZIM_PATHS_KEY;
import static org.kiwix.kiwixmobile.wifi_hotspot.HotspotNotificationManager.HOTSPOT_NOTIFICATION_ID;
/**
* HotspotService is used to add a foreground service for the wifi hotspot.
* Created by Adeel Zafar on 07/01/2019.
*/
public class HotspotService extends Service implements HotspotStateListener, IpAddressCallbacks {
public static final String ACTION_TOGGLE_HOTSPOT = "toggle_hotspot";
public static final String ACTION_LOCATION_ACCESS_GRANTED = "location_access_granted";
public static final String ACTION_START_SERVER = "start_server";
public static final String ACTION_STOP_SERVER = "stop_server";
public static final String ACTION_CHECK_IP_ADDRESS = "check_ip_address";
public static final String ACTION_STOP = "hotspot_stop";
private ZimHostCallbacks zimHostCallbacks;
private final IBinder serviceBinder = new HotspotBinder();
@Inject
WebServerHelper webServerHelper;
@Inject
WifiHotspotManager hotspotManager;
@Inject
HotspotNotificationManager hotspotNotificationManager;
@Override public void onCreate() {
KiwixApplication.getApplicationComponent()
.serviceComponent()
.service(this)
.build()
.inject(this);
super.onCreate();
}
@Override public int onStartCommand(@NonNull Intent intent, int flags, int startId) {
switch (intent.getAction()) {
case ACTION_TOGGLE_HOTSPOT:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (hotspotManager.isHotspotStarted()) {
stopHotspotAndDismissNotification();
} else {
zimHostCallbacks.requestLocationAccess();
}
}
break;
case ACTION_LOCATION_ACCESS_GRANTED:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
hotspotManager.turnOnHotspot();
}
break;
case ACTION_START_SERVER:
if (webServerHelper.startServerHelper(
intent.getStringArrayListExtra(SELECTED_ZIM_PATHS_KEY))) {
zimHostCallbacks.onServerStarted(ServerUtils.getSocketAddress());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
startForegroundNotificationHelper();
}
Toast.makeText(this, R.string.server_started__successfully_toast_message,
Toast.LENGTH_SHORT).show();
} else {
zimHostCallbacks.onServerFailedToStart();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true);
stopSelf();
hotspotNotificationManager.dismissNotification();
}
}
break;
case ACTION_STOP_SERVER:
stopHotspotAndDismissNotification();
break;
case ACTION_CHECK_IP_ADDRESS:
webServerHelper.pollForValidIpAddress();
break;
case ACTION_STOP:
stopHotspotAndDismissNotification();
break;
default:
break;
}
return START_NOT_STICKY;
}
@Nullable @Override public IBinder onBind(@Nullable Intent intent) {
return serviceBinder;
}
//Dismiss notification and turn off hotspot for devices>=O
private void stopHotspotAndDismissNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
hotspotManager.turnOffHotspot();
} else {
webServerHelper.stopAndroidWebServer();
zimHostCallbacks.onServerStopped();
stopForeground(true);
stopSelf();
hotspotNotificationManager.dismissNotification();
}
}
public void registerCallBack(@Nullable ZimHostCallbacks myCallback) {
zimHostCallbacks = myCallback;
}
private void startForegroundNotificationHelper() {
startForeground(HOTSPOT_NOTIFICATION_ID,
hotspotNotificationManager.buildForegroundNotification());
}
@Override public void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration) {
startForegroundNotificationHelper();
zimHostCallbacks.onHotspotTurnedOn(wifiConfiguration);
}
@Override public void onHotspotFailedToStart() {
zimHostCallbacks.onHotspotFailedToStart();
}
@Override public void onHotspotStopped() {
webServerHelper.stopAndroidWebServer();
zimHostCallbacks.onServerStopped();
stopForeground(true);
stopSelf();
hotspotNotificationManager.dismissNotification();
}
@Override public void onIpAddressValid() {
zimHostCallbacks.onIpAddressValid();
}
@Override public void onIpAddressInvalid() {
zimHostCallbacks.onIpAddressInvalid();
}
public class HotspotBinder extends Binder {
@NonNull public HotspotService getService() {
return HotspotService.this;
}
}
}

View File

@ -0,0 +1,12 @@
package org.kiwix.kiwixmobile.wifi_hotspot;
import android.net.wifi.WifiConfiguration;
import androidx.annotation.NonNull;
public interface HotspotStateListener {
void onHotspotTurnedOn(@NonNull WifiConfiguration wifiConfiguration);
void onHotspotFailedToStart();
void onHotspotStopped();
}

View File

@ -0,0 +1,8 @@
package org.kiwix.kiwixmobile.wifi_hotspot;
public interface IpAddressCallbacks {
void onIpAddressValid();
void onIpAddressInvalid();
}

View File

@ -0,0 +1,86 @@
package org.kiwix.kiwixmobile.wifi_hotspot;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import javax.inject.Inject;
/**
* WifiHotstopManager class makes use of the Android's WifiManager and WifiConfiguration class
* to implement the wifi hotspot feature.
* Created by Adeel Zafar on 28/5/2019.
*/
public class WifiHotspotManager {
private static final String TAG = "WifiHotspotManager";
private WifiManager wifiManager;
private WifiManager.LocalOnlyHotspotReservation hotspotReservation;
private HotspotStateListener hotspotStateListener;
@Inject
public WifiHotspotManager(@NonNull WifiManager wifiManager,
@NonNull HotspotStateListener hotspotStateListener) {
this.wifiManager = wifiManager;
this.hotspotStateListener = hotspotStateListener;
}
//Workaround to turn on hotspot for Oreo versions
@RequiresApi(api = Build.VERSION_CODES.O)
public void turnOnHotspot() {
wifiManager.startLocalOnlyHotspot(new WifiManager.LocalOnlyHotspotCallback() {
@Override
public void onStarted(WifiManager.LocalOnlyHotspotReservation reservation) {
super.onStarted(reservation);
hotspotReservation = reservation;
WifiConfiguration currentConfig = hotspotReservation.getWifiConfiguration();
printCurrentConfig(currentConfig);
hotspotStateListener.onHotspotTurnedOn(currentConfig);
Log.v(TAG, "Local Hotspot Started");
}
@Override
public void onStopped() {
super.onStopped();
hotspotStateListener.onHotspotStopped();
Log.v(TAG, "Local Hotspot Stopped");
}
@Override
public void onFailed(int reason) {
super.onFailed(reason);
hotspotStateListener.onHotspotFailedToStart();
Log.v(TAG, "Local Hotspot failed to start");
}
}, new Handler());
}
//Workaround to turn off hotspot for Oreo versions
@RequiresApi(api = Build.VERSION_CODES.O)
public void turnOffHotspot() {
if (hotspotReservation != null) {
hotspotReservation.close();
hotspotReservation = null;
hotspotStateListener.onHotspotStopped();
Log.v(TAG, "Turned off hotspot");
}
}
//This method checks the state of the hostpot for devices>=Oreo
@RequiresApi(api = Build.VERSION_CODES.O)
public boolean isHotspotStarted() {
return hotspotReservation != null;
}
private void printCurrentConfig(WifiConfiguration wifiConfiguration) {
Log.v(TAG, "THE PASSWORD IS: "
+ wifiConfiguration.preSharedKey
+ " \n SSID is : "
+ wifiConfiguration.SSID);
}
}

View File

@ -34,7 +34,7 @@ sealed class BookOnDiskDelegate<I : BooksOnDiskListItem, out VH : BookOnDiskView
class BookDelegate(
val sharedPreferenceUtil: SharedPreferenceUtil,
private val clickAction: (BookOnDisk) -> Unit,
private val clickAction: ((BookOnDisk) -> Unit)? = null,
private val longClickAction: ((BookOnDisk) -> Unit)? = null,
private val multiSelectAction: ((BookOnDisk) -> Unit)? = null
) : BookOnDiskDelegate<BookOnDisk, BookViewHolder>() {

View File

@ -32,7 +32,7 @@ sealed class BookOnDiskViewHolder<in T : BooksOnDiskListItem>(containerView: Vie
class BookViewHolder(
containerView: View,
private val sharedPreferenceUtil: SharedPreferenceUtil,
private val clickAction: (BookOnDisk) -> Unit,
private val clickAction: ((BookOnDisk) -> Unit)?,
private val longClickAction: ((BookOnDisk) -> Unit)?,
private val multiSelectAction: ((BookOnDisk) -> Unit)?
) : BookOnDiskViewHolder<BookOnDisk>(containerView) {
@ -80,7 +80,7 @@ sealed class BookOnDiskViewHolder<in T : BooksOnDiskListItem>(containerView: Vie
}
NORMAL -> {
itemBookCheckbox.visibility = View.GONE
item_book_clickable_area.setOnClickListener { clickAction.invoke(item) }
item_book_clickable_area.setOnClickListener { clickAction?.invoke(item) }
item_book_clickable_area.setOnLongClickListener {
longClickAction?.invoke(item)
return@setOnLongClickListener true

View File

@ -0,0 +1,69 @@
<?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"
android:fitsSystemWindows="true"
tools:context=".webserver.ZimHostActivity"
>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/zimHostAppBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
app:layout_constraintTop_toTopOf="parent"
>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/AppTheme.AppBarOverlay"
app:popupTheme="@style/AppTheme.PopupOverlay"
/>
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/server_textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/server_textview_default_message"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/zimHostAppBarLayout"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_zim_host"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_marginBottom="8dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@+id/startServerButton"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/server_textView"
/>
<Button
android:id="@+id/startServerButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:background="@color/greenTick"
android:text="@string/start_server_label"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -79,6 +79,11 @@
android:visible="false"
app:showAsAction="never"/>
<item
android:id="@+id/menu_host_books"
android:title="@string/menu_host_books"
app:showAsAction="never"/>
<item
android:id="@+id/menu_help"
android:title="@string/menu_help"

View File

@ -26,6 +26,7 @@
<color name="actionModeBackground">#4285F4</color>
<color name="titleBar">#000000</color>
<color name="greenTick">#4CAF50</color>
<color name="stopServer">#E53935</color>
<!-- Dark Text Color for Light Background -->
<color name="textDarkPrimary">#000000</color> <!-- 0% opacity-->

View File

@ -14,6 +14,7 @@
<string name="menu_read_aloud">Read aloud</string>
<string name="menu_read_aloud_stop">Stop reading aloud</string>
<string name="menu_support_kiwix">Support Kiwix</string>
<string name="menu_host_books">Host Books</string>
<string name="save_media">Save Media</string>
<string name="save_media_error">An error occurred when trying to save the media!</string>
<string name="save_media_saved">Saved media as %s to Android/media/org.kiwix…/</string>
@ -21,6 +22,29 @@
<string name="search_label">Search</string>
<string name="choose_file">Select a Content File (*.zim)</string>
<string name="open_in_new_tab">Open link in new tab?</string>
<string name="hotspot_service_channel_name">Hotspot Service Channel</string>
<string name="hotspot_failed_title">Failed to start hotspot</string>
<string name="hotspot_failed_message">It seems like your hotspot is already turned on. Please disable your wifi hotspot to continue.</string>
<string name="go_to_wifi_settings_label">Go to WIFI settings</string>
<string name="go_to_settings_label">Go to settings</string>
<string name="hotspot_running">Running Hotspot</string>
<string name="stop_hotspot_button">STOP</string>
<string name="no_books_selected_toast_message">Please select books first</string>
<string name="server_failed_message">Couldnt start server. Please turn on your hotspot</string>
<string name="server_failed_toast_message">Couldnt start server.</string>
<string name="server_started__successfully_toast_message">Server started successfully.</string>
<string name="hotspot_turned_on">Hotspot turned on</string>
<string name="hotspot_details_message">Following are the details of your local hotspot. \nSSID : %1$s \nPass : %2$s</string>
<string name="server_textview_default_message">Select the files you wish to host on the server</string>
<string name="progress_dialog_starting_server">Starting server</string>
<string name="hotspot_dialog_title">Turn on your WIFI hotspot</string>
<string name="hotspot_dialog_message">In order for this feature to work you need to first turn on your WIFI hotspot manually.</string>
<string name="hotspot_dialog_neutral_button">YES, IVE TURNED IT ON</string>
<string name="hotspot_channel_description">Updates about the state of your hotspot/server.</string>
<string name="hotspot_notification_content_title">Kiwix Hotspot</string>
<string name="start_server_label">Start server</string>
<string name="stop_server_label">Stop server</string>
<string name="server_started_message">Enter this ip address into your browser to access the server %s</string>
<string name="error_file_not_found">Error: The selected ZIM file could not be found.</string>
<string name="error_file_invalid">Error: The selected file is not a valid ZIM file.</string>
<string name="error_article_url_not_found">Error: Loading article (Url: %1$s) failed.</string>