Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fcc6b6ce authored by Jaewan Kim's avatar Jaewan Kim Committed by Gerrit Code Review
Browse files

Merge "Enable Linux terminal app via developer settings" into main

parents 71c59bae 591d4fd9
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -839,4 +839,7 @@

    <!-- Package name for diagnostics app. -->
    <string name="config_device_diagnostics_package_name" translatable="false">com.android.devicediagnostics</string>

    <!-- Package name for Linux terminal app -->
    <string name="config_linux_terminal_app_package_name" translatable="false">com.android.virtualization.terminal</string>
</resources>
+5 −0
Original line number Diff line number Diff line
@@ -199,6 +199,11 @@
            android:title="@string/enable_terminal_title"
            android:summary="@string/enable_terminal_summary" />

        <SwitchPreferenceCompat
            android:key="enable_linux_terminal"
            android:title="@string/enable_linux_terminal_title"
            android:summary="@string/enable_linux_terminal_summary" />

        <SwitchPreferenceCompat
            android:key="bugreport_in_power"
            android:title="@string/bugreport_in_power"
+1 −0
Original line number Diff line number Diff line
@@ -676,6 +676,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
        controllers.add(new WirelessDebuggingPreferenceController(context, lifecycle));
        controllers.add(new AdbAuthorizationTimeoutPreferenceController(context));
        controllers.add(new LocalTerminalPreferenceController(context));
        controllers.add(new LinuxTerminalPreferenceController(context));
        controllers.add(new BugReportInPowerPreferenceController(context));
        controllers.add(new AutomaticSystemServerHeapDumpPreferenceController(context));
        controllers.add(new MockLocationAppPreferenceController(context, fragment));
+127 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.development;

import android.content.Context;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;

import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.development.DeveloperOptionsPreferenceController;

public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenceController
        implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
    private static final String TAG = "LinuxTerminalPrefCtrl";

    private static final String ENABLE_TERMINAL_KEY = "enable_linux_terminal";

    @NonNull
    private final PackageManager mPackageManager;

    @Nullable
    private final String mTerminalPackageName;

    public LinuxTerminalPreferenceController(@NonNull Context context) {
        super(context);
        mPackageManager = mContext.getPackageManager();

        String packageName = mContext.getString(R.string.config_linux_terminal_app_package_name);
        mTerminalPackageName =
                isPackageInstalled(mPackageManager, packageName) ? packageName : null;

        Log.d(TAG, "Terminal app package name=" + packageName + ", isAvailable=" + isAvailable());
    }

    // Avoid lazy initialization because this may be called before displayPreference().
    @Override
    public boolean isAvailable() {
        // Returns true only if the terminal app is installed which only happens when the build flag
        // RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES is true.
        // TODO(b/343795511): Add explicitly check for the flag when it's accessible from Java code.
        return getTerminalPackageName() != null;
    }

    @Override
    @NonNull
    public String getPreferenceKey() {
        return ENABLE_TERMINAL_KEY;
    }

    @Override
    public void displayPreference(@NonNull PreferenceScreen screen) {
        super.displayPreference(screen);
        mPreference.setEnabled(isAvailable());
    }

    @Override
    public boolean onPreferenceChange(
                @NonNull Preference preference, @NonNull Object newValue) {
        String packageName = getTerminalPackageName();
        if (packageName == null) {
            return false;
        }

        boolean terminalEnabled = (Boolean) newValue;
        int state = terminalEnabled
                ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
        mPackageManager.setApplicationEnabledSetting(packageName, state, /* flags=*/ 0);
        ((TwoStatePreference) mPreference).setChecked(terminalEnabled);
        return true;
    }

    @Override
    public void updateState(@NonNull Preference preference) {
        String packageName = getTerminalPackageName();
        if (packageName == null) {
            return;
        }

        boolean isTerminalEnabled = mPackageManager.getApplicationEnabledSetting(packageName)
                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
        ((TwoStatePreference) mPreference).setChecked(isTerminalEnabled);
    }

    // Can be mocked for testing
    @VisibleForTesting
    @Nullable
    String getTerminalPackageName() {
        return mTerminalPackageName;
    }

    private static boolean isPackageInstalled(PackageManager manager, String packageName) {
        if (TextUtils.isEmpty(packageName)) {
            return false;
        }
        try {
            return manager.getPackageInfo(
                    packageName,
                    PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) != null;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }
    }
}
+131 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.development;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;

import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class LinuxTerminalPreferenceControllerTest {

    @Mock
    private Context mContext;
    @Mock
    private SwitchPreference mPreference;
    @Mock
    private PreferenceScreen mPreferenceScreen;
    @Mock
    private PackageManager mPackageManager;
    @Mock
    private ApplicationInfo mApplicationInfo;

    private String mTerminalPackageName = "com.android.virtualization.terminal";
    private LinuxTerminalPreferenceController mController;

    @Before
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
        doReturn(mPackageManager).when(mContext).getPackageManager();
        doReturn(mApplicationInfo).when(mPackageManager).getApplicationInfo(
                eq(mTerminalPackageName), any());

        mController = spy(new LinuxTerminalPreferenceController(mContext));
        doReturn(true).when(mController).isAvailable();
        doReturn(mTerminalPackageName).when(mController).getTerminalPackageName();
        when(mPreferenceScreen.findPreference(mController.getPreferenceKey()))
                .thenReturn(mPreference);
        mController.displayPreference(mPreferenceScreen);
    }

    @Test
    public void isAvailable_whenPackageNameIsNull_returnsFalse() throws Exception {
        mController = spy(new LinuxTerminalPreferenceController(mContext));
        doReturn(null).when(mController).getTerminalPackageName();

        assertThat(mController.isAvailable()).isFalse();
    }

    @Test
    public void isAvailable_whenAppDoesNotExist_returnsFalse() throws Exception {
        doThrow(new NameNotFoundException()).when(mPackageManager).getApplicationInfo(
                eq(mTerminalPackageName), any());

        mController = spy(new LinuxTerminalPreferenceController(mContext));

        assertThat(mController.isAvailable()).isFalse();
    }

    @Test
    public void onPreferenceChanged_turnOnTerminal() {
        mController.onPreferenceChange(null, true);

        verify(mPackageManager).setApplicationEnabledSetting(
                mTerminalPackageName,
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                /* flags= */ 0);
    }

    @Test
    public void onPreferenceChanged_turnOffTerminal() {
        mController.onPreferenceChange(null, false);

        verify(mPackageManager).setApplicationEnabledSetting(
                mTerminalPackageName,
                PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
                /* flags= */ 0);
    }

    @Test
    public void updateState_preferenceShouldBeChecked() {
        when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName))
                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
        mController.updateState(mPreference);

        verify(mPreference).setChecked(true);
    }

    @Test
    public void updateState_preferenceShouldNotBeChecked() {
        when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName))
                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT);
        mController.updateState(mPreference);

        verify(mPreference).setChecked(false);
    }
}