diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..e430284 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +String getMyVersionName() { + def stdout = new ByteArrayOutputStream() + if (rootProject.file("gradlew").exists()) + exec { commandLine 'git', 'describe', '--tags', '--always', '--dirty'; standardOutput = stdout } + else // automatic build system, don't tag dirty + exec { commandLine 'git', 'describe', '--tags', '--always'; standardOutput = stdout } + return stdout.toString().trim().substring(1) +} + +int getMyVersionCode() { + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'git', 'rev-list', '--count', "HEAD" + standardOutput = stdout + } + return Integer.parseInt(stdout.toString().trim()) +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + dataBinding { + enabled = true + } + + defaultConfig { + versionName getMyVersionName() + versionCode(20000 + getMyVersionCode()) + minSdkVersion Math.max(androidMinSdk, 14) + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + flavorDimensions 'default' + productFlavors { + NetworkLocation { + applicationId = 'com.google.android.gms' + minSdkVersion 19 + dimension 'default' + } + LegacyNetworkLocation { + applicationId = 'com.google.android.location' + dimension 'default' + } + UnifiedNlp { + applicationId = 'org.microg.nlp' + dimension 'default' + } + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + lintOptions { + warning "MissingTranslation" + } +} + +apply from: "../gradle/androidJars.gradle" + +dependencies { + implementation project(':api') + implementation project(':geocode-v1') + implementation project(':location-v2') + implementation project(':location-v3') + implementation project(':service') + api project(':client') + api project(':ui') + + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + + // AndroidX UI + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "androidx.preference:preference:$preferenceVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + + // Navigation + implementation "androidx.navigation:navigation-fragment:$navigationVersion" + implementation "androidx.navigation:navigation-ui:$navigationVersion" + implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" + implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" +} + +afterEvaluate { + android.applicationVariants.all { variant -> + variant.resValue 'string', 'application_id', variant.applicationId + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..84b4b22 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/org/microg/nlp/app/AboutFragment.java b/app/src/main/java/org/microg/nlp/app/AboutFragment.java new file mode 100644 index 0000000..8cb0004 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/AboutFragment.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app; + +import androidx.fragment.app.Fragment; + +import org.microg.nlp.app.BuildConfig; + +import org.microg.nlp.app.tools.ui.AbstractAboutFragment; +import org.microg.nlp.app.tools.ui.AbstractSettingsActivity; + +import java.util.List; + +public class AboutFragment extends AbstractAboutFragment { + + @Override + protected void collectLibraries(List libraries) { + libraries.add(new AbstractAboutFragment.Library("org.microg.nlp.service", "UnifiedNlp", "Apache License 2.0, microG Team")); + } + + public static class AsActivity extends AbstractSettingsActivity { + public AsActivity() { + showHomeAsUp = true; + } + + @Override + protected Fragment getFragment() { + return new AboutFragment(); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/LocationSettingsActivity.java b/app/src/main/java/org/microg/nlp/app/LocationSettingsActivity.java new file mode 100644 index 0000000..ce9484e --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/LocationSettingsActivity.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app; + +import android.app.Activity; + +public class LocationSettingsActivity extends Activity { +} diff --git a/app/src/main/java/org/microg/nlp/app/SelfCheckFragment.java b/app/src/main/java/org/microg/nlp/app/SelfCheckFragment.java new file mode 100644 index 0000000..b56762a --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/SelfCheckFragment.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.util.Log; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import org.microg.nlp.app.tools.selfcheck.PermissionCheckGroup; +import org.microg.nlp.app.tools.selfcheck.SelfCheckGroup; +import org.microg.nlp.app.tools.ui.AbstractSelfCheckFragment; +import org.microg.nlp.app.tools.ui.AbstractSettingsActivity; +import org.microg.nlp.app.tools.selfcheck.NlpOsCompatChecks; +import org.microg.nlp.app.tools.selfcheck.NlpStatusChecks; + +import java.util.ArrayList; +import java.util.List; + +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; + +public class SelfCheckFragment extends AbstractSelfCheckFragment { + + @Override + protected void prepareSelfCheckList(List checks) { + if (SDK_INT > LOLLIPOP_MR1) { + checks.add(new PermissionCheckGroup(ACCESS_COARSE_LOCATION)); + } + checks.add(new NlpOsCompatChecks()); + checks.add(new NlpStatusChecks()); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + reset(LayoutInflater.from(getContext())); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + reset(LayoutInflater.from(getContext())); + super.onActivityResult(requestCode, resultCode, data); + } + + public static class AsActivity extends AbstractSettingsActivity { + public AsActivity() { + showHomeAsUp = true; + } + + @Override + protected Fragment getFragment() { + return new SelfCheckFragment(); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/SettingsActivity.java b/app/src/main/java/org/microg/nlp/app/SettingsActivity.java new file mode 100644 index 0000000..cf19a8f --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/SettingsActivity.java @@ -0,0 +1,34 @@ +package org.microg.nlp.app; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; + +public class SettingsActivity extends AppCompatActivity { + private AppBarConfiguration appBarConfiguration; + + private NavController getNavController() { + return ((NavHostFragment)getSupportFragmentManager().findFragmentById(R.id.navhost)).getNavController(); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.settings_root_activity); + + appBarConfiguration = new AppBarConfiguration.Builder(getNavController().getGraph()).build(); + NavigationUI.setupActionBarWithNavController(this, getNavController(), appBarConfiguration); + } + + @Override + public boolean onSupportNavigateUp() { + return NavigationUI.navigateUp(getNavController(), appBarConfiguration) || super.onSupportNavigateUp(); + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/selfcheck/NlpOsCompatChecks.java b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/NlpOsCompatChecks.java new file mode 100644 index 0000000..6e752a1 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/NlpOsCompatChecks.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2016 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.selfcheck; + +import android.content.Context; + +import org.microg.nlp.app.R; + +import java.util.Arrays; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.JELLY_BEAN; +import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; +import static android.os.Build.VERSION_CODES.KITKAT; +import static android.os.Build.VERSION_CODES.M; + +public class NlpOsCompatChecks implements SelfCheckGroup { + + public static final String CONFIG_NL_PROVIDER = "config_networkLocationProvider"; + public static final String CONFIG_NL_PROVIDER_PACKAGE_NAME = "config_networkLocationProviderPackageName"; + public static final String CONFIG_ENABLE_NL_OVERLAY = "config_enableNetworkLocationOverlay"; + public static final String CONFIG_NL_PROVIDER_PACKAGE_NAMES = "config_locationProviderPackageNames"; + + @Override + public String getGroupName(Context context) { + return context.getString(R.string.self_check_cat_nlpcompat); + } + + @Override + public void doChecks(Context context, ResultCollector collector) { + checkSystemIsSupported(context, collector); + checkSystemIsConfigured(context, collector); + } + + private boolean checkSystemIsSupported(Context context, ResultCollector collector) { + boolean isSupported = (SDK_INT >= KITKAT && SDK_INT <= M); + collector.addResult(context.getString(R.string.self_check_name_system_supported), + isSupported ? Result.Positive : Result.Unknown, context.getString(R.string.self_check_resolution_system_supported)); + return isSupported; + } + + private boolean checkSystemIsConfigured(Context context, ResultCollector collector) { + // 2.3+ com.android.internal.R.string.config_networkLocationProvider + // 4.1+ com.android.internal.R.string.config_networkLocationProviderPackageName + // 4.2+ com.android.internal.R.array.config_locationProviderPackageNames + // 4.3+ com.android.internal.R.array.config_locationProviderPackageNames / + // com.android.internal.R.string.config_networkLocationProviderPackageName / + // com.android.internal.R.bool.config_enableNetworkLocationOverlay + boolean systemMatchesPackage = false; + if (SDK_INT < JELLY_BEAN) { + systemMatchesPackage |= context.getPackageName().equals(getResourceString(context, CONFIG_NL_PROVIDER)); + } else { + boolean overlay = getResourceBool(context, CONFIG_ENABLE_NL_OVERLAY); + if (SDK_INT < JELLY_BEAN_MR1 || (SDK_INT > JELLY_BEAN_MR1 && !overlay)) { + systemMatchesPackage |= context.getPackageName().equals(getResourceString(context, CONFIG_NL_PROVIDER_PACKAGE_NAME)); + } + if (SDK_INT == JELLY_BEAN_MR1 || (SDK_INT > JELLY_BEAN_MR1 && overlay)) { + systemMatchesPackage |= Arrays.asList(getResourceArray(context, CONFIG_NL_PROVIDER_PACKAGE_NAMES)).contains(context.getPackageName()); + } + } + collector.addResult(context.getString(R.string.self_check_name_nlp_package_name), + systemMatchesPackage ? Result.Positive : Result.Negative, context.getString(R.string.self_check_resolution_nlp_package_name)); + return systemMatchesPackage; + } + + private String[] getResourceArray(Context context, String identifier) { + try { + int resId = context.getResources().getIdentifier(identifier, "array", "android"); + if (resId == 0) + resId = context.getResources().getIdentifier(identifier, "array", "com.android.internal"); + return context.getResources().getStringArray(resId); + } catch (Exception e) { + return new String[0]; + } + } + + private boolean getResourceBool(Context context, String identifier) { + try { + int resId = context.getResources().getIdentifier(identifier, "bool", "android"); + if (resId == 0) + resId = context.getResources().getIdentifier(identifier, "bool", "com.android.internal"); + return context.getResources().getBoolean(resId); + } catch (Exception e) { + return false; + } + } + + private String getResourceString(Context context, String identifier) { + try { + int resId = context.getResources().getIdentifier(identifier, "string", "android"); + if (resId == 0) + resId = context.getResources().getIdentifier(identifier, "string", "com.android.internal"); + return context.getString(resId); + } catch (Exception e) { + return null; + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/selfcheck/NlpStatusChecks.java b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/NlpStatusChecks.java new file mode 100644 index 0000000..149c65c --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/NlpStatusChecks.java @@ -0,0 +1,125 @@ +/* + * Copyright 2013-2016 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.selfcheck; + +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.text.TextUtils; + +import org.microg.nlp.app.R; +import org.microg.nlp.client.UnifiedLocationClient; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static android.content.Context.LOCATION_SERVICE; +import static android.location.LocationManager.NETWORK_PROVIDER; +import static org.microg.nlp.api.Constants.LOCATION_EXTRA_BACKEND_COMPONENT; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Negative; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Positive; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Unknown; + +public class NlpStatusChecks implements SelfCheckGroup { + @Override + public String getGroupName(Context context) { + return context.getString(R.string.self_check_cat_nlp_status); + } + + @Override + public void doChecks(Context context, ResultCollector collector) { + providerWasBound(context, collector); + if (isNetworkLocationEnabled(context, collector)) { + isProvidingLastLocation(context, collector); + isProvidingLocation(context, collector); + } + } + + private boolean providerWasBound(Context context, ResultCollector collector) { + collector.addResult(context.getString(R.string.self_check_name_nlp_bound), + UnifiedLocationClient.get(context).isAvailable() ? Positive : Negative, context.getString(R.string.self_check_resolution_nlp_bound)); + return UnifiedLocationClient.get(context).isAvailable(); + } + + private boolean isNetworkLocationEnabled(Context context, ResultCollector collector) { + LocationManager locationManager = (LocationManager) context.getSystemService(LOCATION_SERVICE); + boolean networkEnabled = locationManager.getProviders(true).contains(NETWORK_PROVIDER); + collector.addResult(context.getString(R.string.self_check_name_network_enabled), + networkEnabled ? Positive : Negative, context.getString(R.string.self_check_resolution_network_enabled)); + return networkEnabled; + } + + private boolean isProvidingLastLocation(Context context, ResultCollector collector) { + LocationManager locationManager = (LocationManager) context.getSystemService(LOCATION_SERVICE); + try { + Location location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + boolean hasKnown = location != null && location.getExtras() != null && + location.getExtras().containsKey(LOCATION_EXTRA_BACKEND_COMPONENT); + collector.addResult(context.getString(R.string.self_check_name_last_location), + hasKnown ? Positive : Unknown, context.getString(R.string.self_check_resolution_last_location)); + return hasKnown; + } catch (SecurityException e) { + collector.addResult(context.getString(R.string.self_check_name_last_location), Unknown, context.getString(R.string.self_check_loc_perm_missing)); + return false; + } + } + + private void isProvidingLocation(final Context context, final ResultCollector collector) { + final AtomicBoolean result = new AtomicBoolean(false); + LocationManager locationManager = (LocationManager) context.getSystemService(LOCATION_SERVICE); + new Thread(new Runnable() { + @Override + public void run() { + synchronized (result) { + try { + result.wait(10000); + } catch (InterruptedException e) { + } + collector.addResult(context.getString(R.string.self_check_name_nlp_is_providing), + result.get() ? Positive : Unknown, context.getString(R.string.self_check_resolution_nlp_is_providing)); + } + } + }).start(); + try { + locationManager.requestSingleUpdate(NETWORK_PROVIDER, new LocationListener() { + @Override + public void onLocationChanged(Location location) { + synchronized (result) { + result.set(location != null && location.getExtras() != null && + location.getExtras().containsKey(LOCATION_EXTRA_BACKEND_COMPONENT)); + result.notifyAll(); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + } + }, null); + } catch (SecurityException e) { + collector.addResult(context.getString(R.string.self_check_name_last_location), Unknown, context.getString(R.string.self_check_loc_perm_missing)); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/selfcheck/PermissionCheckGroup.java b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/PermissionCheckGroup.java new file mode 100644 index 0000000..9044675 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/PermissionCheckGroup.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.selfcheck; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.util.Log; + +import androidx.fragment.app.Fragment; + +import org.microg.nlp.app.R; + +import static android.os.Build.VERSION_CODES.M; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Negative; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Positive; + +@TargetApi(M) +public class PermissionCheckGroup implements SelfCheckGroup { + private static final String TAG = "SelfCheckPerms"; + + private String[] permissions; + + public PermissionCheckGroup(String... permissions) { + this.permissions = permissions; + } + + @Override + public String getGroupName(Context context) { + return context.getString(R.string.self_check_cat_permissions); + } + + @Override + public void doChecks(Context context, ResultCollector collector) { + for (String permission : permissions) { + doPermissionCheck(context, collector, permission); + } + } + + private void doPermissionCheck(Context context, ResultCollector collector, final String permission) { + PackageManager pm = context.getPackageManager(); + try { + PermissionInfo info = pm.getPermissionInfo(permission, 0); + CharSequence permLabel = info.loadLabel(pm); + collector.addResult(context.getString(R.string.self_check_name_permission, permLabel), + context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED ? Positive : Negative, + context.getString(R.string.self_check_resolution_permission), + fragment -> fragment.requestPermissions(new String[]{permission}, 0)); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, e); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/selfcheck/SelfCheckGroup.java b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/SelfCheckGroup.java new file mode 100644 index 0000000..6dd0e43 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/selfcheck/SelfCheckGroup.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.selfcheck; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +public interface SelfCheckGroup { + String getGroupName(Context context); + + void doChecks(Context context, ResultCollector collector); + + interface ResultCollector { + void addResult(String name, Result value, String resolution); + + void addResult(String name, Result value, String resolution, CheckResolver resolver); + } + + interface CheckResolver { + void tryResolve(Fragment fragment); + } + + enum Result { + Positive, Negative, Unknown + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractAboutFragment.java b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractAboutFragment.java new file mode 100644 index 0000000..46f95d8 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractAboutFragment.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.ui; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.microg.nlp.app.R; + +public abstract class AbstractAboutFragment extends Fragment { + + protected abstract void collectLibraries(List libraries); + + public static Drawable getIcon(Context context) { + try { + PackageManager pm = context.getPackageManager(); + return pm.getPackageInfo(context.getPackageName(), 0).applicationInfo.loadIcon(pm); + } catch (PackageManager.NameNotFoundException e) { + // Never happens, self package always exists! + throw new RuntimeException(e); + } + } + + public static String getAppName(Context context) { + try { + PackageManager pm = context.getPackageManager(); + CharSequence label = pm.getPackageInfo(context.getPackageName(), 0).applicationInfo.loadLabel(pm); + if (TextUtils.isEmpty(label)) return context.getPackageName(); + return label.toString().trim(); + } catch (PackageManager.NameNotFoundException e) { + // Never happens, self package always exists! + throw new RuntimeException(e); + } + } + + protected String getAppName() { + return getAppName(getContext()); + } + + public static String getLibVersion(String packageName) { + try { + String versionName = (String) Class.forName(packageName + ".BuildConfig").getField("VERSION_NAME").get(null); + if (TextUtils.isEmpty(versionName)) return ""; + return versionName.trim(); + } catch (Exception e) { + return ""; + } + } + + public static String getSelfVersion(Context context) { + return getLibVersion(context.getPackageName()); + } + + protected String getSelfVersion() { + return getSelfVersion(getContext()); + } + + protected String getSummary() { + return null; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View aboutRoot = inflater.inflate(R.layout.about_root, container, false); + ((ImageView) aboutRoot.findViewById(android.R.id.icon)).setImageDrawable(getIcon(getContext())); + ((TextView) aboutRoot.findViewById(android.R.id.title)).setText(getAppName()); + ((TextView) aboutRoot.findViewById(R.id.about_version)).setText(getString(R.string.about_version_str, getSelfVersion())); + String summary = getSummary(); + if (summary != null) { + ((TextView) aboutRoot.findViewById(android.R.id.summary)).setText(summary); + aboutRoot.findViewById(android.R.id.summary).setVisibility(View.VISIBLE); + } + + List libraries = new ArrayList(); + collectLibraries(libraries); + Collections.sort(libraries); + ((ListView) aboutRoot.findViewById(android.R.id.list)).setAdapter(new LibraryAdapter(getContext(), libraries.toArray(new Library[libraries.size()]))); + + return aboutRoot; + } + + private class LibraryAdapter extends ArrayAdapter { + + public LibraryAdapter(Context context, Library[] libraries) { + super(context, android.R.layout.simple_list_item_2, android.R.id.text1, libraries); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = super.getView(position, convertView, parent); + ((TextView) v.findViewById(android.R.id.text1)).setText(getString(R.string.about_name_version_str, getItem(position).name, getLibVersion(getItem(position).packageName))); + ((TextView) v.findViewById(android.R.id.text2)).setText(getItem(position).copyright != null ? getItem(position).copyright : getString(R.string.about_default_license)); + return v; + } + } + + protected static class Library implements Comparable { + private final String packageName; + private final String name; + private final String copyright; + + public Library(String packageName, String name, String copyright) { + this.packageName = packageName; + this.name = name; + this.copyright = copyright; + } + + @Override + public String toString() { + return name + ", " + copyright; + } + + @Override + public int compareTo(Library another) { + return name.toLowerCase(Locale.US).compareTo(another.name.toLowerCase(Locale.US)); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSelfCheckFragment.java b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSelfCheckFragment.java new file mode 100644 index 0000000..13f8fbf --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSelfCheckFragment.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.ui; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import org.microg.nlp.app.tools.selfcheck.SelfCheckGroup; + +import java.util.ArrayList; +import java.util.List; + +import static android.view.View.GONE; +import static android.view.View.INVISIBLE; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Negative; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Positive; +import static org.microg.nlp.app.tools.selfcheck.SelfCheckGroup.Result.Unknown; + +import org.microg.nlp.app.R; + +public abstract class AbstractSelfCheckFragment extends Fragment { + private static final String TAG = "SelfCheck"; + + private ViewGroup root; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View scrollRoot = inflater.inflate(R.layout.self_check, container, false); + root = (ViewGroup) scrollRoot.findViewById(R.id.self_check_root); + reset(inflater); + return scrollRoot; + } + + protected abstract void prepareSelfCheckList(List checks); + + protected void reset(LayoutInflater inflater) { + List selfCheckGroupList = new ArrayList(); + prepareSelfCheckList(selfCheckGroupList); + + root.removeAllViews(); + for (SelfCheckGroup group : selfCheckGroupList) { + View groupView = inflater.inflate(R.layout.self_check_group, root, false); + ((TextView) groupView.findViewById(android.R.id.title)).setText(group.getGroupName(getContext())); + final ViewGroup viewGroup = (ViewGroup) groupView.findViewById(R.id.group_content); + final SelfCheckGroup.ResultCollector collector = new GroupResultCollector(viewGroup); + try { + group.doChecks(getContext(), collector); + } catch (Exception e) { + Log.w(TAG, "Failed during check " + group.getGroupName(getContext()), e); + collector.addResult("Self-check failed:", Negative, "An exception occurred during self-check. Please report this issue."); + } + root.addView(groupView); + } + } + + private class GroupResultCollector implements SelfCheckGroup.ResultCollector { + private final ViewGroup viewGroup; + + public GroupResultCollector(ViewGroup viewGroup) { + this.viewGroup = viewGroup; + } + + @Override + public void addResult(final String name, final SelfCheckGroup.Result result, final String resolution) { + addResult(name, result, resolution, null); + } + + @Override + public void addResult(final String name, final SelfCheckGroup.Result result, final String resolution, + final SelfCheckGroup.CheckResolver resolver) { + if (result == null || getActivity() == null) return; + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + View resultEntry = LayoutInflater.from(getContext()).inflate(R.layout.self_check_entry, viewGroup, false); + ((TextView) resultEntry.findViewById(R.id.self_check_name)).setText(name); + resultEntry.findViewById(R.id.self_check_result).setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return true; + } + }); + if (result == Positive) { + ((CheckBox) resultEntry.findViewById(R.id.self_check_result)).setChecked(true); + resultEntry.findViewById(R.id.self_check_resolution).setVisibility(GONE); + } else { + ((TextView) resultEntry.findViewById(R.id.self_check_resolution)).setText(resolution); + if (result == Unknown) { + resultEntry.findViewById(R.id.self_check_result).setVisibility(INVISIBLE); + } + if (resolver != null) { + resultEntry.setClickable(true); + resultEntry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + resolver.tryResolve(AbstractSelfCheckFragment.this); + } + }); + } + } + viewGroup.addView(resultEntry); + } + }); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSettingsActivity.java b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSettingsActivity.java new file mode 100644 index 0000000..7de224f --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSettingsActivity.java @@ -0,0 +1,78 @@ +package org.microg.nlp.app.tools.ui; + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.ViewGroup; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import org.microg.nlp.app.R; + +public abstract class AbstractSettingsActivity extends AppCompatActivity { + protected boolean showHomeAsUp = false; + protected int preferencesResource = 0; + private ViewGroup customBarContainer; + protected int customBarLayout = 0; + protected SwitchBar switchBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.settings_activity); + if (showHomeAsUp) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + switchBar = (SwitchBar) findViewById(R.id.switch_bar); + + customBarContainer = (ViewGroup) findViewById(R.id.custom_bar); + if (customBarLayout != 0) { + customBarContainer.addView(getLayoutInflater().inflate(customBarLayout, customBarContainer, false)); + } + + getSupportFragmentManager().beginTransaction() + .replace(R.id.content_wrapper, getFragment()) + .commit(); + } + + public void setCustomBarLayout(int layout) { + customBarLayout = layout; + if (customBarContainer != null) { + customBarContainer.removeAllViews(); + customBarContainer.addView(getLayoutInflater().inflate(customBarLayout, customBarContainer, false)); + } + } + + public SwitchBar getSwitchBar() { + return switchBar; + } + + public void replaceFragment(Fragment fragment) { + getSupportFragmentManager().beginTransaction() + .addToBackStack("root") + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .replace(R.id.content_wrapper, fragment) + .commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + protected Fragment getFragment() { + if (preferencesResource == 0) { + throw new IllegalStateException("Neither preferencesResource given, nor overriden getFragment()"); + } + ResourceSettingsFragment fragment = new ResourceSettingsFragment(); + Bundle b = new Bundle(); + b.putInt(ResourceSettingsFragment.EXTRA_PREFERENCE_RESOURCE, preferencesResource); + fragment.setArguments(b); + return fragment; + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSettingsFragment.java b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSettingsFragment.java new file mode 100644 index 0000000..29e94f7 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/AbstractSettingsFragment.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.ui; + +import androidx.fragment.app.DialogFragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +public abstract class AbstractSettingsFragment extends PreferenceFragmentCompat { + private static final String TAG = AbstractSettingsFragment.class.getSimpleName(); + + private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; + + @Override + public void onDisplayPreferenceDialog(Preference preference) { + if (preference instanceof DialogPreference) { + DialogFragment f = DialogPreference.DialogPreferenceCompatDialogFragment.newInstance(preference.getKey()); + f.setTargetFragment(this, 0); + f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } else { + super.onDisplayPreferenceDialog(preference); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/DialogPreference.java b/app/src/main/java/org/microg/nlp/app/tools/ui/DialogPreference.java new file mode 100644 index 0000000..1410fa5 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/DialogPreference.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.ui; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.fragment.app.DialogFragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceDialogFragmentCompat; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceViewHolder; + +import org.microg.nlp.app.R; + +public class DialogPreference extends androidx.preference.DialogPreference implements PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback { + + private static final String DIALOG_FRAGMENT_TAG = + "android.support.v7.preference.PreferenceFragment.DIALOG"; + + public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public DialogPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialogPreference(Context context) { + super(context); + } + + protected View onCreateDialogView() { + return null; + } + + /** + * Called when the dialog is dismissed and should be used to save data to + * the {@link SharedPreferences}. + * + * @param positiveResult Whether the positive button was clicked (true), or + * the negative button was clicked or the dialog was canceled (false). + */ + protected void onDialogClosed(boolean positiveResult) { + } + + @Override + public boolean onPreferenceDisplayDialog(PreferenceFragmentCompat caller, Preference pref) { + DialogPreferenceCompatDialogFragment fragment = new DialogPreferenceCompatDialogFragment(); + fragment.setTargetFragment(caller, 0); + fragment.show(caller.getFragmentManager(), DIALOG_FRAGMENT_TAG); + return true; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + + ViewGroup.LayoutParams layoutParams = view.findViewById(R.id.icon_frame).getLayoutParams(); + if (layoutParams instanceof LinearLayout.LayoutParams) { + if (((LinearLayout.LayoutParams) layoutParams).leftMargin < 0) { + ((LinearLayout.LayoutParams) layoutParams).leftMargin = 0; + } + } + } + + public static class DialogPreferenceCompatDialogFragment extends PreferenceDialogFragmentCompat { + + @Override + protected View onCreateDialogView(Context context) { + if (getPreference() instanceof DialogPreference) { + View view = ((DialogPreference) getPreference()).onCreateDialogView(); + if (view != null) return view; + } + return super.onCreateDialogView(context); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + if (getPreference() instanceof DialogPreference) { + ((DialogPreference) getPreference()).onDialogClosed(positiveResult); + } + } + + public static DialogFragment newInstance(String key) { + final DialogPreferenceCompatDialogFragment fragment = new DialogPreferenceCompatDialogFragment(); + final Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/ResourceSettingsFragment.java b/app/src/main/java/org/microg/nlp/app/tools/ui/ResourceSettingsFragment.java new file mode 100644 index 0000000..3d978cb --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/ResourceSettingsFragment.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.ui; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +public class ResourceSettingsFragment extends AbstractSettingsFragment { + + public static final String EXTRA_PREFERENCE_RESOURCE = "preferencesResource"; + + protected int preferencesResource; + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + Bundle b = getArguments(); + if (b != null) { + preferencesResource = b.getInt(EXTRA_PREFERENCE_RESOURCE, preferencesResource); + } + if (preferencesResource != 0) { + addPreferencesFromResource(preferencesResource); + } + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/SwitchBar.java b/app/src/main/java/org/microg/nlp/app/tools/ui/SwitchBar.java new file mode 100644 index 0000000..3559ebd --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/SwitchBar.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2014-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.ui; + +import android.content.Context; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.appcompat.widget.SwitchCompat; + +import java.util.ArrayList; + +import static android.os.Build.VERSION.SDK_INT; +import org.microg.nlp.app.R; + +public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener, + View.OnClickListener { + + public static interface OnSwitchChangeListener { + /** + * Called when the checked state of the Switch has changed. + * + * @param switchView The Switch view whose state has changed. + * @param isChecked The new checked state of switchView. + */ + void onSwitchChanged(SwitchCompat switchView, boolean isChecked); + } + + private final TextAppearanceSpan mSummarySpan; + + private ToggleSwitch mSwitch; + private TextView mTextView; + private String mLabel; + private String mSummary; + + private ArrayList mSwitchChangeListeners = + new ArrayList(); + + public SwitchBar(Context context) { + this(context, null); + } + + public SwitchBar(Context context, AttributeSet attrs) { + super(context, attrs); + + LayoutInflater.from(context).inflate(R.layout.switch_bar, this); + + mTextView = (TextView) findViewById(R.id.switch_text); + if (SDK_INT > Build.VERSION_CODES.JELLY_BEAN) { + mTextView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + } + mLabel = getResources().getString(R.string.abc_capital_off); + mSummarySpan = new TextAppearanceSpan(context, androidx.appcompat.R.style.TextAppearance_AppCompat_Widget_Switch); + updateText(); + + mSwitch = (ToggleSwitch) findViewById(R.id.switch_widget); + // Prevent onSaveInstanceState() to be called as we are managing the state of the Switch + // on our own + mSwitch.setSaveEnabled(false); + if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mSwitch.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + } + + addOnSwitchChangeListener(new OnSwitchChangeListener() { + @Override + public void onSwitchChanged(SwitchCompat switchView, boolean isChecked) { + setTextViewLabel(isChecked); + } + }); + + setOnClickListener(this); + + // Default is hide + setVisibility(View.GONE); + } + + public void setTextViewLabel(boolean isChecked) { + mLabel = getResources() + .getString(isChecked ? R.string.abc_capital_on : R.string.abc_capital_off); + updateText(); + } + + public void setSummary(String summary) { + mSummary = summary; + updateText(); + } + + private void updateText() { + if (TextUtils.isEmpty(mSummary)) { + mTextView.setText(mLabel); + return; + } + final SpannableStringBuilder ssb = new SpannableStringBuilder(mLabel).append('\n'); + final int start = ssb.length(); + ssb.append(mSummary); + ssb.setSpan(mSummarySpan, start, ssb.length(), 0); + mTextView.setText(ssb); + } + + public void setChecked(boolean checked) { + setTextViewLabel(checked); + mSwitch.setChecked(checked); + } + + public void setCheckedInternal(boolean checked) { + setTextViewLabel(checked); + mSwitch.setCheckedInternal(checked); + } + + public boolean isChecked() { + return mSwitch.isChecked(); + } + + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mTextView.setEnabled(enabled); + mSwitch.setEnabled(enabled); + } + + public final ToggleSwitch getSwitch() { + return mSwitch; + } + + public void show() { + if (!isShowing()) { + setVisibility(View.VISIBLE); + mSwitch.setOnCheckedChangeListener(this); + } + } + + public void hide() { + if (isShowing()) { + setVisibility(View.GONE); + mSwitch.setOnCheckedChangeListener(null); + } + } + + public boolean isShowing() { + return (getVisibility() == View.VISIBLE); + } + + @Override + public void onClick(View v) { + final boolean isChecked = !mSwitch.isChecked(); + setChecked(isChecked); + } + + public void propagateChecked(boolean isChecked) { + final int count = mSwitchChangeListeners.size(); + for (int n = 0; n < count; n++) { + mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked); + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + propagateChecked(isChecked); + } + + public void addOnSwitchChangeListener(OnSwitchChangeListener listener) { + if (mSwitchChangeListeners.contains(listener)) { + throw new IllegalStateException("Cannot add twice the same OnSwitchChangeListener"); + } + mSwitchChangeListeners.add(listener); + } + + public void removeOnSwitchChangeListener(OnSwitchChangeListener listener) { + if (!mSwitchChangeListeners.contains(listener)) { + throw new IllegalStateException("Cannot remove OnSwitchChangeListener"); + } + mSwitchChangeListeners.remove(listener); + } + + static class SavedState extends BaseSavedState { + boolean checked; + boolean visible; + + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + checked = (Boolean) in.readValue(Boolean.class.getClassLoader()); + visible = (Boolean) in.readValue(Boolean.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeValue(checked); + out.writeValue(visible); + } + + @Override + public String toString() { + return "SwitchBar.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " checked=" + checked + + " visible=" + visible + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + ss.checked = mSwitch.isChecked(); + ss.visible = isShowing(); + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + + mSwitch.setCheckedInternal(ss.checked); + setTextViewLabel(ss.checked); + setVisibility(ss.visible ? View.VISIBLE : View.GONE); + mSwitch.setOnCheckedChangeListener(ss.visible ? this : null); + + requestLayout(); + } +} diff --git a/app/src/main/java/org/microg/nlp/app/tools/ui/ToggleSwitch.java b/app/src/main/java/org/microg/nlp/app/tools/ui/ToggleSwitch.java new file mode 100644 index 0000000..dcf3860 --- /dev/null +++ b/app/src/main/java/org/microg/nlp/app/tools/ui/ToggleSwitch.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2014-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.app.tools.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; + +import androidx.appcompat.widget.SwitchCompat; + +@SuppressLint("NewApi") +public class ToggleSwitch extends SwitchCompat { + + private ToggleSwitch.OnBeforeCheckedChangeListener mOnBeforeListener; + + public interface OnBeforeCheckedChangeListener { + boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked); + } + + public ToggleSwitch(Context context) { + super(context); + } + + public ToggleSwitch(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ToggleSwitch(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) { + mOnBeforeListener = listener; + } + + @Override + public void setChecked(boolean checked) { + if (mOnBeforeListener != null + && mOnBeforeListener.onBeforeCheckedChanged(this, checked)) { + return; + } + super.setChecked(checked); + } + + public void setCheckedInternal(boolean checked) { + super.setChecked(checked); + } +} diff --git a/app/src/main/kotlin/org/microg/nlp/app/SettingsFragment.kt b/app/src/main/kotlin/org/microg/nlp/app/SettingsFragment.kt new file mode 100644 index 0000000..ede1a49 --- /dev/null +++ b/app/src/main/kotlin/org/microg/nlp/app/SettingsFragment.kt @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.app + +import android.os.Bundle +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import org.microg.nlp.client.UnifiedLocationClient +import org.microg.nlp.app.tools.ui.ResourceSettingsFragment +import org.microg.nlp.ui.navigate + +class SettingsFragment : ResourceSettingsFragment() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + super.onCreatePreferences(savedInstanceState, rootKey) + + findPreference(PREF_UNIFIEDNLP)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openUnifiedNlpSettings) + true + } + findPreference(PREF_ABOUT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openAbout) + true + } + } + + override fun onResume() { + super.onResume() + lifecycleScope.launchWhenResumed { + updateDetails() + } + } + + private suspend fun updateDetails() { + val backendCount = UnifiedLocationClient[requireContext()].getLocationBackends().size + UnifiedLocationClient[requireContext()].getGeocoderBackends().size + findPreference(PREF_UNIFIEDNLP)!!.summary = resources.getQuantityString(R.plurals.pref_unifiednlp_summary, backendCount, backendCount); + } + + companion object { + const val PREF_ABOUT = "pref_about" + const val PREF_UNIFIEDNLP = "pref_unifiednlp" + } + + init { + preferencesResource = R.xml.preferences_start + } +} diff --git a/app/src/main/res/drawable/ic_info_outline.xml b/app/src/main/res/drawable/ic_info_outline.xml new file mode 100644 index 0000000..8e049cc --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_map_marker.xml b/app/src/main/res/drawable/ic_map_marker.xml new file mode 100644 index 0000000..b97f8b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/self_check.xml b/app/src/main/res/drawable/self_check.xml new file mode 100644 index 0000000..714b5e1 --- /dev/null +++ b/app/src/main/res/drawable/self_check.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/layout/about_root.xml b/app/src/main/res/layout/about_root.xml new file mode 100644 index 0000000..e563293 --- /dev/null +++ b/app/src/main/res/layout/about_root.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/preference_category_no_label.xml b/app/src/main/res/layout/preference_category_no_label.xml new file mode 100644 index 0000000..dafabb2 --- /dev/null +++ b/app/src/main/res/layout/preference_category_no_label.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/layout/self_check.xml b/app/src/main/res/layout/self_check.xml new file mode 100644 index 0000000..4037035 --- /dev/null +++ b/app/src/main/res/layout/self_check.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/self_check_entry.xml b/app/src/main/res/layout/self_check_entry.xml new file mode 100644 index 0000000..da3d743 --- /dev/null +++ b/app/src/main/res/layout/self_check_entry.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/self_check_group.xml b/app/src/main/res/layout/self_check_group.xml new file mode 100644 index 0000000..3f97b14 --- /dev/null +++ b/app/src/main/res/layout/self_check_group.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml new file mode 100644 index 0000000..6914f34 --- /dev/null +++ b/app/src/main/res/layout/settings_activity.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/settings_root_activity.xml b/app/src/main/res/layout/settings_root_activity.xml new file mode 100644 index 0000000..a592e38 --- /dev/null +++ b/app/src/main/res/layout/settings_root_activity.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/layout/switch_bar.xml b/app/src/main/res/layout/switch_bar.xml new file mode 100644 index 0000000..5907f4e --- /dev/null +++ b/app/src/main/res/layout/switch_bar.xml @@ -0,0 +1,43 @@ + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_nlp_app.png b/app/src/main/res/mipmap-hdpi/ic_nlp_app.png new file mode 100644 index 0000000..86a29e7 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_nlp_app.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_nlp_settings.png b/app/src/main/res/mipmap-hdpi/ic_nlp_settings.png new file mode 100644 index 0000000..c08d3f6 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_nlp_settings.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_nlp_app.png b/app/src/main/res/mipmap-mdpi/ic_nlp_app.png new file mode 100644 index 0000000..a7ae3c4 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_nlp_app.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_nlp_settings.png b/app/src/main/res/mipmap-mdpi/ic_nlp_settings.png new file mode 100644 index 0000000..6d3adc6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_nlp_settings.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_nlp_app.png b/app/src/main/res/mipmap-xhdpi/ic_nlp_app.png new file mode 100644 index 0000000..cd30228 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_nlp_app.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_nlp_settings.png b/app/src/main/res/mipmap-xhdpi/ic_nlp_settings.png new file mode 100644 index 0000000..35034cd Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_nlp_settings.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_nlp_app.png b/app/src/main/res/mipmap-xxhdpi/ic_nlp_app.png new file mode 100644 index 0000000..7296a1c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_nlp_app.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_nlp_settings.png b/app/src/main/res/mipmap-xxhdpi/ic_nlp_settings.png new file mode 100644 index 0000000..7fcda52 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_nlp_settings.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_nlp_app.png b/app/src/main/res/mipmap-xxxhdpi/ic_nlp_app.png new file mode 100644 index 0000000..57c386e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_nlp_app.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_nlp_settings.png b/app/src/main/res/mipmap-xxxhdpi/ic_nlp_settings.png new file mode 100644 index 0000000..b2ab77d Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_nlp_settings.png differ diff --git a/app/src/main/res/navigation/nav_settings.xml b/app/src/main/res/navigation/nav_settings.xml new file mode 100644 index 0000000..e318a3b --- /dev/null +++ b/app/src/main/res/navigation/nav_settings.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..1a92a94 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,20 @@ + + + + UnifiedNlp + Ustawienia UnifiedNlp + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000..07fd16b --- /dev/null +++ b/app/src/main/res/values-ro/strings.xml @@ -0,0 +1,20 @@ + + + + Nlp Unificat + Setări Nlp Unificat + diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000..9d7daab --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,42 @@ + + + + Обједињени МПЛ + Поставке Обједињеног МПЛ + + Подршка за Мрежне Провајдере Локације + Издање Андроида подржано: + Ваше издање Андроида није службено подржано. Ово нужно не мора да значи ништа. + Систем подржава провајдере локације: + Ваш систем не подржава овај пакет Обједињеног МПЛ. Или инсталирајте одговарајући пакет или Xposed модул за компатибилност. + + Стање Обједињеног МПЛ-а + Обједињени МПЛ је регистрован у систему: + Систем се није повезао са услугом Обједињеног МПЛ-а. Ако сте инсталирали Обједињени МПЛ покушајте са рестартом уређаја. + Обједињени МПЛ има познату локацију: + Обједињени МПЛ нема последњу познату локацију. Неке апликације ће краховати због овога. + Обједињени МПЛ пружа освежавања локације: + Систем није пружио ниједну локацију Обједињеног МПЛ-а у последњих 10 секунди. + Мрежна локација укључена: + Или сте искључили мрежну локацију (у поставкама система) или систем није подржан. + Дозвола за локацију још није одобрена + + О програму + О микроГ Обједињеном МПЛ-у + Подаци о издању и коришћеним библиотекама + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..738c356 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,19 @@ + + + + #ff37474f + diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml new file mode 100644 index 0000000..654e922 --- /dev/null +++ b/app/src/main/res/values/plurals.xml @@ -0,0 +1,23 @@ + + + + + + %1$d backend configured + %1$d backends configured + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..9d098a6 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,61 @@ + + + + UnifiedNlp + UnifiedNlp Settings + + + + Self-Check + Check if the system is correctly set up to use microG. + + Network location provider support + Android version supported: + Your Android version is not officially supported. This does not necessarily mean anything. + System supports location provider: + Your system does not support this UnifiedNlp package. Either install a matching package or a compatibility Xposed module. + + UnifiedNlp status + UnifiedNlp is registered in system: + The system did not bind the UnifiedNlp service. If you just installed UnifiedNlp you should try to reboot this device. + UnifiedNlp has known location: + UnifiedNlp has no last known location. This will cause some apps to fail. + UnifiedNlp provides location updates: + No UnifiedNlp location was provided by the system within 10 seconds. + Network-based location enabled: + You either disabled network-based location (in system settings) or the system is not supported. + Location permission not yet granted + + Permissions granted + Permission to %1$s: + Touch here to grant permission. Not granting the permission can result in misbehaving applications. + + + Version %1$s + %1$s %2$s + All rights reserved. + + UnifiedNlp UI Demo + Summary + Version v0.1.0 + Included libraries + + About + About microG UnifiedNlp + Version information and used libraries + + diff --git a/app/src/main/res/xml/preferences_start.xml b/app/src/main/res/xml/preferences_start.xml new file mode 100644 index 0000000..67e4765 --- /dev/null +++ b/app/src/main/res/xml/preferences_start.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt b/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt index 141a705..41e5305 100644 --- a/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt +++ b/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt @@ -501,6 +501,8 @@ class UnifiedLocationClient private constructor(context: Context) { } catch (e: Exception) { Log.w(TAG, "Failed to register location callback", e) } + context.get()?.resources?.getStringArray(R.array.force_location_backends)?.let { setLocationBackends(it + getLocationBackends()) } + context.get()?.resources?.getStringArray(R.array.force_geocoder_backends)?.let { setGeocoderBackends(it + getGeocoderBackends()) } updateServiceInterval() if (continuations.size > 0) { Log.d(TAG, "Resuming ${continuations.size} continuations") diff --git a/client/src/main/res/values/arrays.xml b/client/src/main/res/values/arrays.xml new file mode 100644 index 0000000..d71add4 --- /dev/null +++ b/client/src/main/res/values/arrays.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 9581e9d..65a2052 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,6 @@ include ':ui' //include ':service-app' //include ':client-app' -//include ':app' +include ':app' include ':docs:backend-sample'