aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuutaW <17158086+Trumeet@users.noreply.github.com>2019-02-24 11:59:17 -0800
committerYuutaW <17158086+Trumeet@users.noreply.github.com>2019-02-24 11:59:17 -0800
commita08328403be84d85c006f801169a3feed0d956a4 (patch)
treeceebece6443a3e6662a4937b911c58904bb5b1ff
downloadWorkMode-a08328403be84d85c006f801169a3feed0d956a4.tar
WorkMode-a08328403be84d85c006f801169a3feed0d956a4.tar.gz
WorkMode-a08328403be84d85c006f801169a3feed0d956a4.tar.bz2
WorkMode-a08328403be84d85c006f801169a3feed0d956a4.zip
First Commit
Signed-off-by: YuutaW <17158086+Trumeet@users.noreply.github.com>
-rw-r--r--.gitignore9
-rw-r--r--app/.gitignore1
-rw-r--r--app/build.gradle56
-rw-r--r--app/proguard-rules.pro21
-rw-r--r--app/src/main/AndroidManifest.xml58
-rw-r--r--app/src/main/java/com/android/settings/widget/SwitchBar.java309
-rw-r--r--app/src/main/java/com/android/settings/widget/ToggleSwitch.java63
-rw-r--r--app/src/main/java/moe/yuuta/workmode/App.kt10
-rw-r--r--app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt222
-rw-r--r--app/src/main/java/moe/yuuta/workmode/MainActivity.kt406
-rw-r--r--app/src/main/java/moe/yuuta/workmode/MoreDetailsActivity.kt12
-rw-r--r--app/src/main/java/moe/yuuta/workmode/Setup.kt64
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt86
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt199
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt9
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt7
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt9
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt317
-rw-r--r--app/src/main/java/moe/yuuta/workmode/async/Async.kt47
-rw-r--r--app/src/main/java/moe/yuuta/workmode/async/Callback.kt6
-rw-r--r--app/src/main/java/moe/yuuta/workmode/async/Stoppable.kt6
-rw-r--r--app/src/main/java/moe/yuuta/workmode/async/StoppableGroup.kt28
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt27
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt30
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt38
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt40
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/data/ListMode.kt6
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/data/Status.kt6
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt68
-rw-r--r--app/src/main/java/moe/yuuta/workmode/update/UpdateChecker.kt70
-rw-r--r--app/src/main/java/moe/yuuta/workmode/utils/BundleUtils.java89
-rw-r--r--app/src/main/java/moe/yuuta/workmode/utils/ByteArraySerializer.kt26
-rw-r--r--app/src/main/java/moe/yuuta/workmode/utils/Utils.kt127
-rw-r--r--app/src/main/res/color/switchbar_switch_thumb_tint.xml17
-rw-r--r--app/src/main/res/color/switchbar_switch_track_tint.xml18
-rw-r--r--app/src/main/res/drawable-v24/ic_launcher_foreground.xml34
-rw-r--r--app/src/main/res/drawable/ic_add_24dp.xml5
-rw-r--r--app/src/main/res/drawable/ic_check_24dp.xml5
-rw-r--r--app/src/main/res/drawable/ic_launcher_background.xml170
-rw-r--r--app/src/main/res/drawable/ic_work_24dp.xml5
-rw-r--r--app/src/main/res/layout/activity_application_picker.xml33
-rw-r--r--app/src/main/res/layout/activity_main.xml91
-rw-r--r--app/src/main/res/layout/item_application.xml31
-rw-r--r--app/src/main/res/layout/item_application_select.xml42
-rw-r--r--app/src/main/res/layout/switch_bar.xml51
-rw-r--r--app/src/main/res/menu/menu_main.xml23
-rw-r--r--app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2963 bytes
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 4905 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2060 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2783 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4490 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 6895 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6387 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 10413 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9128 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 15132 bytes
-rw-r--r--app/src/main/res/values/attrs.xml8
-rw-r--r--app/src/main/res/values/colors.xml8
-rw-r--r--app/src/main/res/values/dimens.xml8
-rw-r--r--app/src/main/res/values/strings.xml27
-rw-r--r--app/src/main/res/values/styles.xml35
-rw-r--r--app/src/main/res/xml/filepaths.xml6
-rw-r--r--build.gradle29
-rw-r--r--gradle.properties21
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 54329 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew172
-rw-r--r--gradlew.bat84
-rw-r--r--settings.gradle1
71 files changed, 3412 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ef5a0e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..e7ddaed
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,56 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
+apply plugin: 'com.google.android.gms.oss-licenses-plugin'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "moe.yuuta.workmode"
+ minSdkVersion 28
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ lintOptions {
+ checkReleaseBuilds false
+ abortOnError false
+ }
+ kapt {
+ generateStubs = true
+ }
+ sourceSets {
+ main {
+ java.srcDirs += 'src/main/kotlin'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.core:core-ktx:1.0.1'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+ implementation 'eu.chainfire:librootjava:1.2.0'
+ implementation 'com.elvishew:xlog:1.6.1'
+ implementation 'eu.chainfire:libsuperuser:1.0.0.+'
+ implementation 'com.google.android.material:material:1.1.0-alpha03'
+ implementation 'androidx.recyclerview:recyclerview:1.0.0'
+ implementation 'com.google.android.gms:play-services-oss-licenses:16.0.2'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..435ee2b
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="moe.yuuta.workmode">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:name=".App"
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ tools:ignore="GoogleAppIndexingWarning">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity android:name=".MoreDetailsActivity"
+ android:label="MoreDetails"
+ android:noHistory="true"
+ android:permission="android.permission.SEND_SHOW_SUSPENDED_APP_DETAILS">
+ <intent-filter>
+ <action android:name="android.intent.action.SHOW_SUSPENDED_APP_DETAILS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity android:name=".ApplicationPickerActivity"
+ android:label="@string/add" />
+ <service
+ android:name=".suspend.SuspendTile"
+ android:icon="@drawable/ic_work_24dp"
+ android:label="@string/work"
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+ <intent-filter>
+ <action
+ android:name="android.service.quicksettings.action.QS_TILE"/>
+ </intent-filter>
+ <meta-data
+ android:name="android.service.quicksettings.ACTIVE_TILE"
+ android:value="true" />
+ </service>
+ <provider
+ android:name="androidx.core.content.FileProvider"
+ android:authorities="${applicationId}.fileprovider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/filepaths" />
+ </provider>
+ </application>
+</manifest> \ No newline at end of file
diff --git a/app/src/main/java/com/android/settings/widget/SwitchBar.java b/app/src/main/java/com/android/settings/widget/SwitchBar.java
new file mode 100644
index 0000000..fe82949
--- /dev/null
+++ b/app/src/main/java/com/android/settings/widget/SwitchBar.java
@@ -0,0 +1,309 @@
+/*
+ * 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 com.android.settings.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+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.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import moe.yuuta.workmode.R;
+
+public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener {
+
+ public 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(Switch switchView, boolean isChecked);
+ }
+
+ private static final int[] XML_ATTRIBUTES = {
+ R.attr.switchBarMarginStart,
+ R.attr.switchBarMarginEnd,
+ R.attr.switchBarBackgroundColor,
+ R.attr.switchBarBackgroundActivatedColor};
+
+ private final List<OnSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
+ private final TextAppearanceSpan mSummarySpan;
+
+ private ToggleSwitch mSwitch;
+ private TextView mTextView;
+ private String mLabel;
+ private String mSummary;
+ @ColorInt
+ private int mBackgroundColor;
+ @ColorInt
+ private int mBackgroundActivatedColor;
+ @StringRes
+ private int mOnTextId;
+ @StringRes
+ private int mOffTextId;
+
+ public SwitchBar(Context context) {
+ this(context, null);
+ }
+
+ public SwitchBar(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ LayoutInflater.from(context).inflate(R.layout.switch_bar, this);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, XML_ATTRIBUTES);
+ int switchBarMarginStart = (int) a.getDimension(0, 0);
+ int switchBarMarginEnd = (int) a.getDimension(1, 0);
+ mBackgroundColor = a.getColor(2, 0);
+ mBackgroundActivatedColor = a.getColor(3, 0);
+ a.recycle();
+
+ mTextView = findViewById(R.id.switch_text);
+ mSummarySpan = new TextAppearanceSpan(getContext(), R.style.TextAppearance_Small_SwitchBar);
+ ViewGroup.MarginLayoutParams lp = (MarginLayoutParams) mTextView.getLayoutParams();
+ lp.setMarginStart(switchBarMarginStart);
+
+ mSwitch = 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);
+
+ lp = (MarginLayoutParams) mSwitch.getLayoutParams();
+ lp.setMarginEnd(switchBarMarginEnd);
+ setBackgroundColor(mBackgroundColor);
+
+ setSwitchBarText(R.string.switch_on_text, R.string.switch_off_text);
+
+ addOnSwitchChangeListener(
+ (switchView, isChecked) -> setTextViewLabelAndBackground(isChecked));
+
+ // Default is hide
+ setVisibility(View.GONE);
+ }
+
+ public void setTextViewLabelAndBackground(boolean isChecked) {
+ mLabel = getResources().getString(isChecked ? mOnTextId : mOffTextId);
+ setBackgroundColor(isChecked ? mBackgroundActivatedColor : mBackgroundColor);
+ updateText();
+ }
+
+ public void setSwitchBarText(int onText, int offText) {
+ mOnTextId = onText;
+ mOffTextId = offText;
+ setTextViewLabelAndBackground(isChecked());
+ }
+
+ 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) {
+ setTextViewLabelAndBackground(checked);
+ mSwitch.setChecked(checked);
+ }
+
+ public void setCheckedInternal(boolean checked) {
+ setTextViewLabelAndBackground(checked);
+ mSwitch.setCheckedInternal(checked);
+ }
+
+ public boolean isChecked() {
+ return mSwitch.isChecked();
+ }
+
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mTextView.setEnabled(enabled);
+ mSwitch.setEnabled(enabled);
+ }
+
+ @VisibleForTesting
+ View getDelegatingView() {
+ return mSwitch;
+ }
+
+ public final ToggleSwitch getSwitch() {
+ return mSwitch;
+ }
+
+ public void show() {
+ if (!isShowing()) {
+ setVisibility(View.VISIBLE);
+ mSwitch.setOnCheckedChangeListener(this);
+ // Make the entire bar work as a switch
+ post(() -> setTouchDelegate(
+ new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()),
+ getDelegatingView())));
+ }
+ }
+
+ public void hide() {
+ if (isShowing()) {
+ setVisibility(View.GONE);
+ mSwitch.setOnCheckedChangeListener(null);
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if ((w > 0) && (h > 0)) {
+ setTouchDelegate(new TouchDelegate(new Rect(0, 0, w, h),
+ getDelegatingView()));
+ }
+ }
+
+ public boolean isShowing() {
+ return (getVisibility() == View.VISIBLE);
+ }
+
+ 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)) {
+ return;
+ // 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(null);
+ visible = (Boolean) in.readValue(null);
+ }
+
+ @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<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ 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);
+ setTextViewLabelAndBackground(ss.checked);
+ setVisibility(ss.visible ? View.VISIBLE : View.GONE);
+ mSwitch.setOnCheckedChangeListener(ss.visible ? this : null);
+
+ requestLayout();
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/android/settings/widget/ToggleSwitch.java b/app/src/main/java/com/android/settings/widget/ToggleSwitch.java
new file mode 100644
index 0000000..3c0c36e
--- /dev/null
+++ b/app/src/main/java/com/android/settings/widget/ToggleSwitch.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2013 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 com.android.settings.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Switch;
+
+public class ToggleSwitch extends Switch {
+
+ 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 ToggleSwitch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ 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);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/App.kt b/app/src/main/java/moe/yuuta/workmode/App.kt
new file mode 100644
index 0000000..3735a3e
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/App.kt
@@ -0,0 +1,10 @@
+package moe.yuuta.workmode
+
+import android.app.Application
+
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ Setup.initLogs(Setup.getLogsPath(this).absolutePath)
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt b/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt
new file mode 100644
index 0000000..a5e3d0c
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt
@@ -0,0 +1,222 @@
+package moe.yuuta.workmode
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.util.LruCache
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.elvishew.xlog.Logger
+import com.elvishew.xlog.XLog
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import moe.yuuta.workmode.async.Async
+import moe.yuuta.workmode.async.Callback
+import moe.yuuta.workmode.async.Runnable
+import moe.yuuta.workmode.async.StoppableGroup
+import moe.yuuta.workmode.utils.Utils
+import java.util.stream.Collectors
+
+class ApplicationPickerActivity : AppCompatActivity() {
+ companion object {
+ const val EXTRA_SELECTED_PACKAGE_NAME = "moe.yuuta.workmode.ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME"
+ }
+ private val logger: Logger = XLog.tag("ApplicationPickerActivity").build()
+
+ private lateinit var mAdapter: Adapter
+ private val mStoppableGroup: StoppableGroup = StoppableGroup()
+ private lateinit var mProgressBar: ProgressBar
+ private lateinit var fab: FloatingActionButton
+
+ private fun setResultAndFinish(packageNames: Array<String>?) {
+ setResult(if (packageNames == null) Activity.RESULT_CANCELED else Activity.RESULT_OK,
+ Intent().putExtra(EXTRA_SELECTED_PACKAGE_NAME, packageNames ?: arrayOf()))
+ finish()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_application_picker)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ val recyclerView: RecyclerView = findViewById(R.id.recycler_apps)
+ fab = findViewById(R.id.fab_ok)
+ mProgressBar = findViewById(R.id.progress_load)
+ recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
+ mAdapter = Adapter()
+ recyclerView.adapter = mAdapter
+ fab.setOnClickListener {
+ setResultAndFinish(mAdapter.checked.toTypedArray())
+ }
+ load()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun load() {
+ mStoppableGroup.add(Async.beginTask(object : Runnable<List<SelectedApp>> {
+ override fun run(): List<SelectedApp> {
+ val selected = intent.getStringArrayExtra(EXTRA_SELECTED_PACKAGE_NAME) ?: arrayOf()
+ return packageManager.getInstalledApplications(0)
+ .stream()
+ .filter(Utils.buildGeneralApplicationInfoFilter(this@ApplicationPickerActivity))
+ .sorted(ApplicationInfo.DisplayNameComparator(packageManager))
+ .map {
+ return@map SelectedApp(it.packageName, selected.contains(it.packageName))
+ }
+ .collect(Collectors.toList())
+ }
+ }, object : Callback<List<SelectedApp>> {
+ override fun onStart() {
+ mProgressBar.visibility = View.VISIBLE
+ fab.visibility = View.GONE
+ }
+
+ override fun onStop(success: Boolean, result: List<SelectedApp>?, e: Throwable?) {
+ mProgressBar.visibility = View.GONE
+ fab.visibility = View.VISIBLE
+ if (success && result != null) {
+ display(result)
+ } else {
+ Toast.makeText(this@ApplicationPickerActivity,
+ R.string.error_load_applications, Toast.LENGTH_LONG)
+ .show()
+ if (e != null) {
+ logger.e("Load applications", e)
+ } else {
+ logger.e("Cannot load applications (no stacktrace)")
+ }
+ // Not sure if the toast will dismiss immediately
+ // setResultAndFinish()
+ }
+ }
+ }))
+ }
+
+ private fun display(result: List<SelectedApp>) {
+ val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
+ override fun getOldListSize(): Int = mAdapter.itemCount
+
+ override fun getNewListSize(): Int = result.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ mAdapter.data[oldItemPosition]::class.java.name ==
+ result[newItemPosition]::class.java.name
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ mAdapter.data[oldItemPosition] ==
+ result[newItemPosition]
+ })
+ mAdapter.data = result
+ diff.dispatchUpdatesTo(mAdapter)
+ }
+
+ override fun onDestroy() {
+ mStoppableGroup.stop()
+ mAdapter.destroy()
+ super.onDestroy()
+ }
+
+ private class Adapter : RecyclerView.Adapter<Adapter.VH>() {
+ private val mIconMemoryCaches: LruCache<String, Drawable> =
+ LruCache(Runtime.getRuntime().maxMemory().toInt() / 5)
+ private val mStoppableGroup: StoppableGroup = StoppableGroup()
+ internal var data: List<SelectedApp> = listOf()
+ internal val checked: MutableSet<String> = mutableSetOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
+ VH(LayoutInflater.from(parent.context).inflate(R.layout.item_application_select, parent, false))
+
+ override fun getItemCount(): Int = data.size
+
+ override fun onBindViewHolder(holder: VH, position: Int) {
+ val context = holder.itemView.context
+ val packageInfo = data[position]
+ val icon = getIconFromMemoryCache(packageInfo.packageName)
+ if (icon != null) {
+ holder.icon.setImageDrawable(icon)
+ } else {
+ loadIcon(packageInfo.packageName, holder.itemView.context, holder.icon)
+ }
+ holder.title.text = context
+ .packageManager
+ .getApplicationLabel(
+ context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
+ )
+ if (packageInfo.selected) checked.add(packageInfo.packageName)
+ else checked.remove(packageInfo.packageName)
+ holder.checkBox.isChecked = checked.contains(packageInfo.packageName)
+ holder.checkBox.setOnClickListener {
+ val selected = holder.checkBox.isChecked
+ if (selected) checked.add(packageInfo.packageName)
+ else checked.remove(packageInfo.packageName)
+ }
+ }
+
+ class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val icon: ImageView = itemView.findViewById(android.R.id.icon)
+ internal val title: TextView = itemView.findViewById(android.R.id.title)
+ internal val checkBox: CheckBox = itemView.findViewById(android.R.id.checkbox)
+ }
+
+ private fun addDrawableToMemoryCache(pkg: String?, icon: Drawable) {
+ if (getIconFromMemoryCache(pkg) == null) {
+ mIconMemoryCaches.put(pkg ?: "", icon)
+ }
+ }
+
+ private fun getIconFromMemoryCache(pkg: String?): Drawable? {
+ return mIconMemoryCaches.get(pkg ?: "")
+ }
+
+ private fun loadIcon(pkg: String, context: Context, imageView: ImageView) {
+ mStoppableGroup.add(Async.beginTask(object : Runnable<Drawable> {
+ override fun run(): Drawable {
+ var icon: Drawable?
+ try {
+ icon = context.packageManager
+ .getApplicationIcon(pkg)
+ } catch (ignore: PackageManager.NameNotFoundException) {
+ icon = null
+ }
+ if (icon == null) {
+ icon = ContextCompat.getDrawable(context, android.R.mipmap.sym_def_app_icon)!!
+ }
+ addDrawableToMemoryCache(pkg, icon)
+ return icon
+ }
+ }, object : Callback<Drawable> {
+ override fun onStop(success: Boolean, result: Drawable?, e: Throwable?) {
+ if (success && result != null) imageView.setImageDrawable(result)
+ }
+ }))
+ }
+
+ fun destroy() {
+ mStoppableGroup.stop()
+ }
+ }
+}
+
+private data class SelectedApp(
+ val packageName: String,
+ val selected: Boolean
+) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/MainActivity.kt b/app/src/main/java/moe/yuuta/workmode/MainActivity.kt
new file mode 100644
index 0000000..b1d425b
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/MainActivity.kt
@@ -0,0 +1,406 @@
+package moe.yuuta.workmode
+
+import android.app.Activity
+import android.content.*
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.service.quicksettings.TileService
+import android.util.Log
+import android.util.LruCache
+import android.view.*
+import android.widget.*
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.settings.widget.SwitchBar
+import com.elvishew.xlog.Logger
+import com.elvishew.xlog.XLog
+import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.tabs.TabLayout
+import moe.yuuta.workmode.access.AccessorStarter
+import moe.yuuta.workmode.async.*
+import moe.yuuta.workmode.suspend.AsyncSuspender
+import moe.yuuta.workmode.suspend.SuspendTile
+import moe.yuuta.workmode.suspend.data.ListMode
+import moe.yuuta.workmode.suspend.data.Status
+import moe.yuuta.workmode.suspend.data.SuspendedStorage
+import moe.yuuta.workmode.update.Update
+import moe.yuuta.workmode.update.UpdateChecker
+import moe.yuuta.workmode.utils.Utils
+import java.util.stream.Collectors
+
+class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View.OnClickListener {
+ private val logger: Logger = XLog.tag("MainActivity").build()
+
+ companion object {
+ const val RC_PICK = 1
+ }
+
+ private lateinit var mAdapter: Adapter
+
+ private lateinit var switchBar: SwitchBar
+ private lateinit var progressBar: ProgressBar
+ private lateinit var tabLayout: TabLayout
+ private lateinit var welcomeTip: TextView
+
+ private val mStoppableGroup: StoppableGroup = StoppableGroup()
+ private var mSortDisplayStoppable: Stoppable? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ switchBar = findViewById(R.id.switch_bar)
+ switchBar.show()
+ welcomeTip = findViewById(R.id.welcome_tip)
+ progressBar = findViewById(R.id.progress_apply)
+ tabLayout = findViewById(R.id.tab)
+ tabLayout.addTab(tabLayout.newTab().setTag("blacklist").setText(R.string.blacklist))
+ tabLayout.addTab(tabLayout.newTab().setTag("whitelist").setText(R.string.whitelist))
+ val fab: FloatingActionButton = findViewById(R.id.fab_add)
+ fab.setOnClickListener(this)
+ val recyclerView: RecyclerView = findViewById(R.id.recycler_apps)
+ recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
+ mAdapter = Adapter()
+ recyclerView.adapter = mAdapter
+ displayUI()
+ val filter = IntentFilter(AccessorStarter.ACTION_UPDATE_UI_STATE)
+ filter.addAction(AccessorStarter.ACTION_UPDATE_UI_PROGRESS)
+ registerReceiver(mUIUpdateReceiver, filter)
+ scheduleUpdateChecking()
+ setProgressUI(false)
+ }
+
+ override fun onSwitchChanged(switchView: Switch?, isChecked: Boolean) {
+ SuspendedStorage(this).setStatus(if (isChecked) Status.ON else Status.OFF)
+ scheduleApply()
+ }
+
+ /**
+ * Apply settings which are stored in SuspendedStorage to OS
+ */
+ private fun scheduleApply() {
+ mStoppableGroup.add(AsyncSuspender(this).applyFromSettings(object : Callback<Unit> {
+ override fun onStart() {
+ setProgressUI(true)
+ }
+
+ override fun onStop(success: Boolean, result: Unit?, e: Throwable?) {
+ setProgressUI(false)
+ displayUI()
+ if (!success) {
+ logger.e("Unable scheduleApply settings", e)
+ Toast.makeText(this@MainActivity, R.string.error_apply, Toast.LENGTH_LONG).show()
+ }
+ }
+ }))
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ when (requestCode) {
+ RC_PICK -> {
+ if (resultCode == Activity.RESULT_OK && data != null && data.hasExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME)) {
+ val newSet = data.getStringArrayExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME).toSet()
+ logger.d("AR() $newSet")
+ SuspendedStorage(this).setList(newSet)
+ scheduleApply()
+ }
+ }
+ }
+ }
+
+ private fun setProgressUI(showProgress: Boolean) {
+ TileService.requestListeningState(this, ComponentName(this, SuspendTile::class.java))
+ Utils.setViewTreeEnable(findViewById(android.R.id.content), !showProgress)
+ Utils.makeTabLayoutDisable(tabLayout, !showProgress)
+ progressBar.visibility = if (showProgress) View.VISIBLE else View.GONE
+ }
+
+ override fun onDestroy() {
+ mStoppableGroup.stop()
+ mAdapter.destroy()
+ if (mSortDisplayStoppable != null) (mSortDisplayStoppable as Stoppable).stop()
+ unregisterReceiver(mUIUpdateReceiver)
+ super.onDestroy()
+ }
+
+ /**
+ * Display the data from SuspendedStorage to UI
+ */
+ private fun displayUI() {
+ switchBar.removeOnSwitchChangeListener(this)
+ switchBar.isChecked = SuspendedStorage(this).getStatus() == Status.ON
+ switchBar.addOnSwitchChangeListener(this)
+ tabLayout.removeOnTabSelectedListener(mSwitchListModeListener)
+ tabLayout.getTabAt(
+ when (SuspendedStorage(this).getListMode()) {
+ ListMode.BLACKLIST -> 0
+ ListMode.WHITELIST -> 1
+ }
+ )!!.select()
+ tabLayout.addOnTabSelectedListener(mSwitchListModeListener)
+ if (mSortDisplayStoppable != null) {
+ val stoppable = mSortDisplayStoppable as Stoppable
+ stoppable.stop()
+ mSortDisplayStoppable = null
+ }
+ mSortDisplayStoppable = Async.beginTask(object : Runnable<List<String>> {
+ override fun run(): List<String>? {
+ val sCollator = java.text.Collator.getInstance()
+ return SuspendedStorage(this@MainActivity).getList()
+ .stream()
+ .sorted { o1, o2 ->
+ return@sorted sCollator.compare(packageManager.getApplicationLabel(packageManager.getApplicationInfo(o1, 0)).toString()
+ , packageManager.getApplicationLabel(packageManager.getApplicationInfo(o2, 0)))
+ }
+ .collect(Collectors.toList())
+ }
+ }, object : Callback<List<String>> {
+ override fun onStop(success: Boolean, result: List<String>?, e: Throwable?) {
+ if (result != null) {
+ val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
+ override fun getOldListSize(): Int = mAdapter.itemCount
+
+ override fun getNewListSize(): Int = result.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ mAdapter.data[oldItemPosition]::class.java.name ==
+ result[newItemPosition]::class.java.name
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ mAdapter.data[oldItemPosition] ==
+ result[newItemPosition]
+ })
+ mAdapter.data = result
+ diff.dispatchUpdatesTo(mAdapter)
+ if (result.isEmpty()) {
+ welcomeTip.setText(when (SuspendedStorage(this@MainActivity).getListMode()) {
+ ListMode.BLACKLIST -> R.string.blacklist_welcome
+ ListMode.WHITELIST -> R.string.whitelist_welcome
+ })
+ welcomeTip.visibility = View.VISIBLE
+ } else {
+ welcomeTip.visibility = View.GONE
+ }
+ } else {
+ if (e == null) logger.e("Unable to sort data")
+ else logger.e("Unable to sort data with error", e)
+ }
+ }
+ })
+ }
+
+ private val mUIUpdateReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent == null) return
+ when (intent.action) {
+ AccessorStarter.ACTION_UPDATE_UI_STATE -> {
+ displayUI()
+ }
+ AccessorStarter.ACTION_UPDATE_UI_PROGRESS -> {
+ logger.d("Updating progress from receiver")
+ setProgressUI(intent.getBooleanExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, false))
+ }
+ }
+ }
+ }
+
+ private fun scheduleUpdateChecking() {
+ mStoppableGroup.add(Async.beginTask(UpdateChecker(), object : Callback<Update> {
+ override fun onStop(success: Boolean, result: Update?, e: Throwable?) {
+ if (result == null) return
+ if (result.version <= BuildConfig.VERSION_CODE) return
+ if (!shouldOpenGooglePlay() && !result.altUrlEnabled && !result.altUrlForce) return
+ Snackbar.make(findViewById(android.R.id.content),
+ getString(R.string.update_available,
+ result.name),
+ Snackbar.LENGTH_LONG)
+ .setAction(R.string.view) {
+ val url = if (shouldOpenGooglePlay() && !result.altUrlForce)
+ "https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}"
+ else result.altUrl
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ } catch (ignored: ActivityNotFoundException) {}
+ }
+ .show()
+ }
+
+ private fun shouldOpenGooglePlay(): Boolean =
+ "com.android.vending" == this@MainActivity.packageManager.getInstallerPackageName(BuildConfig.APPLICATION_ID)
+
+ }))
+ }
+
+ private val mSwitchListModeListener = object : TabLayout.OnTabSelectedListener {
+ override fun onTabReselected(tab: TabLayout.Tab?) {
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) {
+ }
+
+ override fun onTabSelected(tab: TabLayout.Tab) {
+ when (tab.tag) {
+ "blacklist" -> {
+ AlertDialog.Builder(this@MainActivity)
+ .setTitle(R.string.blacklist_toggle_title)
+ .setMessage(R.string.blacklist_toggle_information)
+ .setCancelable(false)
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ tabLayout.removeOnTabSelectedListener(this)
+ tabLayout.getTabAt(1)?.select()
+ tabLayout.addOnTabSelectedListener(this)
+ }
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ SuspendedStorage(this@MainActivity).setListMode(ListMode.BLACKLIST)
+ scheduleApply()
+ }
+ .show()
+ }
+ "whitelist" -> {
+ AlertDialog.Builder(this@MainActivity)
+ .setTitle(R.string.whitelist_toggle_title)
+ .setMessage(R.string.whitelist_toggle_information)
+ .setCancelable(false)
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ tabLayout.removeOnTabSelectedListener(this)
+ tabLayout.getTabAt(0)?.select()
+ tabLayout.addOnTabSelectedListener(this)
+ }
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ SuspendedStorage(this@MainActivity).setListMode(ListMode.WHITELIST)
+ scheduleApply()
+ }
+ .show()
+ }
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_get_logs -> {
+ return try {
+ startActivity(Intent.createChooser(Setup.buildShareLogsIntent(this),
+ getString(R.string.get_logs)))
+ true
+ } catch (e: Exception) {
+ try {
+ logger.e("Share logs", e)
+ } catch (ignored: Exception) {}
+ System.err.println("Unable to share logs, ${Log.getStackTraceString(e)}")
+ true
+ }
+ }
+ R.id.action_feedback -> {
+ val intent = Intent(Intent.ACTION_SENDTO)
+ .setData(Uri.parse("mailto:"))
+ intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.feedback_subject))
+ intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("android-apps@yuuta.moe"))
+ startActivity(Intent.createChooser(intent, getString(R.string.feedback)))
+ return true
+ }
+ R.id.action_check_update -> {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}")))
+ return true
+ }
+ R.id.action_oss -> {
+ startActivity(Intent(this, OssLicensesMenuActivity::class.java))
+ return true
+ }
+ else -> return super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onClick(v: View?) {
+ if (v == null) return
+ when (v.id) {
+ R.id.fab_add -> {
+ startActivityForResult(Intent(this, ApplicationPickerActivity::class.java)
+ .putExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME,
+ SuspendedStorage(this).getList().toTypedArray()), RC_PICK)
+ }
+ }
+ }
+}
+
+private class Adapter : RecyclerView.Adapter<Adapter.VH>() {
+ private val mIconMemoryCaches: LruCache<String, Drawable> =
+ LruCache(Runtime.getRuntime().maxMemory().toInt() / 5)
+ private val mStoppableGroup: StoppableGroup = StoppableGroup()
+ internal var data: List<String> = listOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
+ VH(LayoutInflater.from(parent.context).inflate(R.layout.item_application, parent, false))
+
+ override fun getItemCount(): Int = data.size
+
+ override fun onBindViewHolder(holder: VH, position: Int) {
+ val context = holder.itemView.context
+ val packageName = data[position]
+ val icon = getIconFromMemoryCache(packageName)
+ if (icon != null) {
+ holder.icon.setImageDrawable(icon)
+ } else {
+ loadIcon(packageName, holder.itemView.context, holder.icon)
+ }
+ holder.title.text = context
+ .packageManager
+ .getApplicationLabel(
+ context.packageManager.getApplicationInfo(packageName, 0)
+ )
+ }
+
+ class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val icon: ImageView = itemView.findViewById(android.R.id.icon)
+ internal val title: TextView = itemView.findViewById(android.R.id.title)
+ }
+
+ private fun addDrawableToMemoryCache(pkg: String?, icon: Drawable) {
+ if (getIconFromMemoryCache(pkg) == null) {
+ mIconMemoryCaches.put(pkg ?: "", icon)
+ }
+ }
+
+ private fun getIconFromMemoryCache(pkg: String?): Drawable? {
+ return mIconMemoryCaches.get(pkg ?: "")
+ }
+
+ private fun loadIcon(pkg: String, context: Context, imageView: ImageView) {
+ mStoppableGroup.add(Async.beginTask(object : Runnable<Drawable> {
+ override fun run(): Drawable {
+ var icon: Drawable?
+ try {
+ icon = context.packageManager
+ .getApplicationIcon(pkg)
+ } catch (ignore: PackageManager.NameNotFoundException) {
+ icon = null
+ }
+ if (icon == null) {
+ icon = ContextCompat.getDrawable(context, android.R.mipmap.sym_def_app_icon)!!
+ }
+ addDrawableToMemoryCache(pkg, icon)
+ return icon
+ }
+ }, object : Callback<Drawable> {
+ override fun onStop(success: Boolean, result: Drawable?, e: Throwable?) {
+ if (success && result != null) imageView.setImageDrawable(result)
+ }
+ }))
+ }
+
+ fun destroy() {
+ mStoppableGroup.stop()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/MoreDetailsActivity.kt b/app/src/main/java/moe/yuuta/workmode/MoreDetailsActivity.kt
new file mode 100644
index 0000000..254e130
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/MoreDetailsActivity.kt
@@ -0,0 +1,12 @@
+package moe.yuuta.workmode
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+class MoreDetailsActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ startActivity(Intent(this, MainActivity::class.java))
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/Setup.kt b/app/src/main/java/moe/yuuta/workmode/Setup.kt
new file mode 100644
index 0000000..fd75ff0
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/Setup.kt
@@ -0,0 +1,64 @@
+package moe.yuuta.workmode
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.core.content.FileProvider
+import com.elvishew.xlog.LogConfiguration
+import com.elvishew.xlog.LogUtils
+import com.elvishew.xlog.XLog
+import com.elvishew.xlog.interceptor.BlacklistTagsFilterInterceptor
+import com.elvishew.xlog.printer.AndroidPrinter
+import com.elvishew.xlog.printer.file.FilePrinter
+import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
+import moe.yuuta.workmode.utils.Utils
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+
+object Setup {
+ fun getLogsPath(context: Context): File =
+ File(context.applicationContext.dataDir.absolutePath + "/logs")
+
+ fun initLogs(logsPath: String) {
+ val config = LogConfiguration.Builder()
+ .tag("WorkMode")
+ .addInterceptor(BlacklistTagsFilterInterceptor("FCore"))
+ .addObjectFormatter(Bundle::class.java) {
+ return@addObjectFormatter Utils.dumpExtras(it)
+ }
+ .build()
+
+ val androidPrinter = AndroidPrinter()
+ val filePrinter = FilePrinter
+ .Builder(logsPath)
+ .cleanStrategy(FileLastModifiedCleanStrategy(1000 * 60 * 60 * 24 * 5))
+ .build()
+
+ XLog.init(config, androidPrinter, filePrinter)
+ }
+
+ internal fun buildShareLogsIntent(context: Context): Intent {
+ val zipFile = File("${context.externalCacheDir.absolutePath}/logs/logs-" +
+ "${SimpleDateFormat("yyyy-mm-dd-H-m-s", Locale.US).format(Date())}.zip")
+ LogUtils.compress(Setup.getLogsPath(context).absolutePath,
+ zipFile.absolutePath)
+ val fileUri = FileProvider.getUriForFile(
+ context,
+ BuildConfig.APPLICATION_ID + ".fileprovider",
+ zipFile)
+ if (fileUri == null || !zipFile.exists()) {
+ throw NullPointerException()
+ }
+ val intent = Intent()
+ intent.action = Intent.ACTION_SEND
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ var type = context.contentResolver.getType(fileUri)
+ if (type == null || type.trim().equals("")) {
+ type = "application/zip"
+ }
+ intent.type = type
+ intent.putExtra(Intent.EXTRA_STREAM, fileUri)
+ return intent
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt b/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt
new file mode 100644
index 0000000..78ef5dd
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt
@@ -0,0 +1,86 @@
+package moe.yuuta.workmode.access
+
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.PersistableBundle
+import android.os.Process
+import android.os.UserHandle
+import android.system.Os
+import moe.yuuta.workmode.BuildConfig
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+
+/**
+ * An layer to access package suspending related APIs, it is a low-level layer which is used to call System APIs directly.
+ *
+ * TODO: Multi-user support
+ */
+internal class AccessLayer(private val mContext: Context) {
+ private val mPM: PackageManager = mContext.packageManager
+
+ fun setPackagesSuspended(packageNames: Array<String>, suspended: Boolean,
+ appExtras: PersistableBundle, launcherExtras: PersistableBundle,
+ dialogMessage: String): Array<String> {
+ // ApplicationPackageManager ALWAYS uses context.getOpPackageName() as the argument "callingPackage"
+ // My callingPackage MUSTN'T equals to 'android'
+ // If we are using packageName of 'android', system will show disabled
+ // by admin dialog instead of suspended dialog
+ // F**k Google
+ val func: Method = Class.forName("android.content.pm.IPackageManager")
+ .getDeclaredMethod("setPackagesSuspendedAsUser",
+ Array<String>::class.java,
+ Boolean::class.java,
+ PersistableBundle::class.java,
+ PersistableBundle::class.java,
+ String::class.java,
+ String::class.java,
+ Int::class.java)
+
+ // It's an unstable design
+ val iPM: Field = mPM::class.java.getDeclaredField("mPM")
+ iPM.isAccessible = true
+
+ return func.invoke(iPM.get(mPM),
+ packageNames,
+ suspended,
+ appExtras,
+ launcherExtras,
+ dialogMessage,
+ BuildConfig.APPLICATION_ID,
+ UserHandle.getUserHandleForUid(mPM.getPackageUid(mContext.packageName, 0)).hashCode()) as Array<String>
+ }
+
+ /**
+ * This method will SET your UID and you WON'T BE ABLE TO GO BACK.
+ * Create a new process and access it.
+ */
+ fun getSuspendedPackageAppExtras(packageName: String): PersistableBundle? {
+ Os.setuid(mPM.getPackageUid(packageName, 0))
+ // ApplicationPackageManager ALWAYS uses context.getOpPackageName() as the package name
+ // F**k Google
+ val func: Method = Class.forName("android.content.pm.IPackageManager")
+ .getDeclaredMethod("getSuspendedPackageAppExtras",
+ String::class.java,
+ Int::class.java)
+
+ // It's an unstable design
+ val iPM: Field = mPM::class.java.getDeclaredField("mPM")
+ iPM.isAccessible = true
+
+ return func.invoke(iPM.get(mPM),
+ packageName,
+ UserHandle.getUserHandleForUid(mPM.getPackageUid(packageName, 0)).hashCode()) as PersistableBundle?
+ }
+
+ @Throws(PackageManager.NameNotFoundException::class)
+ fun isPackageSuspended(packageName: String): Boolean {
+ val func: Method = PackageManager::class.java.getDeclaredMethod("isPackageSuspended",
+ String::class.java)
+ return func.invoke(mPM, packageName) as Boolean
+ }
+
+ fun getSuspendedPackageLauncherExtras(packageName: String): Bundle? =
+ mContext.getSystemService(LauncherApps::class.java).getSuspendedPackageLauncherExtras(packageName, Process.myUserHandle())
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt b/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt
new file mode 100644
index 0000000..c8aae67
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt
@@ -0,0 +1,199 @@
+package moe.yuuta.workmode.access
+
+import android.content.Context
+import android.os.Bundle
+import android.os.Parcel
+import android.os.PersistableBundle
+import com.elvishew.xlog.Logger
+import com.elvishew.xlog.XLog
+import eu.chainfire.librootjava.RootJava
+import eu.chainfire.libsuperuser.Shell
+import moe.yuuta.workmode.BuildConfig
+import moe.yuuta.workmode.suspend.data.ListMode
+import moe.yuuta.workmode.suspend.data.Status
+import moe.yuuta.workmode.utils.ByteArraySerializer
+
+/**
+ * The high-level API accessor, as known as the launcher (starter) of the accessor, wraps
+ * the necessary start steps to launch it and deserialize the result.
+ */
+open class AccessorStarter(private val mContext: Context, private val mLogPath: String) {
+ private val logger: Logger = XLog.tag("AccessorStarter").build()
+
+ companion object {
+ const val ACTION_UPDATE_UI_STATE = "moe.yuuta.workmode.access.ACTION_UPDATE_UI_STATE"
+ const val ACTION_UPDATE_UI_PROGRESS = "moe.yuuta.workmode.access.ACTION_UPDATE_UI_PROGRESS"
+ const val EXTRA_SHOW_PROGRESS = "moe.yuuta.workmode.access.EXTRA_SHOW_PROGRESS"
+ }
+
+ private fun launchRootProcess(root: Boolean, vararg args: String): MutableList<String> {
+ val command = RootJava.getLaunchScript(mContext,
+ WorkModeAccessor::class.java,
+ null,
+ null,
+ args,
+ BuildConfig.APPLICATION_ID + ":accessor")
+
+ return if (root) {
+ Shell.SU.run(command)
+ } else {
+ Shell.SH.run(command)
+ }
+ }
+
+ fun getSuspendedPackageAppExtras(packageName: String, root: Boolean): Bundle? {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_GET_APP_EXTRAS)
+ argumentParcel.writeString(packageName)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val bundle = result.readBundle()
+ result.recycle()
+ return bundle
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun getSuspendedPackageLauncherExtras(packageName: String, root: Boolean): Bundle? {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_GET_LAUNCHER_EXTRAS)
+ argumentParcel.writeString(packageName)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val bundle = result.readBundle()
+ result.recycle()
+ return bundle
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun isPackageSuspended(packageNames: Array<out String>, root: Boolean): Boolean {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_IS_SUSPENDED)
+ argumentParcel.writeStringArray(packageNames)
+ val marshalledResult = launchRootProcess(root,
+ WorkModeAccessor.ACTION_IS_SUSPENDED,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val isSuspended = result.readByte() == 1.toByte()
+ result.recycle()
+ return isSuspended
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun dump(packageName: String, root: Boolean): DumpResult {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_DUMP)
+ argumentParcel.writeString(packageName)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val data = DumpResult(
+ result.readByte() == 1.toByte(),
+ result.readBundle(),
+ result.readBundle()
+ )
+ result.recycle()
+ return data
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun setPackagesSuspended(packageNames: Array<String>, suspended: Boolean,
+ appExtras: PersistableBundle, launcherExtras: PersistableBundle,
+ dialogMessage: String, root: Boolean): Array<String> {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_SET_SUSPENDED)
+ argumentParcel.writeStringArray(packageNames)
+ argumentParcel.writeByte(if (suspended) 1 else 0)
+ argumentParcel.writeBundle(Bundle(appExtras))
+ argumentParcel.writeBundle(Bundle(launcherExtras))
+ argumentParcel.writeString(dialogMessage)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val rs = result.createStringArray() ?: arrayOf()
+ result.recycle()
+ return rs
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ @Throws(Throwable::class)
+ private fun deserialize(byteArray: ByteArray): Parcel {
+ val result = Parcel.obtain()
+ result.unmarshall(byteArray, 0, byteArray.size)
+ // Thanks to https://github.com/jiaminghan/droidplanner-master/blob/743b5436df6311cbbbfdecd21f796e2b948cbac7/Android/src/com/o3dr/services/android/lib/util/ParcelableUtils.java#L35
+ result.setDataPosition(0)
+ when (result.readByte()) {
+ 1.toByte() -> {
+ }
+ 0.toByte() -> {
+ val throwable = result.readSerializable() as Throwable?
+ if (throwable != null) {
+ throw throwable
+ }
+ throw RuntimeException("Unsuccessful result with unknown stacktrace")
+ }
+ }
+ return result
+ }
+
+ fun getPackagesSuspendedByWorkMode(root: Boolean): List<String> {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_GET_ALL_PACKAGES_SUSPENDED_BY_WORK_MODE)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val data = result.createStringArrayList()
+ result.recycle()
+ return data ?: listOf()
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun apply(suspendList: Array<String>, listMode: ListMode, status: Status, root: Boolean) {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_APPLY)
+ argumentParcel.writeStringArray(suspendList)
+ argumentParcel.writeInt(when (listMode) {
+ ListMode.BLACKLIST -> 1
+ ListMode.WHITELIST -> 2
+ })
+ argumentParcel.writeInt(when (status) {
+ Status.ON -> 1
+ Status.OFF -> 2
+ })
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ result.recycle()
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt b/app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt
new file mode 100644
index 0000000..8715989
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt
@@ -0,0 +1,9 @@
+package moe.yuuta.workmode.access
+
+import android.content.Context
+import moe.yuuta.workmode.Setup
+
+/**
+ * A flavor of AccessorStarter which can be used in standard Android Applications
+ */
+class ApplicationAccessorStarter(private val mContext: Context) : AccessorStarter(mContext, Setup.getLogsPath(mContext).absolutePath) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt b/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt
new file mode 100644
index 0000000..d4a9d79
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt
@@ -0,0 +1,7 @@
+package moe.yuuta.workmode.access
+
+import android.os.Bundle
+
+data class DumpResult(val isSuspended: Boolean,
+ val appExtras: Bundle?,
+ val launcherExtras: Bundle?) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt b/app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt
new file mode 100644
index 0000000..05c4983
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt
@@ -0,0 +1,9 @@
+package moe.yuuta.workmode.access
+
+import eu.chainfire.librootjava.RootJava
+import moe.yuuta.workmode.BuildConfig
+
+/**
+ * A flavor of AccessorStarter which can be used in shell programs
+ */
+class ShellAccessorStarter(private val mLogPath: String) : AccessorStarter(RootJava.getPackageContext(BuildConfig.APPLICATION_ID), mLogPath) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt b/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt
new file mode 100644
index 0000000..d8f17cd
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt
@@ -0,0 +1,317 @@
+package moe.yuuta.workmode.access
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcel
+import android.os.PersistableBundle
+import android.service.quicksettings.TileService
+import com.elvishew.xlog.Logger
+import com.elvishew.xlog.XLog
+import eu.chainfire.librootjava.RootJava
+import moe.yuuta.workmode.BuildConfig
+import moe.yuuta.workmode.R
+import moe.yuuta.workmode.Setup
+import moe.yuuta.workmode.suspend.SuspendTile
+import moe.yuuta.workmode.suspend.SuspendedApp
+import moe.yuuta.workmode.suspend.data.ListMode
+import moe.yuuta.workmode.suspend.data.Status
+import moe.yuuta.workmode.utils.BundleUtils
+import moe.yuuta.workmode.utils.ByteArraySerializer
+import moe.yuuta.workmode.utils.Utils
+import java.util.stream.Collectors
+
+class WorkModeAccessor {
+ companion object {
+ const val ACTION_GET_APP_EXTRAS = "get_app_extras"
+ const val ACTION_IS_SUSPENDED = "is_suspended"
+ const val ACTION_GET_LAUNCHER_EXTRAS = "get_launcher_extras"
+ const val ACTION_SET_SUSPENDED = "set_suspended"
+ const val ACTION_DUMP = "dump"
+ const val ACTION_GET_ALL_PACKAGES_SUSPENDED_BY_WORK_MODE = "get_all_packages_suspended_by_work_mode"
+ const val ACTION_APPLY = "apply"
+
+ @JvmStatic
+ fun main(vararg args: String) {
+ RootJava.restoreOriginalLdLibraryPath()
+ WorkModeAccessor().go(args)
+ }
+ }
+
+ private lateinit var logger: Logger
+ private lateinit var mContext: Context
+ private lateinit var pmAccess: AccessLayer
+ private lateinit var mLogPath: String
+
+ private fun go(args: Array<out String>) {
+ mContext = RootJava.getPackageContext(BuildConfig.APPLICATION_ID)
+ pmAccess = AccessLayer(mContext)
+ mContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS)
+ .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, true))
+ var parcel = Parcel.obtain()
+ val argsByteArray = ByteArraySerializer.deserialize(args[0])
+ val argsParcel = Parcel.obtain()
+ argsParcel.unmarshall(argsByteArray, 0, argsByteArray.size)
+ argsParcel.setDataPosition(0)
+ mLogPath = argsParcel.readString() ?: "/data/adb"
+ Setup.initLogs(mLogPath)
+ logger = XLog.tag("Accessor").build()
+ try {
+ // General successful flag: 1 = success; 0 = unsuccessful
+ parcel.writeByte(1)
+ runGo(argsParcel, parcel)
+ } catch (e: Throwable) {
+ logger.e("Unexpected exception caused in accessor", e)
+ parcel.recycle()
+ // Re-mark it as unsuccessful
+ parcel = Parcel.obtain()
+ parcel.writeByte(0)
+ parcel.writeSerializable(e)
+ }
+ try {
+ TileService.requestListeningState(mContext, ComponentName(mContext, SuspendTile::class.java))
+ mContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_STATE)
+ .setPackage(BuildConfig.APPLICATION_ID))
+ } catch (e: Throwable) {
+ logger.e("Unable to refresh tile", e)
+ }
+ System.out.println(ByteArraySerializer.serialize(parcel.marshall()))
+ parcel.recycle()
+ mContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS)
+ .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, false))
+ System.exit(0)
+ }
+
+ private fun runGo(argsParcel: Parcel, parcel: Parcel) {
+ when(argsParcel.readString()) {
+ ACTION_GET_APP_EXTRAS -> {
+ val bundle = pmAccess.getSuspendedPackageAppExtras(argsParcel.readString() ?: "android")
+ parcel.writeBundle(if (bundle != null) Bundle(bundle) else Bundle.EMPTY)
+ }
+ ACTION_IS_SUSPENDED -> {
+ val packageNames = argsParcel.createStringArray() ?: arrayOf("android")
+ var allSuspended = true
+ for (packageName in packageNames) {
+ if (!pmAccess.isPackageSuspended(packageName)) allSuspended = false
+ }
+ parcel.writeByte(if (allSuspended) 1 else 0)
+ }
+ ACTION_GET_LAUNCHER_EXTRAS -> {
+ parcel.writeBundle(pmAccess.getSuspendedPackageLauncherExtras(argsParcel.readString() ?: "android") ?: Bundle.EMPTY)
+ }
+ ACTION_SET_SUSPENDED -> {
+ val packageNames = argsParcel.createStringArray() ?: arrayOf("android")
+ val suspended = argsParcel.readByte() == 1.toByte()
+ logger.d("Running suspend: $suspended on ${packageNames.size} packages.")
+ val appExtras = argsParcel.readBundle()
+ val launcherExtras = argsParcel.readBundle()
+ val dialogMessage = argsParcel.readString() ?: "WorkMode"
+ argsParcel.recycle()
+ parcel.writeStringArray(suspend(packageNames, suspended, appExtras, launcherExtras, dialogMessage))
+ }
+ ACTION_DUMP -> {
+ val pkg = argsParcel.readString() ?: "android"
+ parcel.writeByte(if (pmAccess.isPackageSuspended(pkg)) 1 else 0)
+ parcel.writeBundle(ShellAccessorStarter(mLogPath).getSuspendedPackageAppExtras(pkg, false))
+ parcel.writeBundle(pmAccess.getSuspendedPackageLauncherExtras(pkg) ?: Bundle.EMPTY)
+ }
+ ACTION_GET_ALL_PACKAGES_SUSPENDED_BY_WORK_MODE -> {
+ parcel.writeStringList(getPackagesSuspendedByWorkMode())
+ }
+ ACTION_APPLY -> {
+ apply(argsParcel)
+ }
+ }
+ }
+
+ private fun suspend(packageNames: Array<String>, suspended: Boolean,
+ appExtras: Bundle, launcherExtras: Bundle,
+ dialogMessage: String): Array<String> =
+ pmAccess.setPackagesSuspended(
+ packageNames,
+ suspended,
+ BundleUtils.toPersistableBundle(appExtras),
+ BundleUtils.toPersistableBundle(launcherExtras),
+ dialogMessage
+ )
+
+ private fun getPackagesSuspendedByWorkMode(): List<String> =
+ mContext.packageManager.getInstalledApplications(0)
+ .stream()
+ .filter(Utils.buildGeneralApplicationInfoFilter(mContext))
+ .filter {
+ return@filter pmAccess.isPackageSuspended(it.packageName) &&
+ SuspendedApp.deserializeBundle(pmAccess.getSuspendedPackageLauncherExtras(it.packageName)).isSuspendedByWorkMode
+ }
+ .map {
+ return@map it.packageName
+ }
+ .collect(Collectors.toList())
+
+ private fun apply(args: Parcel) {
+ // Compare system's list and ours.
+ // Blacklist:
+ // System suspended -> {
+ // in our list -> ON - don't care; OFF - unsuspend
+ // not in our list -> unsuspend
+ // }
+ // System not suspended -> {
+ // in our list -> ON - suspend; OFF - don't care
+ // not in our list -> don't care
+ // }
+ // Whitelist:
+ // System suspended -> {
+ // in our whitelist -> unsuspend
+ // not in our whitelist -> ON - don't care; OFF - unsuspend
+ // }
+ // System not suspended -> {
+ // in our whitelist -> don't care
+ // not in our whitelist -> ON - suspend; OFF - don't care
+ // }
+
+ // This is the plan for Off->On or On->On situations. If we are heading
+ // Off, we just ignore all tasks which is going to suspend an app. Because
+ // we need to restore.
+
+ // We use these two lists to determine whatever an app is suspended
+ // It it is suspended but not appears in systemSuspendedList, we know that
+ // it is suspended by other apps, like D**ital Wellbeing, we can just override it.
+ val systemSuspendedList = getPackagesSuspendedByWorkMode()
+ val systemAllAppList = mContext.packageManager.getInstalledApplications(0)
+ .stream()
+ .filter(Utils.buildGeneralApplicationInfoFilter(mContext))
+ .map { return@map it.packageName }
+ .collect(Collectors.toList())
+ val ourList = args.createStringArrayList()
+ val listMode = when (args.readInt()) {
+ 1 -> ListMode.BLACKLIST
+ 2 -> ListMode.WHITELIST
+ else -> throw IllegalArgumentException("Unexpected list mode")
+ }
+ val status = when (args.readInt()) {
+ 1 -> Status.ON
+ 2 -> Status.OFF
+ else -> throw IllegalArgumentException("Unexpected status")
+ }
+
+ val tasks = systemAllAppList.stream()
+ // Filter "don't care" situations, do not map them here.
+ .filter {
+ val systemSuspended = systemSuspendedList.contains(it)
+ val inOurList = ourList.contains(it)
+ when (listMode) {
+ ListMode.BLACKLIST -> {
+ if (systemSuspended) {
+ if (inOurList) {
+ if (status == Status.ON) {
+ return@filter false
+ } else {
+ return@filter true
+ }
+ } else {
+ if (status == Status.ON) {
+ return@filter true
+ } else {
+ return@filter false
+ }
+ }
+ } else {
+ if (status == Status.ON) {
+ return@filter inOurList
+ } else {
+ return@filter false
+ }
+ }
+ }
+ ListMode.WHITELIST -> {
+ if (systemSuspended) {
+ if (inOurList) {
+ return@filter true
+ } else {
+ if (status == Status.ON) {
+ return@filter false
+ } else {
+ return@filter true
+ }
+ }
+ } else {
+ if (status == Status.ON) {
+ return@filter !inOurList
+ } else {
+ return@filter false
+ }
+ }
+ }
+ }
+ }
+ // Now, map them and determine that whatever a package should be suspended or un-suspended.
+ .map {
+ val systemSuspended = systemSuspendedList.contains(it)
+ when (listMode) {
+ ListMode.BLACKLIST -> {
+ if (systemSuspended) {
+ // It must be off (if in our list),
+ // or need to un-suspend (whatever on or off)
+ return@map SuspendTask(it, false)
+ } else {
+ // It must be on
+ return@map SuspendTask(it, true)
+ }
+ }
+ ListMode.WHITELIST -> {
+ if (systemSuspended) {
+ // It must be un-suspended (if in our list)
+ // (or off)
+ return@map SuspendTask(it, false)
+ } else {
+ // It must be on
+ return@map SuspendTask(it, true)
+ }
+ }
+ }
+ }
+ // Collect them, we will execute later.
+ .collect(Collectors.toList())
+ // Suspend first
+ if (status == Status.ON) {
+ val suspendList = tasks.stream()
+ .filter {
+ return@filter it.suspend
+ }
+ .map {
+ return@map it.packageName
+ }
+ .collect(Collectors.toList())
+ if (suspendList.size > 0) {
+ suspend(suspendList.toTypedArray(),
+ true)
+ }
+ }
+ // Then unsuspand
+ val unsuspendList = tasks.stream()
+ .filter {
+ return@filter !it.suspend
+ }
+ .map {
+ return@map it.packageName
+ }
+ .collect(Collectors.toList())
+ if (unsuspendList.size > 0) {
+ suspend(unsuspendList.toTypedArray(),
+ false)
+ }
+ }
+
+ private fun suspend(packageNames: Array<String>, suspended: Boolean): Array<String> =
+ pmAccess.setPackagesSuspended(packageNames,
+ suspended,
+ PersistableBundle(),
+ SuspendedApp.getDefault().serializeBundle(), // We use LauncherExtras because they are easy to read
+ mContext.getString(R.string.suspended_message))
+}
+
+private data class SuspendTask(
+ val packageName: String,
+ val suspend: Boolean
+) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/async/Async.kt b/app/src/main/java/moe/yuuta/workmode/async/Async.kt
new file mode 100644
index 0000000..41542cd
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/async/Async.kt
@@ -0,0 +1,47 @@
+package moe.yuuta.workmode.async
+
+import android.annotation.SuppressLint
+import android.os.AsyncTask
+
+object Async {
+ fun <T> beginTask(runnable: Runnable<T>, callback: Callback<T>): Stoppable {
+ val task = @SuppressLint("StaticFieldLeak")
+ object : AsyncTask<Void, Void, TaskResult<T>>() {
+ override fun onPostExecute(result: TaskResult<T>) {
+ callback.onStop(result.successful, result.result, result.e)
+ }
+
+ override fun doInBackground(vararg params: Void?): TaskResult<T> {
+ try {
+ return TaskResult(true, runnable.run(), null)
+ } catch (e: Throwable) {
+ return TaskResult(false, null, e)
+ }
+ }
+
+ override fun onPreExecute() {
+ callback.onStart()
+ }
+ }
+ val stoppable = object : Stoppable {
+ override fun stop() {
+ if (!isStopped()) task.cancel(true)
+ }
+
+ override fun isStopped(): Boolean = task.isCancelled
+ }
+ task.execute()
+ return stoppable
+ }
+}
+
+@FunctionalInterface
+interface Runnable<T> {
+ fun run(): T?
+}
+
+data class TaskResult<T>(
+ val successful: Boolean,
+ val result: T?,
+ val e: Throwable?
+) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/async/Callback.kt b/app/src/main/java/moe/yuuta/workmode/async/Callback.kt
new file mode 100644
index 0000000..bac767b
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/async/Callback.kt
@@ -0,0 +1,6 @@
+package moe.yuuta.workmode.async
+
+interface Callback<T> {
+ fun onStart() {}
+ fun onStop(success: Boolean, result: T?, e: Throwable?) {}
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/async/Stoppable.kt b/app/src/main/java/moe/yuuta/workmode/async/Stoppable.kt
new file mode 100644
index 0000000..36e0335
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/async/Stoppable.kt
@@ -0,0 +1,6 @@
+package moe.yuuta.workmode.async
+
+interface Stoppable {
+ fun stop()
+ fun isStopped(): Boolean
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/async/StoppableGroup.kt b/app/src/main/java/moe/yuuta/workmode/async/StoppableGroup.kt
new file mode 100644
index 0000000..ef651fe
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/async/StoppableGroup.kt
@@ -0,0 +1,28 @@
+package moe.yuuta.workmode.async
+
+class StoppableGroup : Stoppable {
+ private val innerList: MutableList<Stoppable> = mutableListOf()
+
+ fun add(stoppable: Stoppable): StoppableGroup {
+ innerList.add(stoppable)
+ return this
+ }
+
+ fun remove(stoppable: Stoppable): StoppableGroup {
+ innerList.remove(stoppable)
+ return this
+ }
+
+ override fun stop() {
+ innerList.stream().forEach {
+ it.stop()
+ }
+ }
+
+ override fun isStopped(): Boolean {
+ for (stoppable in innerList) {
+ if (!stoppable.isStopped()) return false
+ }
+ return true
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt b/app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt
new file mode 100644
index 0000000..8cb4947
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt
@@ -0,0 +1,27 @@
+package moe.yuuta.workmode.suspend
+
+import android.content.Context
+import moe.yuuta.workmode.async.Async
+import moe.yuuta.workmode.async.Callback
+import moe.yuuta.workmode.async.Runnable
+import moe.yuuta.workmode.async.Stoppable
+
+/**
+ * An async suspender which wraps suspend tasks and run them in the background
+ */
+class AsyncSuspender(private val mContext: Context) {
+ fun suspend(packageNames: Array<String>, suspended: Boolean, callback: Callback<Array<String>>): Stoppable =
+ Async.beginTask(object : Runnable<Array<String>> {
+ override fun run(): Array<String> = Suspender(mContext).suspend(packageNames, suspended)
+ }, callback)
+
+ fun isSuspended(packageNames: Array<String>, callback: Callback<Boolean>): Stoppable =
+ Async.beginTask(object : Runnable<Boolean> {
+ override fun run(): Boolean = Suspender(mContext).isSuspended(packageNames)
+ }, callback)
+
+ fun applyFromSettings(callback: Callback<Unit>): Stoppable =
+ Async.beginTask(object : Runnable<Unit> {
+ override fun run(): Unit = Suspender(mContext).applyFromSettings()
+ }, callback)
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt b/app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt
new file mode 100644
index 0000000..f3fbc70
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt
@@ -0,0 +1,30 @@
+package moe.yuuta.workmode.suspend
+
+import android.content.Intent
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import moe.yuuta.workmode.suspend.data.Status
+import moe.yuuta.workmode.suspend.data.SuspendedStorage
+
+class SuspendTile : TileService() {
+ override fun onClick() {
+ val storage = SuspendedStorage(this)
+ storage.setStatus(
+ when (storage.getStatus()) {
+ Status.ON -> Status.OFF
+ Status.OFF -> Status.ON
+ }
+ )
+ sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+ Suspender(this).applyFromSettings()
+ }
+
+ override fun onStartListening() {
+ val tile = qsTile
+ tile.state = when (SuspendedStorage(this@SuspendTile).getStatus()) {
+ Status.ON -> Tile.STATE_ACTIVE
+ Status.OFF -> Tile.STATE_INACTIVE
+ }
+ tile.updateTile()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt b/app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt
new file mode 100644
index 0000000..9c6ddf7
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt
@@ -0,0 +1,38 @@
+package moe.yuuta.workmode.suspend
+
+import android.os.Bundle
+import android.os.PersistableBundle
+import moe.yuuta.workmode.BuildConfig
+
+/**
+ * The data class of a suspended app. This is ONLY used in this app, which
+ * can be understood as "a bridge between the extras stored in the system and
+ * information which is used in this app"
+ */
+data class SuspendedApp(
+ val isSuspendedByWorkMode: Boolean, // The flag which is used to determine whatever is suspended by WorkMode
+ val versionCodeSuspended: Int // The version code of this app when suspended the target
+) {
+ companion object {
+ // These values are stored by the system, should not be usually changed for migrating
+ const val EXTRA_IS_SUSPENDED_BY_WORK_MODE = "moe.yuuta.workmode.EXTRA_IS_SUSPENDED_BY_WORK_MODE"
+ const val EXTRA_VERSION_CODE = "moe.yuuta.workmode.EXTRA_VERSION_CODE"
+
+ fun deserializeBundle(launcherExtras: Bundle?): SuspendedApp {
+ if (launcherExtras == null) return SuspendedApp(false, -1)
+ return SuspendedApp(
+ launcherExtras.getBoolean(EXTRA_IS_SUSPENDED_BY_WORK_MODE, false),
+ launcherExtras.getInt(EXTRA_VERSION_CODE, -1)
+ )
+ }
+
+ fun getDefault(): SuspendedApp = SuspendedApp(true, BuildConfig.VERSION_CODE)
+ }
+
+ fun serializeBundle(): PersistableBundle {
+ val bundle = PersistableBundle()
+ bundle.putBoolean(EXTRA_IS_SUSPENDED_BY_WORK_MODE, isSuspendedByWorkMode)
+ bundle.putInt(EXTRA_VERSION_CODE, versionCodeSuspended)
+ return bundle
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt b/app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt
new file mode 100644
index 0000000..49924bb
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt
@@ -0,0 +1,40 @@
+package moe.yuuta.workmode.suspend
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.PersistableBundle
+import android.service.quicksettings.TileService
+import moe.yuuta.workmode.R
+import moe.yuuta.workmode.access.ApplicationAccessorStarter
+import moe.yuuta.workmode.suspend.data.SuspendedStorage
+
+/**
+ * The highest-level suspender to wrap all information needed to suspend or vice versa. This
+ * should be called from UI components directly
+ * Chain: UI -> Suspender -> AccessorStarter -> WorkModeAccessor -> AccessLayer -> Framework
+ */
+class Suspender(private val mContext: Context) {
+ fun suspend(packageNames: Array<String>, suspended: Boolean): Array<String> =
+ ApplicationAccessorStarter(mContext).setPackagesSuspended(packageNames,
+ suspended,
+ PersistableBundle(),
+ SuspendedApp.getDefault().serializeBundle(), // We use LauncherExtras because they are easy to read
+ mContext.getString(R.string.suspended_message),
+ true)
+
+ fun isSuspended(packageNames: Array<String>): Boolean =
+ ApplicationAccessorStarter(mContext).isPackageSuspended(packageNames, true)
+
+ fun getPackagesSuspendedByWorkMode(): List<String> =
+ ApplicationAccessorStarter(mContext).getPackagesSuspendedByWorkMode(true)
+
+ fun applyFromSettings() {
+ val storage = SuspendedStorage(mContext)
+ storage.cleanList(mContext)
+ val status = storage.getStatus()
+ val listMode = storage.getListMode()
+ val list = storage.getList()
+ ApplicationAccessorStarter(mContext).apply(list.toTypedArray(), listMode, status, true)
+ TileService.requestListeningState(mContext, ComponentName(mContext, SuspendTile::class.java))
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/data/ListMode.kt b/app/src/main/java/moe/yuuta/workmode/suspend/data/ListMode.kt
new file mode 100644
index 0000000..2ba0cc1
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/suspend/data/ListMode.kt
@@ -0,0 +1,6 @@
+package moe.yuuta.workmode.suspend.data
+
+enum class ListMode {
+ BLACKLIST,
+ WHITELIST
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/data/Status.kt b/app/src/main/java/moe/yuuta/workmode/suspend/data/Status.kt
new file mode 100644
index 0000000..aa57564
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/suspend/data/Status.kt
@@ -0,0 +1,6 @@
+package moe.yuuta.workmode.suspend.data
+
+enum class Status {
+ OFF,
+ ON
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt b/app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt
new file mode 100644
index 0000000..c02af49
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt
@@ -0,0 +1,68 @@
+package moe.yuuta.workmode.suspend.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.elvishew.xlog.XLog
+import moe.yuuta.workmode.utils.Utils
+import java.util.stream.Collectors
+
+/**
+ * An independent storage of suspended status
+ */
+class SuspendedStorage(private val mContext: Context) {
+ private val logger = XLog.tag("Storage").build()
+
+ private fun getStorage(): SharedPreferences = mContext.getSharedPreferences("suspended", Context.MODE_PRIVATE)
+
+ fun getList(): List<String> = (getStorage().getStringSet("list", setOf()) ?: listOf<String>()).toList()
+
+ fun setList(set: Set<String>) {
+ logger.d("s() $set")
+ getStorage().edit().putStringSet("list", set).apply()
+ }
+
+ fun setStatus(status: Status) {
+ getStorage().edit().putInt("status",
+ when (status) {
+ Status.OFF -> 0
+ Status.ON -> 1
+ }).apply()
+ }
+
+ fun getStatus(): Status =
+ when (getStorage().getInt("status", 0)) {
+ 0 -> Status.OFF
+ 1 -> Status.ON
+ else -> Status.OFF
+ }
+
+ fun setListMode(listMode: ListMode) {
+ getStorage().edit().putInt("list_mode",
+ when (listMode) {
+ ListMode.BLACKLIST -> 0
+ ListMode.WHITELIST -> 1
+ }).apply()
+ }
+
+ fun getListMode(): ListMode =
+ when (getStorage().getInt("list_mode", 0)) {
+ 0 -> ListMode.BLACKLIST
+ 1 -> ListMode.WHITELIST
+ else -> ListMode.BLACKLIST
+ }
+
+ fun cleanList(context: Context) {
+ val installed = mContext.packageManager.getInstalledApplications(0)
+ .stream()
+ .filter(Utils.buildGeneralApplicationInfoFilter(context))
+ .map {
+ return@map it.packageName
+ }
+ .collect(Collectors.toList())
+ setList(getList().stream()
+ .filter {
+ return@filter installed.contains(it)
+ }
+ .collect(Collectors.toSet()))
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/update/UpdateChecker.kt b/app/src/main/java/moe/yuuta/workmode/update/UpdateChecker.kt
new file mode 100644
index 0000000..e88ab80
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/update/UpdateChecker.kt
@@ -0,0 +1,70 @@
+package moe.yuuta.workmode.update
+
+import com.elvishew.xlog.XLog
+import moe.yuuta.workmode.BuildConfig
+import moe.yuuta.workmode.async.Runnable
+import org.json.JSONObject
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.net.HttpURLConnection
+import java.net.URL
+
+
+class UpdateChecker : Runnable<Update> {
+ private val logger = XLog.tag("UpdateChecker").build()
+ override fun run(): Update {
+ logger.d("Start checking")
+ var httpURLConnection: HttpURLConnection? = null
+ try {
+ val url = URL("https://raw.githubusercontent.com/Trumeet/_priv/master/update/${BuildConfig.APPLICATION_ID}.json")
+ httpURLConnection = url.openConnection() as HttpURLConnection
+ httpURLConnection.requestMethod = "GET"
+ httpURLConnection.connect()
+
+ val inputStream = httpURLConnection.inputStream
+ val buffer = StringBuffer()
+
+ if (inputStream == null) {
+ throw IllegalStateException("Str is null.")
+ }
+ val reader = BufferedReader(InputStreamReader(inputStream))
+
+ var line: String?
+ while (true) {
+ line = reader.readLine()
+ if (line == null) break
+ buffer.append(line + "\n")
+ }
+
+ if (buffer.isEmpty()) {
+ throw IllegalStateException("Buffer is empty")
+ }
+
+ val response = buffer.toString()
+
+ logger.i("Response: ${httpURLConnection.responseCode} (${httpURLConnection.responseMessage}), data: $response")
+
+ val json = JSONObject(response)
+ return Update(json.getInt("version"),
+ json.getString("name"),
+ json.getString("alt_url"),
+ json.getBoolean("alt_url_enable"),
+ json.getBoolean("alt_url_force"))
+ } catch (e: Exception) {
+ logger.e("Unable to check", e)
+ throw e
+ } finally {
+ // this is done so that there are no open connections left when this task is going to complete
+ if (httpURLConnection != null)
+ httpURLConnection.disconnect()
+ }
+ }
+}
+
+data class Update(
+ val version: Int,
+ val name: String,
+ val altUrl: String,
+ val altUrlEnabled: Boolean,
+ val altUrlForce: Boolean
+) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/utils/BundleUtils.java b/app/src/main/java/moe/yuuta/workmode/utils/BundleUtils.java
new file mode 100644
index 0000000..28c96b0
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/utils/BundleUtils.java
@@ -0,0 +1,89 @@
+package moe.yuuta.workmode.utils;
+
+import android.os.BaseBundle;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+public class BundleUtils {
+ /**
+ * Creates a new {@link Bundle} based on the specified {@link PersistableBundle}.
+ */
+ public static Bundle toBundle(PersistableBundle persistableBundle) {
+ if (persistableBundle == null) {
+ return null;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putAll(persistableBundle);
+ return bundle;
+ }
+
+ /**
+ * Creates a new {@link PersistableBundle} from the specified {@link Bundle}.
+ * Will ignore all values that are not persistable, according
+ * to {@link #isPersistableBundleType(Object)}.
+ */
+ public static PersistableBundle toPersistableBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ PersistableBundle persistableBundle = new PersistableBundle();
+ for (String key : bundle.keySet()) {
+ Object value = bundle.get(key);
+ if (isPersistableBundleType(value)) {
+ putIntoBundle(persistableBundle, key, value);
+ }
+ }
+ return persistableBundle;
+ }
+
+ /**
+ * Checks if the specified object can be put into a {@link PersistableBundle}.
+ *
+ * @see <a href="https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PersistableBundle.java#49">PersistableBundle Implementation</a>
+ */
+ public static boolean isPersistableBundleType(Object value) {
+ return ((value instanceof PersistableBundle) ||
+ (value instanceof Integer) || (value instanceof int[]) ||
+ (value instanceof Long) || (value instanceof long[]) ||
+ (value instanceof Double) || (value instanceof double[]) ||
+ (value instanceof String) || (value instanceof String[]) ||
+ (value instanceof Boolean) || (value instanceof boolean[])
+ );
+ }
+
+ /**
+ * Attempts to insert the specified key value pair into the specified bundle.
+ *
+ * @throws IllegalArgumentException if the value type can not be put into the bundle.
+ */
+ public static void putIntoBundle(BaseBundle baseBundle, String key, Object value) throws IllegalArgumentException {
+ if (value == null) {
+ throw new IllegalArgumentException("Unable to determine type of null values");
+ } else if (value instanceof Integer) {
+ baseBundle.putInt(key, (int) value);
+ } else if (value instanceof int[]) {
+ baseBundle.putIntArray(key, (int[]) value);
+ } else if (value instanceof Long) {
+ baseBundle.putLong(key, (long) value);
+ } else if (value instanceof long[]) {
+ baseBundle.putLongArray(key, (long[]) value);
+ } else if (value instanceof Double) {
+ baseBundle.putDouble(key, (double) value);
+ } else if (value instanceof double[]) {
+ baseBundle.putDoubleArray(key, (double[]) value);
+ } else if (value instanceof String) {
+ baseBundle.putString(key, (String) value);
+ } else if (value instanceof String[]) {
+ baseBundle.putStringArray(key, (String[]) value);
+ } else if (value instanceof Boolean) {
+ baseBundle.putBoolean(key, (boolean) value);
+ } else if (value instanceof boolean[]) {
+ baseBundle.putBooleanArray(key, (boolean[]) value);
+ } else if (value instanceof PersistableBundle) {
+ baseBundle.putAll((PersistableBundle) value);
+ } else {
+ throw new IllegalArgumentException("Objects of type " + value.getClass().getSimpleName()
+ + " can not be put into a " + BaseBundle.class.getSimpleName());
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/utils/ByteArraySerializer.kt b/app/src/main/java/moe/yuuta/workmode/utils/ByteArraySerializer.kt
new file mode 100644
index 0000000..8792e04
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/utils/ByteArraySerializer.kt
@@ -0,0 +1,26 @@
+package moe.yuuta.workmode.utils
+
+object ByteArraySerializer {
+ fun serialize(array: ByteArray): String {
+ val builder = StringBuilder()
+ array.toList().stream()
+ .forEachOrdered {
+ builder.append(it)
+ builder.append(',')
+ }
+ var result = builder.toString()
+ result = result.substring(0, result.length - 1)
+ return result
+ }
+
+ fun deserialize(value: String): ByteArray {
+ val list = value.split(',').toList()
+ var array = ByteArray(0)
+
+ list.stream()
+ .forEachOrdered {
+ array = array.plus(it.toByte())
+ }
+ return array
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/utils/Utils.kt b/app/src/main/java/moe/yuuta/workmode/utils/Utils.kt
new file mode 100644
index 0000000..2ac68e9
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/utils/Utils.kt
@@ -0,0 +1,127 @@
+package moe.yuuta.workmode.utils
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.os.Bundle
+import android.os.Process
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.core.view.children
+import com.google.android.material.tabs.TabLayout
+import moe.yuuta.workmode.BuildConfig
+import java.util.function.Predicate
+import java.util.stream.Collectors
+
+
+object Utils {
+ private val PROTECTED_UIDS = arrayOf(Process.SYSTEM_UID,
+ Process.myUid(),
+ Process.PHONE_UID,
+ 0,
+ 2000,
+ 1007,
+ 1010,
+ 1013,
+ 1019,
+ 1016,
+ 1017,
+ 1027,
+ 1002,
+ 1023,
+ 1032,
+ 1037,
+ 1041,
+ 1047,
+ 1053,
+ 1061,
+ 1067,
+ 1068,
+ 9999)
+
+ private val PROTECTED_PACKAGES = arrayOf(
+ "android",
+ BuildConfig.APPLICATION_ID
+ )
+
+ private val PROTECTED_PACKAGES_WIDE_MATCH = arrayOf(
+ "com.android."
+ )
+
+ private val WHITELIST_PKGS = arrayOf(
+ "com.android.chrome"
+ )
+
+ fun buildGeneralApplicationInfoFilter(context: Context): Predicate<ApplicationInfo> {
+ val i = Intent(Intent.ACTION_MAIN)
+ i.addCategory(Intent.CATEGORY_HOME)
+ val launchers = context.packageManager.queryIntentActivities(i, 0)
+ .stream()
+ .map {
+ return@map it.resolvePackageName
+ }
+ .collect(Collectors.toList())
+ return object : Predicate<ApplicationInfo> {
+ override fun test(it: ApplicationInfo): Boolean {
+ for (pkg in WHITELIST_PKGS)
+ if (pkg == it.packageName) {
+ return true
+ }
+ for (pkg in PROTECTED_PACKAGES)
+ if (pkg == it.packageName) {
+ return false
+ }
+ for (pkg in PROTECTED_PACKAGES_WIDE_MATCH)
+ if (it.packageName.startsWith(pkg)) {
+ return false
+ }
+ for (uid in PROTECTED_UIDS)
+ if (uid == it.uid) {
+ return false
+ }
+ if (launchers.contains(it.packageName)) {
+ return false
+ }
+ if (it.uid < Process.FIRST_APPLICATION_UID || it.uid > Process.LAST_APPLICATION_UID) {
+ return false
+ }
+ return context.packageManager.getLaunchIntentForPackage(it.packageName) != null
+ }
+ }
+ }
+
+ fun dumpExtras(bundle: Bundle?): String {
+ val builder = StringBuilder()
+ if (bundle != null) {
+ for (key in bundle.keySet()) {
+ val value = bundle.get(key)
+ builder.append("value: ")
+ builder.append(value?.toString())
+ builder.append(" key: ")
+ builder.append(key)
+ builder.append(" type: ")
+ builder.append(value.javaClass.name)
+ }
+ }
+ return builder.toString()
+ }
+
+ fun setViewTreeEnable(viewGroup: ViewGroup, isEnabled: Boolean) {
+ for (child in viewGroup.children) {
+ if (child is ViewGroup)
+ setViewTreeEnable(child, isEnabled)
+ else
+ child.isEnabled = isEnabled
+ }
+ }
+
+ /**
+ * Thanks to https://stackoverflow.com/questions/31702725/disable-tablayout
+ */
+ fun makeTabLayoutDisable(tabLayout: TabLayout, enable: Boolean) {
+ val tabStrip = tabLayout.getChildAt(0) as LinearLayout
+ for (i in 0 until tabStrip.childCount) {
+ tabStrip.getChildAt(i).setOnTouchListener { v, event -> !enable }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/res/color/switchbar_switch_thumb_tint.xml b/app/src/main/res/color/switchbar_switch_thumb_tint.xml
new file mode 100644
index 0000000..d274d93
--- /dev/null
+++ b/app/src/main/res/color/switchbar_switch_thumb_tint.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#FFFFFF" />
+</selector> \ No newline at end of file
diff --git a/app/src/main/res/color/switchbar_switch_track_tint.xml b/app/src/main/res/color/switchbar_switch_track_tint.xml
new file mode 100644
index 0000000..f316d32
--- /dev/null
+++ b/app/src/main/res/color/switchbar_switch_track_tint.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.
+ -->
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#BFFFFFFF" />
+</selector> \ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1f6bb29
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillType="evenOdd"
+ android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="78.5885"
+ android:endY="90.9159"
+ android:startX="48.7653"
+ android:startY="61.0927"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_add_24dp.xml b/app/src/main/res/drawable/ic_add_24dp.xml
new file mode 100644
index 0000000..e3979cd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_24dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_check_24dp.xml b/app/src/main/res/drawable/ic_check_24dp.xml
new file mode 100644
index 0000000..17aca2a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check_24dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..0d025f9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#008577"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_work_24dp.xml b/app/src/main/res/drawable/ic_work_24dp.xml
new file mode 100644
index 0000000..f211713
--- /dev/null
+++ b/app/src/main/res/drawable/ic_work_24dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z"/>
+</vector>
diff --git a/app/src/main/res/layout/activity_application_picker.xml b/app/src/main/res/layout/activity_application_picker.xml
new file mode 100644
index 0000000..2dfaac2
--- /dev/null
+++ b/app/src/main/res/layout/activity_application_picker.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_apps"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <ProgressBar
+ android:id="@+id/progress_load"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fab_ok"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:src="@drawable/ic_check_24dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..23f0416
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity">
+
+ <com.android.settings.widget.SwitchBar
+ android:id="@+id/switch_bar"
+ android:layout_height="?attr/actionBarSize"
+ android:layout_width="match_parent"
+ android:theme="?attr/switchBarTheme"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@id/switch_bar"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+ <ProgressBar
+ android:id="@+id/progress_apply"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/guideline"
+ android:indeterminate="true"
+ android:paddingTop="-4dp"
+ style="@style/Widget.AppCompat.ProgressBar.Horizontal"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/guideline"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_begin="2dp" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tab"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@id/switch_bar"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toBottomOf="@id/tab"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_apps"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <TextView
+ android:id="@+id/welcome_tip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:text="@string/blacklist_welcome"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fab_add"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:src="@drawable/ic_add_24dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_application.xml b/app/src/main/res/layout/item_application.xml
new file mode 100644
index 0000000..7ab17dc
--- /dev/null
+++ b/app/src/main/res/layout/item_application.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="56dp">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginStart="16dp"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:src="@android:mipmap/sym_def_app_icon"/>
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ android:layout_marginStart="16dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:textSize="16sp"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
+ tools:text="App"/>
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_application_select.xml b/app/src/main/res/layout/item_application_select.xml
new file mode 100644
index 0000000..c5068d4
--- /dev/null
+++ b/app/src/main/res/layout/item_application_select.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="56dp">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginStart="16dp"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:src="@android:mipmap/sym_def_app_icon"/>
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ android:layout_marginStart="16dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:textSize="16sp"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
+ tools:text="App"/>
+
+ <CheckBox
+ android:id="@android:id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginEnd="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"/>
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
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..b95c6ea
--- /dev/null
+++ b/app/src/main/res/layout/switch_bar.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <TextView
+ android:id="@+id/switch_text"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.Switch"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="18sp"
+ android:textAlignment="viewStart" />
+
+ <ImageView
+ android:id="@+id/restricted_icon"
+ android:layout_width="@dimen/restricted_icon_size"
+ android:layout_height="@dimen/restricted_icon_size"
+ android:theme="@android:style/Theme.Material"
+ android:layout_gravity="center_vertical"
+ android:layout_marginEnd="@dimen/restricted_icon_margin_end"
+ android:visibility="gone" />
+
+ <com.android.settings.widget.ToggleSwitch
+ android:id="@+id/switch_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:theme="@style/Widget.SwitchBar.Switch" />
+
+</merge> \ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..df1c7f3
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:title="@string/feedback">
+ <menu>
+ <group>
+ <item android:id="@+id/action_get_logs"
+ android:title="@string/get_logs" />
+ <item android:id="@+id/action_feedback"
+ android:title="@string/feedback" />
+ </group>
+ </menu>
+ </item>
+ <item android:title="@string/about">
+ <menu>
+ <group>
+ <item android:id="@+id/action_check_update"
+ android:title="@string/check_update" />
+ <item android:id="@+id/action_oss"
+ android:title="@string/oss_license_title" />
+ </group>
+ </menu>
+ </item>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..898f3ed
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dffca36
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..64ba76f
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dae5e08
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e5ed465
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..14ed0af
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b0907ca
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d8ae031
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2c18de9
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..beed3cd
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..9e90cdd
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <attr name="switchBarTheme" format="reference" />
+ <attr name="switchBarMarginStart" format="dimension" />
+ <attr name="switchBarMarginEnd" format="dimension" />
+ <attr name="switchBarBackgroundColor" format="color" />
+ <attr name="switchBarBackgroundActivatedColor" format="color" />
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..5a06de3
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#008577</color>
+ <color name="colorPrimaryDark">#00574B</color>
+ <color name="colorAccent">#D81B60</color>
+ <color name="switch_bar_background">#ff80868B</color>
+ <color name="preference_fallback_accent_color">#ff80cbc4</color>
+</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..fead519
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="min_tap_target_size">48dp</dimen>
+ <dimen name="switchbar_subsettings_margin_start">72dp</dimen>
+ <dimen name="switchbar_subsettings_margin_end">16dp</dimen>
+ <dimen name="restricted_icon_size">16dp</dimen>
+ <dimen name="restricted_icon_margin_end">16dp</dimen>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..a536b1e
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,27 @@
+<resources>
+ <string name="app_name">Work Mode</string>
+ <string name="work">Work</string>
+ <string name="suspended_message">This app is disabled because you want to enjoy your work.</string>
+ <!-- Switch On/Off -->
+ <string name="switch_on_text">On</string>
+ <string name="switch_off_text">Off</string>
+ <string name="apps">Apps</string>
+ <string name="add">Add</string>
+ <string name="blacklist">Blacklist</string>
+ <string name="whitelist">Whitelist</string>
+ <string name="blacklist_welcome">Add apps to be disabled when you are working</string>
+ <string name="whitelist_welcome">Add apps to be enabled only when you are working</string>
+ <string name="blacklist_toggle_title">Switch to blacklist</string>
+ <string name="blacklist_toggle_information">These apps will be disabled and others will be enabled when you are working</string>
+ <string name="whitelist_toggle_title">Switch to whitelist</string>
+ <string name="whitelist_toggle_information">These apps will be enabled and others will be disabled when you are working</string>
+ <string name="error_load_applications">Cannot get applications</string>
+ <string name="error_apply">Unable to apply settings</string>
+ <string name="feedback">Feedback</string>
+ <string name="get_logs">Get logs</string>
+ <string name="check_update">Check update</string>
+ <string name="about">About</string>
+ <string name="feedback_subject">Work Mode feedback</string>
+ <string name="update_available">%1$s is available</string>
+ <string name="view">View</string>
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..7af43d0
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,35 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ <item name="switchBarTheme">@style/ThemeOverlay.SwitchBar.Settings</item>
+ </style>
+
+
+ <style name="TextAppearance.Switch" parent="@style/TextAppearance.AppCompat.Title" />
+
+ <style name="Widget.SwitchBar.Switch" parent="@style/Widget.AppCompat.CompoundButton.Switch">
+ <item name="trackTint">@color/switchbar_switch_track_tint</item>
+ <item name="android:thumbTint">@color/switchbar_switch_thumb_tint</item>
+ <item name="android:minHeight">@dimen/min_tap_target_size</item>
+ </style>
+
+ <style name="TextAppearance.Small" parent="@style/TextAppearance.AppCompat.Small" />
+
+ <style name="TextAppearance.Small.SwitchBar">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="ThemeOverlay.SwitchBar.Settings" parent="@style/ThemeOverlay.AppCompat.ActionBar">
+ <item name="android:textColorPrimary">@android:color/white</item>
+ <item name="switchBarMarginStart">@dimen/switchbar_subsettings_margin_start</item>
+ <item name="switchBarMarginEnd">@dimen/switchbar_subsettings_margin_end</item>
+ <item name="switchBarBackgroundColor">@color/switch_bar_background</item>
+ <item name="switchBarBackgroundActivatedColor">?attr/colorAccent</item>
+ </style>
+</resources>
diff --git a/app/src/main/res/xml/filepaths.xml b/app/src/main/res/xml/filepaths.xml
new file mode 100644
index 0000000..513a653
--- /dev/null
+++ b/app/src/main/res/xml/filepaths.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+ <external-cache-path
+ name="logs"
+ path="logs" />
+</paths> \ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..1a9979d
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,29 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.21'
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0-alpha03'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.google.android.gms:oss-licenses-plugin:0.9.4'
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..23339e0
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b0e751c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Feb 22 18:50:48 PST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'