diff --git a/microg-ui-tools/build.gradle b/microg-ui-tools/build.gradle new file mode 100644 index 00000000..1fb652d4 --- /dev/null +++ b/microg-ui-tools/build.gradle @@ -0,0 +1,53 @@ +/* + * 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. + */ + +apply plugin: 'com.android.library' + +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) +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + // TODO: Remove MissingTranslation once we have stable strings and proper translations. + disable 'MissingTranslation' + } +} + +dependencies { + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "androidx.preference:preference:$preferenceVersion" +} + diff --git a/microg-ui-tools/src/main/AndroidManifest.xml b/microg-ui-tools/src/main/AndroidManifest.xml new file mode 100644 index 00000000..004f299c --- /dev/null +++ b/microg-ui-tools/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/microg-ui-tools/src/main/java/org/microg/tools/selfcheck/PermissionCheckGroup.java b/microg-ui-tools/src/main/java/org/microg/tools/selfcheck/PermissionCheckGroup.java new file mode 100644 index 00000000..22543802 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/selfcheck/PermissionCheckGroup.java @@ -0,0 +1,77 @@ +/* + * 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.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.tools.ui.R; + +import static android.os.Build.VERSION_CODES.M; +import static org.microg.tools.selfcheck.SelfCheckGroup.Result.Negative; +import static org.microg.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); + PermissionGroupInfo groupInfo = info.group != null ? pm.getPermissionGroupInfo(info.group, 0) : null; + CharSequence permLabel = info.loadLabel(pm); + CharSequence groupLabel = groupInfo != null ? groupInfo.loadLabel(pm) : permLabel; + 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, groupLabel), + new SelfCheckGroup.CheckResolver() { + + @Override + public void tryResolve(Fragment fragment) { + fragment.requestPermissions(new String[]{permission}, 0); + } + }); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, e); + } + } +} diff --git a/microg-ui-tools/src/main/java/org/microg/tools/selfcheck/SelfCheckGroup.java b/microg-ui-tools/src/main/java/org/microg/tools/selfcheck/SelfCheckGroup.java new file mode 100644 index 00000000..13828574 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/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.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/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractAboutFragment.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractAboutFragment.java new file mode 100644 index 00000000..b23a57e4 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractAboutFragment.java @@ -0,0 +1,150 @@ +/* + * 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.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; + +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(); + libraries.add(new Library(BuildConfig.APPLICATION_ID, getString(R.string.lib_name), getString(R.string.lib_license))); + 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/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractDashboardActivity.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractDashboardActivity.java new file mode 100644 index 00000000..0bd9671c --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractDashboardActivity.java @@ -0,0 +1,116 @@ +package org.microg.tools.ui; + +import android.os.Bundle; +import android.view.ViewGroup; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AbstractDashboardActivity extends AppCompatActivity { + protected int preferencesResource = 0; + + private final List conditions = new ArrayList(); + private ViewGroup conditionContainer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.dashboard_activity); + conditionContainer = (ViewGroup) findViewById(R.id.condition_container); + + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + + getSupportFragmentManager().beginTransaction() + .replace(R.id.content_wrapper, getFragment()) + .commit(); + } + + @Override + protected void onResume() { + super.onResume(); + forceConditionReevaluation(); + } + + private synchronized void resetConditionViews() { + conditionContainer.removeAllViews(); + for (Condition condition : conditions) { + if (condition.isEvaluated()) { + if (condition.isActive(this)) { + addConditionToView(condition); + } + } else { + evaluateConditionAsync(condition); + } + } + } + + private void evaluateConditionAsync(final Condition condition) { + if (condition.willBeEvaluating()) { + new Thread(new Runnable() { + @Override + public void run() { + if (condition.isActive(AbstractDashboardActivity.this)) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (conditions.contains(condition) && condition.isEvaluated()) { + addConditionToView(condition); + } + } + }); + } + } + }).start(); + } + } + + protected void forceConditionReevaluation() { + for (Condition condition : conditions) { + condition.resetEvaluated(); + } + resetConditionViews(); + } + + protected void addAllConditions(Condition[] conditions) { + for (Condition condition : conditions) { + addCondition(condition); + } + } + + protected void addCondition(Condition condition) { + conditions.add(condition); + if (conditionContainer == null) return; + if (condition.isEvaluated()) { + addConditionToView(condition); + } else { + evaluateConditionAsync(condition); + } + } + + private synchronized void addConditionToView(Condition condition) { + for (int i = 0; i < conditionContainer.getChildCount(); i++) { + if (conditionContainer.getChildAt(i).getTag() == condition) return; + } + conditionContainer.addView(condition.createView(this, conditionContainer)); + } + + protected void clearConditions() { + conditions.clear(); + resetConditionViews(); + } + + 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/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSelfCheckFragment.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSelfCheckFragment.java new file mode 100644 index 00000000..ab05ab8f --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSelfCheckFragment.java @@ -0,0 +1,128 @@ +/* + * 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.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.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.tools.selfcheck.SelfCheckGroup.Result.Negative; +import static org.microg.tools.selfcheck.SelfCheckGroup.Result.Positive; +import static org.microg.tools.selfcheck.SelfCheckGroup.Result.Unknown; + +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/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSettingsActivity.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSettingsActivity.java new file mode 100644 index 00000000..b2a366f2 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSettingsActivity.java @@ -0,0 +1,80 @@ +package org.microg.tools.ui; + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.ViewGroup; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; + +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); + + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + 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/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSettingsFragment.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/AbstractSettingsFragment.java new file mode 100644 index 00000000..0f1967a5 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/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.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/microg-ui-tools/src/main/java/org/microg/tools/ui/Condition.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/Condition.java new file mode 100644 index 00000000..7ea35cd8 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/Condition.java @@ -0,0 +1,338 @@ +/* + * 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.tools.ui; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.PluralsRes; +import androidx.annotation.StringRes; +import androidx.core.content.res.ResourcesCompat; + +public class Condition { + @DrawableRes + private final int iconRes; + private final Drawable icon; + + @StringRes + private final int titleRes; + @PluralsRes + private final int titlePluralsRes; + private final CharSequence title; + + @StringRes + private final int summaryRes; + @PluralsRes + private final int summaryPluralsRes; + private final CharSequence summary; + + @StringRes + private final int firstActionTextRes; + @PluralsRes + private final int firstActionPluralsRes; + private final CharSequence firstActionText; + private final View.OnClickListener firstActionListener; + + @StringRes + private final int secondActionTextRes; + @PluralsRes + private final int secondActionPluralsRes; + private final CharSequence secondActionText; + private final View.OnClickListener secondActionListener; + + private final Evaluation evaluation; + + private boolean evaluated = false; + private boolean evaluating = false; + private int evaluatedPlurals = -1; + private boolean active; + + Condition(Builder builder) { + icon = builder.icon; + title = builder.title; + summary = builder.summary; + firstActionText = builder.firstActionText; + firstActionListener = builder.firstActionListener; + secondActionText = builder.secondActionText; + secondActionListener = builder.secondActionListener; + summaryRes = builder.summaryRes; + iconRes = builder.iconRes; + firstActionTextRes = builder.firstActionTextRes; + secondActionTextRes = builder.secondActionTextRes; + titleRes = builder.titleRes; + evaluation = builder.evaluation; + titlePluralsRes = builder.titlePluralsRes; + summaryPluralsRes = builder.summaryPluralsRes; + firstActionPluralsRes = builder.firstActionPluralsRes; + secondActionPluralsRes = builder.secondActionPluralsRes; + } + + View createView(final Context context, ViewGroup container) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.condition_card, container, false); + Drawable icon = getIcon(context); + if (icon != null) + ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(icon); + ((TextView) view.findViewById(android.R.id.title)).setText(getTitle(context)); + ((TextView) view.findViewById(android.R.id.summary)).setText(getSummary(context)); + Button first = (Button) view.findViewById(R.id.first_action); + first.setText(getFirstActionText(context)); + first.setOnClickListener(getFirstActionListener()); + CharSequence secondActionText = getSecondActionText(context); + if (secondActionText != null) { + Button second = (Button) view.findViewById(R.id.second_action); + second.setText(secondActionText); + second.setOnClickListener(getSecondActionListener()); + second.setVisibility(View.VISIBLE); + } + final View detailGroup = view.findViewById(R.id.detail_group); + final ImageView expandIndicator = (ImageView) view.findViewById(R.id.expand_indicator); + View.OnClickListener expandListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (detailGroup.getVisibility() == View.VISIBLE) { + expandIndicator.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_expand_more, context.getTheme())); + detailGroup.setVisibility(View.GONE); + } else { + expandIndicator.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_expand_less, context.getTheme())); + detailGroup.setVisibility(View.VISIBLE); + } + } + }; + view.findViewById(R.id.collapsed_group).setOnClickListener(expandListener); + expandIndicator.setOnClickListener(expandListener); + view.setTag(this); + return view; + } + + public Drawable getIcon(Context context) { + if (iconRes != 0) { + return ResourcesCompat.getDrawable(context.getResources(), iconRes, context.getTheme()); + } + return icon; + } + + public CharSequence getTitle(Context context) { + if (titleRes != 0) { + return context.getString(titleRes); + } + if (titlePluralsRes != 0) { + return context.getResources().getQuantityString(titlePluralsRes, evaluatedPlurals); + } + return title; + } + + public CharSequence getSummary(Context context) { + if (summaryRes != 0) { + return context.getString(summaryRes); + } + if (summaryPluralsRes != 0) { + return context.getResources().getQuantityString(summaryPluralsRes, evaluatedPlurals); + } + return summary; + } + + public View.OnClickListener getFirstActionListener() { + return firstActionListener; + } + + public CharSequence getFirstActionText(Context context) { + if (firstActionTextRes != 0) { + return context.getString(firstActionTextRes); + } + if (firstActionPluralsRes != 0) { + return context.getResources().getQuantityString(firstActionPluralsRes, evaluatedPlurals); + } + return firstActionText; + } + + public View.OnClickListener getSecondActionListener() { + return secondActionListener; + } + + public CharSequence getSecondActionText(Context context) { + if (secondActionTextRes != 0) { + return context.getString(secondActionTextRes); + } + if (secondActionPluralsRes != 0) { + return context.getResources().getQuantityString(secondActionPluralsRes, evaluatedPlurals); + } + return secondActionText; + } + + public synchronized boolean willBeEvaluating() { + if (!evaluating && !evaluated && evaluation != null) { + return evaluating = true; + } else { + return false; + } + } + + public boolean isEvaluated() { + return evaluated || evaluation == null; + } + + public synchronized void evaluate(Context context) { + active = evaluation == null || evaluation.isActive(context); + evaluatedPlurals = evaluation.getPluralsCount(); + evaluated = true; + evaluating = false; + } + + public boolean isActive(Context context) { + if (!evaluated && evaluation != null) evaluate(context); + return active; + } + + public void resetEvaluated() { + this.evaluated = false; + } + + public static abstract class Evaluation { + public abstract boolean isActive(Context context); + + public int getPluralsCount() { + return 1; + } + } + + public static class Builder { + + @DrawableRes + private int iconRes; + private Drawable icon; + @StringRes + private int titleRes; + @PluralsRes + private int titlePluralsRes; + private CharSequence title; + @StringRes + private int summaryRes; + @PluralsRes + private int summaryPluralsRes; + private CharSequence summary; + @StringRes + private int firstActionTextRes; + @PluralsRes + private int firstActionPluralsRes; + private CharSequence firstActionText; + private View.OnClickListener firstActionListener; + @StringRes + private int secondActionTextRes; + @PluralsRes + private int secondActionPluralsRes; + private CharSequence secondActionText; + private View.OnClickListener secondActionListener; + private Evaluation evaluation; + + + public Builder() { + } + + public Builder icon(Drawable val) { + icon = val; + return this; + } + + public Builder icon(@DrawableRes int val) { + iconRes = val; + return this; + } + + public Builder title(CharSequence val) { + title = val; + return this; + } + + public Builder title(@StringRes int val) { + titleRes = val; + return this; + } + + public Builder titlePlurals(@PluralsRes int val) { + titlePluralsRes = val; + return this; + } + + public Builder summary(CharSequence val) { + summary = val; + return this; + } + + public Builder summary(@StringRes int val) { + summaryRes = val; + return this; + } + + public Builder summaryPlurals(@PluralsRes int val) { + summaryPluralsRes = val; + return this; + } + + public Builder firstAction(CharSequence text, View.OnClickListener listener) { + firstActionText = text; + firstActionListener = listener; + return this; + } + + public Builder firstAction(@StringRes int val, View.OnClickListener listener) { + firstActionTextRes = val; + firstActionListener = listener; + return this; + } + + public Builder firstActionPlurals(@PluralsRes int val, View.OnClickListener listener) { + firstActionPluralsRes = val; + firstActionListener = listener; + return this; + } + + public Builder secondAction(CharSequence text, View.OnClickListener listener) { + secondActionText = text; + secondActionListener = listener; + return this; + } + + public Builder secondAction(@StringRes int val, View.OnClickListener listener) { + secondActionTextRes = val; + secondActionListener = listener; + return this; + } + + public Builder secondActionPlurals(@PluralsRes int val, View.OnClickListener listener) { + secondActionPluralsRes = val; + secondActionListener = listener; + return this; + } + + public Builder evaluation(Evaluation evaluation) { + this.evaluation = evaluation; + return this; + } + + public Condition build() { + return new Condition(this); + } + } +} diff --git a/microg-ui-tools/src/main/java/org/microg/tools/ui/DialogPreference.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/DialogPreference.java new file mode 100644 index 00000000..af07a691 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/DialogPreference.java @@ -0,0 +1,114 @@ +/* + * 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.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; + +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/microg-ui-tools/src/main/java/org/microg/tools/ui/DimmableIconPreference.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/DimmableIconPreference.java new file mode 100644 index 00000000..791cbd02 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/DimmableIconPreference.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.tools.ui; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +/** + * A preference item that can dim the icon when it's disabled, either directly or because its parent + * is disabled. + */ +public class DimmableIconPreference extends Preference { + private static final int ICON_ALPHA_ENABLED = 255; + private static final int ICON_ALPHA_DISABLED = 102; + + private final CharSequence mContentDescription; + + public DimmableIconPreference(Context context) { + this(context, (AttributeSet) null); + } + + public DimmableIconPreference(Context context, AttributeSet attrs) { + super(context, attrs); + mContentDescription = null; + } + + public DimmableIconPreference(Context context, CharSequence contentDescription) { + super(context); + mContentDescription = contentDescription; + } + + protected boolean shouldDimIcon() { + return !isEnabled(); + } + + private void dimIcon(boolean dimmed) { + Drawable icon = getIcon(); + if (icon != null) { + icon.mutate().setAlpha(dimmed ? ICON_ALPHA_DISABLED : ICON_ALPHA_ENABLED); + setIcon(icon); + } + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + if (!TextUtils.isEmpty(mContentDescription)) { + final TextView titleView = (TextView) view.findViewById(android.R.id.title); + titleView.setContentDescription(mContentDescription); + } + 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; + } + } + dimIcon(shouldDimIcon()); + } +} diff --git a/microg-ui-tools/src/main/java/org/microg/tools/ui/LongTextPreference.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/LongTextPreference.java new file mode 100644 index 00000000..4c6edb8b --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/LongTextPreference.java @@ -0,0 +1,52 @@ +/* + * 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.tools.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +public class LongTextPreference extends Preference { + + public LongTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public LongTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LongTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LongTextPreference(Context context) { + super(context); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + TextView view = (TextView) holder.findViewById(android.R.id.summary); + if (view != null) { + view.setMaxLines(Integer.MAX_VALUE); + } + } +} diff --git a/microg-ui-tools/src/main/java/org/microg/tools/ui/RadioButtonPreference.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/RadioButtonPreference.java new file mode 100644 index 00000000..e45d3812 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/RadioButtonPreference.java @@ -0,0 +1,46 @@ +/* + * 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.tools.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; + +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.CheckBoxPreference; + +public class RadioButtonPreference extends CheckBoxPreference { + + public RadioButtonPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RadioButtonPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setWidgetLayoutResource(R.layout.preference_widget_radiobutton); + } + + @SuppressLint("RestrictedApi") + public RadioButtonPreference(Context context, AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.checkBoxPreferenceStyle, + android.R.attr.checkBoxPreferenceStyle)); + } + + public RadioButtonPreference(Context context) { + this(context, null); + } +} diff --git a/microg-ui-tools/src/main/java/org/microg/tools/ui/ResourceSettingsFragment.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/ResourceSettingsFragment.java new file mode 100644 index 00000000..78abc781 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/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.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/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBar.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBar.java new file mode 100644 index 00000000..ab282dd5 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBar.java @@ -0,0 +1,265 @@ +/* + * 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.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; + +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/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBarResourceSettingsFragment.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBarResourceSettingsFragment.java new file mode 100644 index 00000000..950ac8b2 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBarResourceSettingsFragment.java @@ -0,0 +1,71 @@ +/* + * 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.tools.ui; + +import android.os.Bundle; + +import androidx.appcompat.widget.SwitchCompat; + +public abstract class SwitchBarResourceSettingsFragment extends ResourceSettingsFragment implements SwitchBar.OnSwitchChangeListener { + protected SwitchBar switchBar; + private SwitchCompat switchCompat; + private boolean listenerSetup = false; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + AbstractSettingsActivity activity = (AbstractSettingsActivity) getActivity(); + + switchBar = activity.getSwitchBar(); + switchBar.show(); + switchCompat = switchBar.getSwitch(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + switchBar.hide(); + } + + @Override + public void onResume() { + super.onResume(); + if (!listenerSetup) { + switchBar.addOnSwitchChangeListener(this); + listenerSetup = true; + } + } + + @Override + public void onPause() { + if (listenerSetup) { + switchBar.removeOnSwitchChangeListener(this); + listenerSetup = false; + } + super.onPause(); + } + + @Override + public void onSwitchChanged(SwitchCompat switchView, boolean isChecked) { + if (switchView == switchCompat) { + onSwitchBarChanged(isChecked); + } + } + + public abstract void onSwitchBarChanged(boolean isChecked); +} diff --git a/microg-ui-tools/src/main/java/org/microg/tools/ui/TintIconPreference.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/TintIconPreference.java new file mode 100644 index 00000000..05486b9c --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/tools/ui/TintIconPreference.java @@ -0,0 +1,45 @@ +package org.microg.tools.ui; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.TypedValue; + +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.preference.PreferenceViewHolder; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; + +public class TintIconPreference extends DimmableIconPreference { + + public TintIconPreference(Context context) { + this(context, (AttributeSet) null); + } + + public TintIconPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private static int getThemeAccentColor(Context context) { + int colorAttr; + if (SDK_INT >= LOLLIPOP) { + colorAttr = android.R.attr.colorAccent; + } else { + //Get colorAccent defined for AppCompat + colorAttr = context.getResources().getIdentifier("colorAccent", "attr", context.getPackageName()); + } + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(colorAttr, outValue, true); + return outValue.data; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + Drawable icon = getIcon(); + if (icon != null) { + DrawableCompat.setTint(icon, getThemeAccentColor(getContext())); + } + } +} diff --git a/microg-ui-tools/src/main/java/org/microg/tools/ui/ToggleSwitch.java b/microg-ui-tools/src/main/java/org/microg/tools/ui/ToggleSwitch.java new file mode 100644 index 00000000..96247305 --- /dev/null +++ b/microg-ui-tools/src/main/java/org/microg/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.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/microg-ui-tools/src/main/res/drawable-v21/switchbar_background.xml b/microg-ui-tools/src/main/res/drawable-v21/switchbar_background.xml new file mode 100644 index 00000000..20909c3a --- /dev/null +++ b/microg-ui-tools/src/main/res/drawable-v21/switchbar_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/microg-ui-tools/src/main/res/drawable/empty.xml b/microg-ui-tools/src/main/res/drawable/empty.xml new file mode 100644 index 00000000..07d028d2 --- /dev/null +++ b/microg-ui-tools/src/main/res/drawable/empty.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/microg-ui-tools/src/main/res/drawable/ic_expand_less.xml b/microg-ui-tools/src/main/res/drawable/ic_expand_less.xml new file mode 100644 index 00000000..62ff063e --- /dev/null +++ b/microg-ui-tools/src/main/res/drawable/ic_expand_less.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/microg-ui-tools/src/main/res/drawable/ic_expand_more.xml b/microg-ui-tools/src/main/res/drawable/ic_expand_more.xml new file mode 100644 index 00000000..871f6575 --- /dev/null +++ b/microg-ui-tools/src/main/res/drawable/ic_expand_more.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/microg-ui-tools/src/main/res/drawable/self_check.xml b/microg-ui-tools/src/main/res/drawable/self_check.xml new file mode 100644 index 00000000..92b47890 --- /dev/null +++ b/microg-ui-tools/src/main/res/drawable/self_check.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/microg-ui-tools/src/main/res/drawable/switchbar_background.xml b/microg-ui-tools/src/main/res/drawable/switchbar_background.xml new file mode 100644 index 00000000..9c5aba9c --- /dev/null +++ b/microg-ui-tools/src/main/res/drawable/switchbar_background.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/microg-ui-tools/src/main/res/layout-v14/preference_category_dashboard.xml b/microg-ui-tools/src/main/res/layout-v14/preference_category_dashboard.xml new file mode 100644 index 00000000..45d12ba7 --- /dev/null +++ b/microg-ui-tools/src/main/res/layout-v14/preference_category_dashboard.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/microg-ui-tools/src/main/res/layout-v21/preference_material.xml b/microg-ui-tools/src/main/res/layout-v21/preference_material.xml new file mode 100644 index 00000000..3f7906ae --- /dev/null +++ b/microg-ui-tools/src/main/res/layout-v21/preference_material.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/microg-ui-tools/src/main/res/layout/about_root.xml b/microg-ui-tools/src/main/res/layout/about_root.xml new file mode 100644 index 00000000..e563293c --- /dev/null +++ b/microg-ui-tools/src/main/res/layout/about_root.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + diff --git a/microg-ui-tools/src/main/res/layout/app_bar.xml b/microg-ui-tools/src/main/res/layout/app_bar.xml new file mode 100644 index 00000000..02d68ae3 --- /dev/null +++ b/microg-ui-tools/src/main/res/layout/app_bar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/microg-ui-tools/src/main/res/layout/condition_card.xml b/microg-ui-tools/src/main/res/layout/condition_card.xml new file mode 100644 index 00000000..d18d9e1d --- /dev/null +++ b/microg-ui-tools/src/main/res/layout/condition_card.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +